Skip to content

sample_model

BackgroundModel

Bases: ModelBase

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

Parameters:

Name Type Description Default
display_name str

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 | None

Unit of the model. Defaults to "meV".

'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

Attributes:

Name Type Description
unit str | Unit

Unit of the model.

components list[ModelComponent]

List of ModelComponents in the model.

Q ndarray | Numeric | list | ArrayLike | Variable | None

Q values of the model.

Source code in src/easydynamics/sample_model/background_model.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class BackgroundModel(ModelBase):
    """BackgroundModel represents a model of the background in an
    experiment at various Q.

    Args:
        display_name (str): Display name of the model.
        unique_name (str | None): Unique name of the model. If None, a
            unique name will be generated.
        unit (str | sc.Unit | None): Unit of the model. Defaults to
            "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.
        Q (Q_type | None): Q values for the model. If None, Q is not
            set.

    Attributes:
        unit (str | sc.Unit): Unit of the model.
        components (list[ModelComponent]): List of ModelComponents in
            the model.
        Q (np.ndarray | Numeric | list | ArrayLike | sc.Variable | None):
            Q values of the model.
    """

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

        Args:
            display_name (str): Display name of the model.
            unique_name (str | None): Unique name of the model. If None,
                a unique name will be generated.
            unit (str | sc.Unit | None): Unit of the model. Defaults to
                "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.
            Q (Q_type | 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

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 | None

Unit of the model. Defaults to "meV".

'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
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,
    display_name: str = 'MyBackgroundModel',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    components: ComponentCollection | ModelComponent | None = None,
    Q: Q_type | None = None,
):
    """Initialize the BackgroundModel.

    Args:
        display_name (str): Display name of the model.
        unique_name (str | None): Unique name of the model. If None,
            a unique name will be generated.
        unit (str | sc.Unit | None): Unit of the model. Defaults to
            "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.
        Q (Q_type | 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.

Parameters:

Name Type Description Default
display_name str

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.

None
unit str | Unit

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

'meV'
scale Numeric

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

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s. Defaults to 1.0.

1.0

Attributes:

Name Type Description
unit str | Unit

Unit of the diffusion model.

scale Parameter

Scale parameter of the diffusion model.

diffusion_coefficient Parameter

Diffusion coefficient D in m^2/s.

Example usage:

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

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

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

    Attributes:
        unit (str | sc.Unit): Unit of the diffusion model.
        scale (Parameter): Scale parameter of the diffusion model.
        diffusion_coefficient (Parameter): Diffusion coefficient D in
            m^2/s.

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

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

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

        Raises:
            TypeError: If scale or diffusion_coefficient is not a
                number.
            ValueError: If scale is negative.
            UnitError: If unit is not a string or scipp Unit.
        """
        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 = DescriptorNumber.from_scipp('hbar', scipp_hbar)
        self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
        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.

        Args:
            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.

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

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

        Q = _validate_and_convert_Q(Q)

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

        return width

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

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

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

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

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

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

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

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

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

        Returns:
            List[ComponentCollection]: List of ComponentCollections with
                Lorentzian components 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).

        Raises:
            TypeError: If component_display_name is not a string.
        """
        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.

        Args:
         Q (float): Scattering vector in 1/angstrom

         Returns:
             str: Dependency expression for the width.

         Raises:
             TypeError: If Q is not a float.
        """
        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.

        Args:
            QISF (float): Quasielastic Incoherent Scattering Function.

        Returns:
            str: Dependency expression for the area.

        Raises:
            TypeError: If QISF is not a float.
        """
        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

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.

None
unit str | Unit

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

'meV'
scale Numeric

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

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s. Defaults to 1.0.

1.0

Raises:

Type Description
TypeError

If scale or diffusion_coefficient is not a number.

ValueError

If scale is negative.

UnitError

If unit is not a string or scipp Unit.

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

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

    Raises:
        TypeError: If scale or diffusion_coefficient is not a
            number.
        ValueError: If scale is negative.
        UnitError: If unit is not a string or scipp Unit.
    """
    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 = DescriptorNumber.from_scipp('hbar', scipp_hbar)
    self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
    self._diffusion_coefficient = diffusion_coefficient

__repr__()

String representation of the BrownianTranslationalDiffusion model.

Returns:

Name Type Description
str str

String representation of the BrownianTranslationalDiffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
333
334
335
336
337
338
339
340
341
342
343
344
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 ndarray | Numeric | list | ArrayLike

Scattering vector in 1/angstrom

required

Returns:

Type Description
ndarray

np.ndarray: EISF values (dimensionless).

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

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

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

calculate_QISF(Q)

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

Parameters:

Name Type Description Default
Q ndarray | Numeric | list | ArrayLike

Scattering vector in 1/angstrom

required

Returns:

Type Description
ndarray

np.ndarray: QISF values (dimensionless).

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

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

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

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

calculate_width(Q)

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

Parameters:

Name Type Description Default
Q ndarray | Numeric | list | ArrayLike

Scattering vector in 1/angstrom

required

Returns:

Type Description
ndarray

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

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

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

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

    Q = _validate_and_convert_Q(Q)

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

    return width

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

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

Parameters:

Name Type Description Default
Q Number, list, or np.ndarray

Scattering vector values.

required
component_display_name str

Name of the Lorentzian component.

'Brownian translational diffusion'

Returns:

Type Description
List[ComponentCollection]

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

Raises:

Type Description
TypeError

If component_display_name is not a string.

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

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

    Returns:
        List[ComponentCollection]: List of ComponentCollections with
            Lorentzian components 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).

    Raises:
        TypeError: If component_display_name is not a string.
    """
    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:

Name Type Description
Parameter Parameter

Diffusion coefficient D in m^2/s.

ComponentCollection

Bases: ModelBase

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

Parameters:

Name Type Description Default
unit str | Unit

Unit of the sample model. Defaults to "meV".

'meV'
display_name str

Display name of the sample model.

'MyComponentCollection'
unique_name str | None

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

None
components List[ModelComponent] | None

Initial model components to add to the ComponentCollection.

None

Attributes:

Name Type Description
components List[ModelComponent]

List of model components in the collection.

unit str | Unit

Unit of the sample model.

display_name str

Display name of the sample model.

unique_name str

Unique name of the sample model.

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

    Args:
        unit (str | sc.Unit): Unit of the sample model. Defaults to
            "meV".
        display_name (str): Display name of the sample model.
        unique_name (str | None): Unique name of the sample model.
            If None, a unique_name is automatically generated.
        components (List[ModelComponent] | None): Initial model
            components to add to the ComponentCollection.

    Attributes:
        components (List[ModelComponent]): List of model components in
            the collection.
        unit (str | sc.Unit): Unit of the sample model.
        display_name (str): Display name of the sample model.
        unique_name (str): Unique name of the sample model.
    """

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

        Args:
        unit (str | sc.Unit | None): Unit of the sample model.
            Defaults to "meV".
        display_name (str | None): Display name of the sample model.
        unique_name (str | None): Unique name of the sample model.
            Defaults to None.
        components (List[ModelComponent] | 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.
            ValueError: If components contains duplicate unique names.
        """

        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.

        Args:
            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.

        Args:
            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.

        Args:
            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.'
            )
        )  # noqa: E501

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

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

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

        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.

        Args:
        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.

        Args:
            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]:
        return list(self._components)

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

        self._components = components

    @property
    def is_empty(self) -> bool:
        return not self._components

    @is_empty.setter
    def is_empty(self, value: bool) -> None:
        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.
            ValueError: 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,
                )

        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[Parameter]: 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.

        Args:
            x (Number, list, np.ndarray, sc.Variable, or 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.

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

        Returns:
            np.ndarray: Evaluated values for the specified component.
        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.
        """
        if not self.components:
            raise ValueError('No components in the model to evaluate.')

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

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

        component = matches[0]

        result = component.evaluate(x)

        return result

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

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

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

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

        Args:
            item (str or ModelComponent): The component name or instance
                to check for.

        Returns:
            bool: True if the component exists, False otherwise.
        """

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

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

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

The component name or instance to check for.

required

Returns:

Name Type Description
bool bool

True if the component exists, False otherwise.

Source code in src/easydynamics/sample_model/component_collection.py
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
def __contains__(self, item: str | ModelComponent) -> bool:
    """Check if a component with the given name or instance exists
    in the ComponentCollection.

    Args:
        item (str or ModelComponent): The component name or instance
            to check for.

    Returns:
        bool: True if the component exists, False otherwise.
    """

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

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

Initialize a new ComponentCollection.

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

Raises:

Type Description
TypeError

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

ValueError

If components contains duplicate unique names.

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

    Args:
    unit (str | sc.Unit | None): Unit of the sample model.
        Defaults to "meV".
    display_name (str | None): Display name of the sample model.
    unique_name (str | None): Unique name of the sample model.
        Defaults to None.
    components (List[ModelComponent] | 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.
        ValueError: If components contains duplicate unique names.
    """

    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:

Name Type Description
str str

String representation of the ComponentCollection.

Source code in src/easydynamics/sample_model/component_collection.py
441
442
443
444
445
446
447
448
449
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.

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

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

    Args:
    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
301
302
303
def clear_components(self) -> None:
    """Remove all components."""
    self._components.clear()

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.

UnitError

If any component cannot be converted to the specified unit.

Source code in src/easydynamics/sample_model/component_collection.py
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 ComponentCollection and all its
    components.

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

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

    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 Number, list, np.ndarray, sc.Variable, or sc.DataArray

Energy axis.

required

Returns np.ndarray: Evaluated model values.

Source code in src/easydynamics/sample_model/component_collection.py
353
354
355
356
357
358
359
360
361
362
363
364
365
366
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the sum of all components.

    Args:
        x (Number, list, np.ndarray, sc.Variable, or 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 Number, list, np.ndarray, sc.Variable, or sc.DataArray

Energy axis.

required
unique_name str

Component unique name.

required

Returns:

Type Description
ndarray

np.ndarray: Evaluated values for the specified component.

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.

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

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

    Returns:
        np.ndarray: Evaluated values for the specified component.
    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.
    """
    if not self.components:
        raise ValueError('No components in the model to evaluate.')

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

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

    component = matches[0]

    result = component.evaluate(x)

    return result

fix_all_parameters()

Fix all free parameters in the model.

Source code in src/easydynamics/sample_model/component_collection.py
406
407
408
409
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
411
412
413
414
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[Parameter]: List of parameters in the component.

Source code in src/easydynamics/sample_model/component_collection.py
344
345
346
347
348
349
350
351
def get_all_variables(self) -> list[DescriptorBase]:
    """Get all parameters from the model component.

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

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

list_component_names()

List the names of all components in the model.

Returns:

Type Description
List[str]

List[str]: List of unique names of the components in the

List[str]

collection.

Source code in src/easydynamics/sample_model/component_collection.py
291
292
293
294
295
296
297
298
299
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.

ValueError

If the total area is zero or not finite, which would prevent normalization.

Source code in src/easydynamics/sample_model/component_collection.py
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
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.
        ValueError: 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,
            )

    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: 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
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 remove_component(self, unique_name: str) -> None:
    """Remove a component from the collection by its unique name.

    Args:
        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

str | sc.Unit | None: The unit of the ComponentCollection,

str | Unit | None

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.

Parameters:

Name Type Description Default
area Int | float

Area under the curve.

1.0
center Int | float

Resonance frequency, approximately the peak position.

1.0
width Int | float

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

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Attributes:

Name Type Description
area Parameter

Area under the curve.

center Parameter

Resonance frequency, approximately the peak position.

width Parameter

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

unit str | Unit

Unit of the parameters.

display_name str | None

Display name of the component.

unique_name str | None

Unique name of the component.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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
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.


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

    Attributes:
        area (Parameter): Area under the curve.
        center (Parameter): Resonance frequency, approximately the
            peak position.
        width (Parameter): Damping constant, approximately the
            half width at half max (HWHM) of the peaks.
        unit (str | sc.Unit): Unit of the parameters.
        display_name (str | None): Display name of the component.
        unique_name (str | None): Unique name of the component.
    """

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

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

        Raises:
            TypeError: If any of the parameters are not numbers or
                Parameters.
            ValueError: If center or width are not positive.
        """

        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.

        Args:
            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.

        Args:
            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.

        Args:
            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.

        Args:
            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}, unit = {self._unit},\n \
        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 Int | float

Area under the curve.

1.0
center Int | float

Resonance frequency, approximately the peak position.

1.0
width Int | float

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

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Raises:

Type Description
TypeError

If any of the parameters are not numbers or Parameters.

ValueError

If center or width are not positive.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
 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
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,
):
    """Initialize the Damped Harmonic Oscillator.

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

    Raises:
        TypeError: If any of the parameters are not numbers or
            Parameters.
        ValueError: If center or width are not positive.
    """

    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:

Name Type Description
str str

A string representation of the Damped Harmonic Oscillator.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
209
210
211
212
213
214
215
216
217
218
219
220
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}, unit = {self._unit},\n \
    area = {self.area},\n center = {self.center},\n width = {self.width})'
    )

area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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

np.ndarray: The intensity of the DHO at the given x values.

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

    Args:
        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:

Name Type Description
Parameter 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.

Parameters:

Name Type Description Default
center Int | float | None

Center of the delta function. If None, defaults to 0 and is fixed.

None
area Int | float

Total area under the curve.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Attributes:

Name Type Description
center Parameter

Center of the delta function.

area Parameter

Total area under the curve.

unit str | Unit

Unit of the parameters.

display_name str | None

Name of the component.

unique_name str | None

Unique name of the component.

Source code in src/easydynamics/sample_model/components/delta_function.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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
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.

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

    Attributes:
        center (Parameter): Center of the delta function.
        area (Parameter): Total area under the curve.
        unit (str | sc.Unit): Unit of the parameters.
        display_name (str | None): Name of the component.
        unique_name (str | None): Unique name of the component.
    """

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

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

        Raises:
            TypeError: If center is not a number or None.
            TypeError: If area is not a number.
            TypeError: If unit is not a string or sc.Unit.
        """
        # 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.

        Args:
            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.

        Args:
            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.

        Args:
            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:
                left = x[1] - x[0] if x.size > 1 else 0.5
            else:
                left = x[i] - x[i - 1]

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

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

            model[i] = area / bin_width

        return model

    def __repr__(self) -> 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 \
        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 Int | float | None

Center of the delta function. If None, defaults to 0 and is fixed.

None
area Int | float

Total area under the curve.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Raises:

Type Description
TypeError

If center is not a number or None.

TypeError

If area is not a number.

TypeError

If unit is not a string or sc.Unit.

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

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

    Raises:
        TypeError: If center is not a number or None.
        TypeError: If area is not a number.
        TypeError: If unit is not a string or sc.Unit.
    """
    # 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:

Name Type Description
str str

A string representation of the Delta function.

Source code in src/easydynamics/sample_model/components/delta_function.py
184
185
186
187
188
189
190
191
192
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 \
    area = {self.area},\n center = {self.center}'

area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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

np.ndarray: The evaluated Delta function at the given x values.

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

    Args:
        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:
            left = x[1] - x[0] if x.size > 1 else 0.5
        else:
            left = x[i] - x[i - 1]

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

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

        model[i] = area / bin_width

    return model

Gaussian

Bases: CreateParametersMixin, ModelComponent

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.

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

Attributes: area (Parameter): Area of the Gaussian. center (Parameter): Center of the Gaussian. width (Parameter): Standard deviation of the Gaussian. unit (str | sc.Unit): Unit of the parameters. display_name (str | None): Name of the component. unique_name (str | None): Unique name of the component.

Source code in src/easydynamics/sample_model/components/gaussian.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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
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.

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

     Attributes:
         area (Parameter): Area of the Gaussian.
         center (Parameter): Center of the Gaussian.
         width (Parameter): Standard deviation of the Gaussian.
         unit (str | sc.Unit): Unit of the parameters.
         display_name (str | None): Name of the component.
         unique_name (str | None): Unique name of the component.
    """

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

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

        Raises:
            TypeError: If area, center, or width are not numbers or
                Parameters.
            ValueError: If width is not positive.
            TypeError: If unit is not a string or sc.Unit.
        """
        # 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.

        Args:
            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 center parameter value.

        Args:
            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.

        Args:
            value (Numeric | None): 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.


        Args:
            x (Numeric or list or np.ndarray or sc.Variable or 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 \
            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 Int | float | Parameter | None

Area of the Gaussian.

1.0
center Int | float | Parameter | None

Center of the Gaussian. If None, defaults to 0 and is fixed.

None
width Int | float | Parameter | None

Standard deviation.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Raises:

Type Description
TypeError

If area, center, or width are not numbers or Parameters.

ValueError

If width is not positive.

TypeError

If unit is not a string or sc.Unit.

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

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

    Raises:
        TypeError: If area, center, or width are not numbers or
            Parameters.
        ValueError: If width is not positive.
        TypeError: If unit is not a string or sc.Unit.
    """
    # 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:

Name Type Description
str str

A string representation of the Gaussian.

Source code in src/easydynamics/sample_model/components/gaussian.py
221
222
223
224
225
226
227
228
229
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 \
        area = {self.area},\n center = {self.center},\n width = {self.width})'

area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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 or list or ndarray or Variable or DataArray

The x values at which to evaluate the Gaussian.

required

Returns:

Type Description
ndarray

np.ndarray: The intensity of the Gaussian at the given x values.

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


    Args:
        x (Numeric or list or np.ndarray or sc.Variable or 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:

Name Type Description
Parameter 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.

Parameters:

Name Type Description Default
display_name str | None

The display name of the InstrumentModel. Default is "MyInstrumentModel".

'MyInstrumentModel'
unique_name str | None

The unique name of the InstrumentModel. Default is None.

None
Q ndarray | list | Variable | 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. Default is None.

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. Default is None.

None
energy_offset float | int | None

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

None
unit str | Unit

The unit of the energy axis. Default is 'meV'.

'meV'

Attributes:

Name Type Description
resolution_model ResolutionModel

The resolution model of the instrument.

background_model BackgroundModel

The background model of the instrument.

Q ndarray | None

The Q values where the instrument is modelled.

energy_offset Parameter

The template energy offset Parameter of the instrument. Will be copied to each Q value.

unit str | Unit

The unit of 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
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.

    Args:
        display_name (str | None): The display name of the
            InstrumentModel. Default is "MyInstrumentModel".
        unique_name (str | None): The unique name of the
            InstrumentModel. Default is None.
        Q (np.ndarray | list | sc.Variable | None): The Q values where
            the instrument is modelled.
        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.
            Default is 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. Default is None.
        energy_offset (float | int | None): Template energy offset of
            the instrument. Will be copied to each Q value. If None, the
            energy offset will be 0. Default is None.
        unit (str | sc.Unit): The unit of the energy axis. Default is
            'meV'.

    Attributes:
        resolution_model (ResolutionModel): The resolution model of the
            instrument.
        background_model (BackgroundModel): The background model of the
            instrument.
        Q (np.ndarray | None): The Q values where the instrument is
            modelled.
        energy_offset (Parameter): The template energy offset Parameter
            of the instrument. Will be copied to each Q value.
        unit (str | sc.Unit): The unit of 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',
    ):
        """Initialize an InstrumentModel.

        Args:
            display_name (str | None): The display name of the
                InstrumentModel. Default is "MyInstrumentModel".
            unique_name (str | None): The unique name of the
                InstrumentModel. Default is None.
            Q (np.ndarray | list | sc.Variable | None): The Q values
                where the instrument is modelled.
            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. Default is 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.
                Default is None.
            energy_offset (float | int | None): Template energy offset
                of the instrument. Will be copied to each Q value. If
                None, the energy offset will be 0. Default is None.
            unit (str | sc.Unit): The unit of the energy axis. Default
                is 'meV'.

        Raises:
            TypeError: If resolution_model is not a ResolutionModel or
                None
            TypeError: If background_model is not a BackgroundModel or
                None
            TypeError: If energy_offset is not a number or None
            UnitError: If unit is not a valid unit string or scipp Unit.
        """
        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):
        """Set the resolution model of the instrument.

        Args:
            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):
        """Set the background model of the instrument.

        Args:
            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 or 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.

        Args:
            value (Q_type | None): The new Q values for the
            InstrumentModel.

        Raises:
            TypeError: If value is not a valid Q_type or None.
        """
        self._Q = _validate_and_convert_Q(value)
        self._on_Q_change()

    @property
    def unit(self) -> 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.

        Args:
            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.'
            )
        )  # noqa: E501

    @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):
        """Set the offset parameter of the instrument model.

        Args:
            value (float | int): 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 convert_unit(self, unit_str: str | sc.Unit) -> None:
        """Convert the unit of the InstrumentModel.

        Args:
            unit_str (str | sc.Unit): The unit to convert to.

        Raises:
            TypeError: If unit_str is not a string or scipp Unit.
            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.

        Args:
            Q_index (int | None): The index of the Q value to get
                variables for. If None, get variables for all Q values.

        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.

        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 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 get_energy_offset_at_Q(self, Q_index: int) -> Parameter:
        """Get the energy offset Parameter at a specific Q index.

        Args:
            Q_index (int): The index of the Q value to get the energy
                offset for.

        Returns:
            Parameter: The energy offset Parameter at the specified Q
                index.

        Raises:
            ValueError: If no Q values are set in the InstrumentModel.
            IndexError: If Q_index is out of bounds.
        """
        if self._Q is None:
            raise ValueError('No Q values are set in the InstrumentModel.')

        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]

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

    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

np.ndarray or 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 | None

The display name of the InstrumentModel. Default is "MyInstrumentModel".

'MyInstrumentModel'
unique_name str | None

The unique name of the InstrumentModel. Default is None.

None
Q ndarray | list | Variable | 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. Default is None.

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. Default is None.

None
energy_offset float | int | None

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

None
unit str | Unit

The unit of the energy axis. Default is 'meV'.

'meV'

Raises:

Type Description
TypeError

If resolution_model is not a ResolutionModel or None

TypeError

If background_model is not a BackgroundModel or None

TypeError

If energy_offset is not a number or None

UnitError

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

Source code in src/easydynamics/sample_model/instrument_model.py
 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
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',
):
    """Initialize an InstrumentModel.

    Args:
        display_name (str | None): The display name of the
            InstrumentModel. Default is "MyInstrumentModel".
        unique_name (str | None): The unique name of the
            InstrumentModel. Default is None.
        Q (np.ndarray | list | sc.Variable | None): The Q values
            where the instrument is modelled.
        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. Default is 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.
            Default is None.
        energy_offset (float | int | None): Template energy offset
            of the instrument. Will be copied to each Q value. If
            None, the energy offset will be 0. Default is None.
        unit (str | sc.Unit): The unit of the energy axis. Default
            is 'meV'.

    Raises:
        TypeError: If resolution_model is not a ResolutionModel or
            None
        TypeError: If background_model is not a BackgroundModel or
            None
        TypeError: If energy_offset is not a number or None
        UnitError: If unit is not a valid unit string or scipp Unit.
    """
    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:

Name Type Description
str str

A string representation of the InstrumentModel.

Source code in src/easydynamics/sample_model/instrument_model.py
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
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:

Name Type Description
BackgroundModel BackgroundModel

The background model of the instrument.

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
TypeError

If unit_str is not a string or scipp Unit.

ValueError

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

Source code in src/easydynamics/sample_model/instrument_model.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
def convert_unit(self, unit_str: str | sc.Unit) -> None:
    """Convert the unit of the InstrumentModel.

    Args:
        unit_str (str | sc.Unit): The unit to convert to.

    Raises:
        TypeError: If unit_str is not a string or scipp Unit.
        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:

Name Type Description
Parameter Parameter

The energy offset template parameter of the instrument model.

fix_resolution_parameters()

Fix all parameters in the resolution model.

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

free_resolution_parameters()

Free all parameters in the resolution model.

Source code in src/easydynamics/sample_model/instrument_model.py
351
352
353
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

Returns:

Type Description
list[Parameter]

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.

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.

Source code in src/easydynamics/sample_model/instrument_model.py
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
def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]:
    """Get all variables in the InstrumentModel.

    Args:
        Q_index (int | None): The index of the Q value to get
            variables for. If None, get variables for all Q values.

    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.

    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 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_at_Q(Q_index)

Get the energy offset Parameter at a specific Q index.

Parameters:

Name Type Description Default
Q_index int

The index of the Q value to get the energy offset for.

required

Returns:

Name Type Description
Parameter Parameter

The energy offset Parameter at the specified Q index.

Raises:

Type Description
ValueError

If no Q values are set in the InstrumentModel.

IndexError

If Q_index is out of bounds.

Source code in src/easydynamics/sample_model/instrument_model.py
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
def get_energy_offset_at_Q(self, Q_index: int) -> Parameter:
    """Get the energy offset Parameter at a specific Q index.

    Args:
        Q_index (int): The index of the Q value to get the energy
            offset for.

    Returns:
        Parameter: The energy offset Parameter at the specified Q
            index.

    Raises:
        ValueError: If no Q values are set in the InstrumentModel.
        IndexError: If Q_index is out of bounds.
    """
    if self._Q is None:
        raise ValueError('No Q values are set in the InstrumentModel.')

    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]

resolution_model property writable

Get the resolution model of the instrument.

Returns:

Name Type Description
ResolutionModel 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.

Parameters:

Name Type Description Default
area Int | float | Parameter

Area of the Lorentzian.

1.0
center Int | float | None | Parameter

Center of the Lorentzian. If None, defaults to 0 and is fixed

None
width Int | float | Parameter

Half width at half maximum (HWHM).

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Attributes:

Name Type Description
area Parameter

Area of the Lorentzian.

center Parameter

Center of the Lorentzian.

width Parameter

Half width at half maximum (HWHM) of the Lorentzian.

unit str | Unit

Unit of the parameters.

display_name str | None

Name of the component.

unique_name str | None

Unique name of the component.

Source code in src/easydynamics/sample_model/components/lorentzian.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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
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.

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

    Attributes:
        area (Parameter): Area of the Lorentzian.
        center (Parameter): Center of the Lorentzian.
        width (Parameter): Half width at half maximum (HWHM) of the
            Lorentzian.
        unit (str | sc.Unit): Unit of the parameters.
        display_name (str | None): Name of the component.
        unique_name (str | None): Unique name of the component.
    """

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

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

        Raises:
            TypeError: If any of the parameters are of the wrong type.
            ValueError: If width is not positive.
        """

        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.

        Args:
            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.

        Args:
            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).

        Args:
            value (Numeric | None): 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 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).

        Args:
            x (Numeric or list or np.ndarray or sc.Variable or 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 \
            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 Int | float | Parameter

Area of the Lorentzian.

1.0
center Int | float | None | Parameter

Center of the Lorentzian. If None, defaults to 0 and is fixed

None
width Int | float | Parameter

Half width at half maximum (HWHM).

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Raises:

Type Description
TypeError

If any of the parameters are of the wrong type.

ValueError

If width is not positive.

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

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

    Raises:
        TypeError: If any of the parameters are of the wrong type.
        ValueError: If width is not positive.
    """

    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:

Name Type Description
str str

A string representation of the Lorentzian.

Source code in src/easydynamics/sample_model/components/lorentzian.py
204
205
206
207
208
209
210
211
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 \
        area = {self.area},\n center = {self.center},\n width = {self.width})'

area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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 or list or ndarray or Variable or DataArray

The x values at which to evaluate the Lorentzian.

required

Returns:

Type Description
ndarray

np.ndarray: The intensity of the Lorentzian at the given x values.

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

    Args:
        x (Numeric or list or np.ndarray or sc.Variable or 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:

Name Type Description
Parameter 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.

Parameters:

Name Type Description Default
coefficients list or tuple

Coefficients c0, c1, ..., cN

(0.0,)
unit str or Unit

Unit of the Polynomial component.

'meV'
display_name str

Display name of the Polynomial component.

'Polynomial'
unique_name str or None

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

None

Attributes:

Name Type Description
coefficients list of Parameter

Coefficients of the polynomial as Parameters.

unit str

Unit of the Polynomial component.

display_name str

Display name of the Polynomial component.

unique_name str or None

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

Source code in src/easydynamics/sample_model/components/polynomial.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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
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.

    Args:
        coefficients (list or tuple): Coefficients c0, c1, ..., cN
        unit (str or sc.Unit): Unit of the Polynomial component.
        display_name (str): Display name of the Polynomial component.
        unique_name (str or None): Unique name of the component.
            If None, a unique_name is automatically generated.

    Attributes:
        coefficients (list of Parameter): Coefficients of the polynomial
            as Parameters.
        unit (str): Unit of the Polynomial component.
        display_name (str): Display name of the Polynomial component.
        unique_name (str or None): Unique name of the component.
            If None, a unique_name is automatically generated.
    """

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

        Args:
            coefficients (list or tuple): Coefficients c0, c1, ..., cN
            unit (str or sc.Unit): Unit of the Polynomial component.
            display_name (str): Display name of the Polynomial
                component.
            unique_name (str or None): Unique name of the component.
                If None, a unique_name is automatically generated.

        Raises:
            TypeError: If coefficients is not a sequence of numbers or
                Parameters.
            ValueError: If coefficients is an empty sequence.
            TypeError: If any item in coefficients is not a number or
                Parameter.
            UnitError: If unit is not a string or sc.Unit.
        """

        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.

        Args:
            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.
            ValueError: If the length of coeffs does not match the
                existing number of coefficients.
            TypeError: If any item in coeffs is not a number or
                Parameter.
        """
        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.
        """
        coefficient_list = [param.value for param in self._coefficients]
        return coefficient_list

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

        Args:
            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,
            )
        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.

        Args:
            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 parameters from the model component.

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

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

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

        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}, \
            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 list or tuple

Coefficients c0, c1, ..., cN

(0.0,)
unit str or Unit

Unit of the Polynomial component.

'meV'
display_name str

Display name of the Polynomial component.

'Polynomial'
unique_name str or None

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

None

Raises:

Type Description
TypeError

If coefficients is not a sequence of numbers or Parameters.

ValueError

If coefficients is an empty sequence.

TypeError

If any item in coefficients is not a number or Parameter.

UnitError

If unit is not a string or sc.Unit.

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

    Args:
        coefficients (list or tuple): Coefficients c0, c1, ..., cN
        unit (str or sc.Unit): Unit of the Polynomial component.
        display_name (str): Display name of the Polynomial
            component.
        unique_name (str or None): Unique name of the component.
            If None, a unique_name is automatically generated.

    Raises:
        TypeError: If coefficients is not a sequence of numbers or
            Parameters.
        ValueError: If coefficients is an empty sequence.
        TypeError: If any item in coefficients is not a number or
            Parameter.
        UnitError: If unit is not a string or sc.Unit.
    """

    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:

Name Type Description
str str

A string representation of the Polynomial.

Source code in src/easydynamics/sample_model/components/polynomial.py
244
245
246
247
248
249
250
251
252
253
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}, \
        unit = {self._unit},\n coefficients = [{coeffs_str}])'

coefficient_values()

Get the coefficients of the polynomial as a list.

Returns:

Type Description
list[float]

list[float]: The coefficient values of the polynomial.

Source code in src/easydynamics/sample_model/components/polynomial.py
144
145
146
147
148
149
150
151
def coefficient_values(self) -> list[float]:
    """Get the coefficients of the polynomial as a list.

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

coefficients property writable

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

Returns:

Type Description
list[Parameter]

list[Parameter]: The coefficients of the polynomial.

convert_unit(unit)

Convert the unit of the polynomial.

Parameters:

Name Type Description Default
unit str or Unit

The target unit to convert to.

required

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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
def convert_unit(self, unit: str | sc.Unit):
    """Convert the unit of the polynomial.

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

    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:

Name Type Description
int 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

np.ndarray: The evaluated Polynomial at the given x values.

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

    Args:
        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,
        )
    return result

get_all_variables()

Get all parameters from the model component.

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

Source code in src/easydynamics/sample_model/components/polynomial.py
211
212
213
214
215
216
217
def get_all_variables(self) -> list[DescriptorBase]:
    """Get all parameters from the model component.

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

ResolutionModel

Bases: ModelBase

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

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 | None

Unit of the model. Defaults to "meV".

'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

Attributes:

Name Type Description
unit str | Unit

Unit of the model.

components list[ModelComponent]

List of ModelComponents in the model.

Q ndarray | Numeric | list | ArrayLike | Variable | None

Q values of the model.

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

    Args:
        display_name (str): Display name of the model.
        unique_name (str | None): Unique name of the model. If None, a
            unique name will be generated.
        unit (str | sc.Unit | None): Unit of the model. Defaults to
            "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.
        Q (Q_type | None): Q values for the model. If None, Q is not
            set.

    Attributes:
        unit (str | sc.Unit): Unit of the model.
        components (list[ModelComponent]): List of ModelComponents in
            the model.
        Q (np.ndarray | Numeric | list | ArrayLike | sc.Variable | None):
            Q values of the model.
    """

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

        Args:
            display_name (str): Display name of the model.
            unique_name (str | None): Unique name of the model. If None,
                a unique name will be generated.
            unit (str | sc.Unit | None): Unit of the model. Defaults to
                "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.
            Q (Q_type | None): Q values for the model. If None, Q is not
                set.

        Raises:
            TypeError: If components is not a ModelComponent or
                ComponentCollection.
            ValueError: If Q is not a valid Q_type.
        """

        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.

        Args:
            component (ModelComponent | ComponentCollection):
                Component(s) to append.

        Raises:
            TypeError: If the component is a DeltaFunction or Polynomial
        """
        if isinstance(component, ComponentCollection):
            components = component.components
        else:
            components = (component,)

        for comp in components:
            if isinstance(comp, (DeltaFunction, Polynomial)):
                raise TypeError(
                    f'Component in ResolutionModel cannot be a {comp.__class__.__name__}'
                )

        super().append_component(component)

__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 | None

Unit of the model. Defaults to "meV".

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

ValueError

If Q is not a valid Q_type.

Source code in src/easydynamics/sample_model/resolution_model.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def __init__(
    self,
    display_name: str = 'MyResolutionModel',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    components: ComponentCollection | ModelComponent | None = None,
    Q: Q_type | None = None,
):
    """Initialize a ResolutionModel.

    Args:
        display_name (str): Display name of the model.
        unique_name (str | None): Unique name of the model. If None,
            a unique name will be generated.
        unit (str | sc.Unit | None): Unit of the model. Defaults to
            "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.
        Q (Q_type | None): Q values for the model. If None, Q is not
            set.

    Raises:
        TypeError: If components is not a ModelComponent or
            ComponentCollection.
        ValueError: If Q is not a valid Q_type.
    """

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

    Args:
        component (ModelComponent | ComponentCollection):
            Component(s) to append.

    Raises:
        TypeError: If the component is a DeltaFunction or Polynomial
    """
    if isinstance(component, ComponentCollection):
        components = component.components
    else:
        components = (component,)

    for comp in components:
        if isinstance(comp, (DeltaFunction, Polynomial)):
            raise TypeError(
                f'Component in ResolutionModel cannot be a {comp.__class__.__name__}'
            )

    super().append_component(component)

SampleModel

Bases: ModelBase

SampleModel represents a model of a sample with components and diffusion models, parameterized by Q and optionally temperature. Generates ComponentCollections for each Q value, combining components from the base model and diffusion models.

Applies detailed balancing based on temperature if provided.

Parameters:

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 | None

Unit of the model. If None, defaults to "meV".

'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 (Number, list, ndarray, array | 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.

None
temperature_unit str | Unit

Unit of the temperature. Defaults to "K".

'K'
divide_by_temperature bool

Whether to divide the detailed balance factor by temperature. Defaults to True.

True

Attributes:

Name Type Description
unit str | Unit

Unit of the model.

components list[ModelComponent]

List of ModelComponents in the model.

Q ndarray | Numeric | list | ArrayLike | Variable | None

Q values of the model.

diffusion_models list[DiffusionModelBase]

List of diffusion models in the SampleModel.

temperature Parameter | None

Temperature Parameter for detailed balancing, or None if not set.

divide_by_temperature bool

Whether to divide the detailed balance factor by temperature.

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

    Args:
        display_name (str): Display name of the model.
        unique_name (str | None): Unique name of the model. If None, a
            unique name will be generated.
        unit (str | sc.Unit | None): Unit of the model. If None,
            defaults to "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.
        Q (Number, list, np.ndarray, sc.array | None):
            Q values for the model. If None, Q is not set.
        diffusion_models (DiffusionModelBase | list[DiffusionModelBase] | None):
            Diffusion models to include in the SampleModel.
            If None, no diffusion models are added.
        temperature (float | None): Temperature for detailed balancing.
            If None, no detailed balancing is applied.
        temperature_unit (str | sc.Unit): Unit of the temperature.
            Defaults to "K".
        divide_by_temperature (bool): Whether to divide the detailed
            balance factor by temperature. Defaults to True.

    Attributes:
        unit (str | sc.Unit): Unit of the model.
        components (list[ModelComponent]): List of ModelComponents in
            the model.
        Q (np.ndarray | Numeric | list | ArrayLike | sc.Variable | None):
            Q values of the model.
        diffusion_models (list[DiffusionModelBase]): List of diffusion
            models in the SampleModel.
        temperature (Parameter | None): Temperature Parameter for
            detailed balancing, or None if not set.
        divide_by_temperature (bool): Whether to divide the detailed
            balance factor by temperature.
    """

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

        Args:
            display_name (str): Display name of the model.
            unique_name (str | None): Unique name of the model. If None,
                a unique name will be generated.
            unit (str | sc.Unit | None): Unit of the model. If None,
                defaults to "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.
            Q (Number, list, np.ndarray, sc.array | None):
                Q values for the model. If None, Q is not set.
            diffusion_models (DiffusionModelBase | list[DiffusionModelBase] | None):
                Diffusion models to include in the SampleModel. If None,
                no diffusion models are added.
            temperature (float | None): Temperature for detailed
                balancing. If None, no detailed balancing is applied.
            temperature_unit (str | sc.Unit): Unit of the temperature.
                Defaults to "K".
            divide_by_temperature (bool): Whether to divide the detailed
                balance factor by temperature. Defaults to True.

        Raises:
            TypeError: If diffusion_models is not a DiffusionModelBase,
                a list of DiffusionModelBase, or None.
            TypeError: If temperature is not a number or None.
            ValueError: If temperature is negative.
            TypeError: If divide_by_temperature is not a bool.
        """
        if diffusion_models is None:
            self._diffusion_models = []
        elif isinstance(diffusion_models, DiffusionModelBase):
            self._diffusion_models = [diffusion_models]
        else:
            if not isinstance(diffusion_models, list) or not all(
                isinstance(dm, DiffusionModelBase) for dm in diffusion_models
            ):
                raise TypeError(
                    'diffusion_models must be a DiffusionModelBase, '
                    'a list of DiffusionModelBase or None'
                )
            self._diffusion_models = diffusion_models

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

        if temperature is None:
            self._temperature = None
        else:
            if not isinstance(temperature, Numeric):
                raise TypeError('temperature must be a number or None')

            if temperature < 0:
                raise ValueError('temperature must be non-negative')
            self._temperature = Parameter(
                name='Temperature',
                value=temperature,
                unit=temperature_unit,
                display_name='Temperature',
                fixed=True,
            )
        self._temperature_unit = temperature_unit

        if not isinstance(divide_by_temperature, bool):
            raise TypeError('divide_by_temperature must be True or False')
        self._divide_by_temperature = divide_by_temperature

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

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

        Args:
            diffusion_model (DiffusionModelBase): The DiffusionModel
                to append.

        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.

        Args:
            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.

        Args:
            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.
        """

        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.

        Args:
            value (Numeric | None): The temperature value to set. Can be
                a number or None to unset the temperature.
        """
        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.

        Args:
            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.'
        )  # noqa: E501

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

        Args:
            unit (str | sc.Unit): The unit to convert the temperature
                Parameter to.

        Raises:
            ValueError: If temperature is not set or conversion fails.
        """

        if self._temperature is None:
            raise ValueError('Temperature is not set, cannot convert unit.')

        old_unit = self._temperature.unit

        try:
            self._temperature.convert_unit(unit)
            self._temperature_unit = unit
        except Exception as e:
            # Attempt to rollback on failure
            try:
                self._temperature.convert_unit(old_unit)
            except Exception:  # noqa: S110
                pass  # Best effort rollback
            raise e

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

        Returns:
            bool: True if the detailed balance factor is divided by
                temperature, False otherwise.
        """
        return self._divide_by_temperature

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

        Args:
            value (bool): True to divide the detailed balance factor by
                temperature, False otherwise.
        """
        if not isinstance(value, bool):
            raise TypeError('divide_by_temperature must be True or False')
        self._divide_by_temperature = value

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

    def evaluate(
        self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray
    ) -> list[np.ndarray]:
        """Evaluate the sample model at all Q for the given x values.

        Args:
            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:
            DBF = _detailed_balance_factor(
                energy=x,
                temperature=self._temperature,
                divide_by_temperature=self._divide_by_temperature,
                energy_unit=self._unit,
            )
            y = [yi * DBF for yi in y]

        return y

    def get_all_variables(self, 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.

        Args:
            Q_index (int | None): If specified, only get variables from
                the ComponentCollection at the given Q index. If None,
                get variables from all ComponentCollections.
        """

        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.
        """
        # TODO regenerate automatically if Q, diffusion models
        # or components have changed
        super()._generate_component_collections()

        if self._Q is None:
            return
        # Generate components from diffusion models
        # and add to component collections
        for diffusion_model in self._diffusion_models:
            diffusion_collections = diffusion_model.create_component_collections(Q=self._Q)
            for target, source in zip(self._component_collections, diffusion_collections):
                for component in source.components:
                    target.append_component(component)

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

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

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 | None

Unit of the model. If None, defaults to "meV".

'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 (Number, list, ndarray, array | 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.

None
temperature_unit str | Unit

Unit of the temperature. Defaults to "K".

'K'
divide_by_temperature bool

Whether to divide the detailed balance factor by temperature. Defaults to True.

True

Raises:

Type Description
TypeError

If diffusion_models is not a DiffusionModelBase, a list of DiffusionModelBase, or None.

TypeError

If temperature is not a number or None.

ValueError

If temperature is negative.

TypeError

If divide_by_temperature is not a bool.

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

    Args:
        display_name (str): Display name of the model.
        unique_name (str | None): Unique name of the model. If None,
            a unique name will be generated.
        unit (str | sc.Unit | None): Unit of the model. If None,
            defaults to "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.
        Q (Number, list, np.ndarray, sc.array | None):
            Q values for the model. If None, Q is not set.
        diffusion_models (DiffusionModelBase | list[DiffusionModelBase] | None):
            Diffusion models to include in the SampleModel. If None,
            no diffusion models are added.
        temperature (float | None): Temperature for detailed
            balancing. If None, no detailed balancing is applied.
        temperature_unit (str | sc.Unit): Unit of the temperature.
            Defaults to "K".
        divide_by_temperature (bool): Whether to divide the detailed
            balance factor by temperature. Defaults to True.

    Raises:
        TypeError: If diffusion_models is not a DiffusionModelBase,
            a list of DiffusionModelBase, or None.
        TypeError: If temperature is not a number or None.
        ValueError: If temperature is negative.
        TypeError: If divide_by_temperature is not a bool.
    """
    if diffusion_models is None:
        self._diffusion_models = []
    elif isinstance(diffusion_models, DiffusionModelBase):
        self._diffusion_models = [diffusion_models]
    else:
        if not isinstance(diffusion_models, list) or not all(
            isinstance(dm, DiffusionModelBase) for dm in diffusion_models
        ):
            raise TypeError(
                'diffusion_models must be a DiffusionModelBase, '
                'a list of DiffusionModelBase or None'
            )
        self._diffusion_models = diffusion_models

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

    if temperature is None:
        self._temperature = None
    else:
        if not isinstance(temperature, Numeric):
            raise TypeError('temperature must be a number or None')

        if temperature < 0:
            raise ValueError('temperature must be non-negative')
        self._temperature = Parameter(
            name='Temperature',
            value=temperature,
            unit=temperature_unit,
            display_name='Temperature',
            fixed=True,
        )
    self._temperature_unit = temperature_unit

    if not isinstance(divide_by_temperature, bool):
        raise TypeError('divide_by_temperature must be True or False')
    self._divide_by_temperature = divide_by_temperature

__repr__()

Return a string representation of the SampleModel.

Returns:

Name Type Description
str str

A string representation of the SampleModel.

Source code in src/easydynamics/sample_model/sample_model.py
441
442
443
444
445
446
447
448
449
450
451
452
453
454
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'divide_by_temperature = {self._divide_by_temperature}'
    )

append_diffusion_model(diffusion_model)

Append a DiffusionModel to the SampleModel.

Parameters:

Name Type Description Default
diffusion_model DiffusionModelBase

The DiffusionModel 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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def append_diffusion_model(self, diffusion_model: DiffusionModelBase) -> None:
    """Append a DiffusionModel to the SampleModel.

    Args:
        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
192
193
194
195
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.

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

    Args:
        unit (str | sc.Unit): The unit to convert the temperature
            Parameter to.

    Raises:
        ValueError: If temperature is not set or conversion fails.
    """

    if self._temperature is None:
        raise ValueError('Temperature is not set, cannot convert unit.')

    old_unit = self._temperature.unit

    try:
        self._temperature.convert_unit(unit)
        self._temperature_unit = unit
    except Exception as e:
        # Attempt to rollback on failure
        try:
            self._temperature.convert_unit(old_unit)
        except Exception:  # noqa: S110
            pass  # Best effort rollback
        raise e

diffusion_models property writable

Get the diffusion models of the SampleModel.

Returns:

Type Description
list[DiffusionModelBase]

list[DiffusionModelBase]: The diffusion models of the SampleModel.

divide_by_temperature property writable

Get whether to divide the detailed balance factor by temperature.

Returns:

Name Type Description
bool bool

True if the detailed balance factor is divided by temperature, False otherwise.

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[np.ndarray]: List of evaluated model values for each Q.

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

    Args:
        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:
        DBF = _detailed_balance_factor(
            energy=x,
            temperature=self._temperature,
            divide_by_temperature=self._divide_by_temperature,
            energy_unit=self._unit,
        )
        y = [yi * DBF for yi in y]

    return y

get_all_variables(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
Source code in src/easydynamics/sample_model/sample_model.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
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.

    Args:
        Q_index (int | None): If specified, only get variables from
            the ComponentCollection at the given Q index. If None,
            get variables from all ComponentCollections.
    """

    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

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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def remove_diffusion_model(self, name: 'str') -> None:
    """Remove a DiffusionModel from the SampleModel by unique name.

    Args:
        name (str): The unique name of the DiffusionModel to remove.

    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

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

str | sc.Unit: The unit of the temperature Parameter.

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.

Parameters:

Name Type Description Default
area Int | float

Total area under the curve.

1.0
center Int | float | None

Center of the Voigt profile.

None
gaussian_width Int | float

Standard deviation of the Gaussian part.

1.0
lorentzian_width Int | float

Half width at half max (HWHM) of the Lorentzian part.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV"

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

None

Attributes:

Name Type Description
area Parameter

Total area under the curve.

center Parameter

Center of the Voigt profile.

gaussian_width Parameter

Standard deviation of the Gaussian part.

lorentzian_width Parameter

Half width at half max (HWHM) of the Lorentzian part.

unit str | Unit

Unit of the parameters.

display_name str | None

Display name of the component.

unique_name str | None

Unique name of the component.

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

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

    Attributes:
        area (Parameter): Total area under the curve.
        center (Parameter): Center of the Voigt profile.
        gaussian_width (Parameter): Standard deviation of the Gaussian
            part.
        lorentzian_width (Parameter): Half width at half max (HWHM) of
            the Lorentzian part.
        unit (str | sc.Unit): Unit of the parameters.
        display_name (str | None): Display name of the component.
        unique_name (str | None): Unique name of the component.
    """

    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,
    ):
        """Initialize a Voigt component.

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

        Raises:
            TypeError: If any of the parameters are not of the correct
                type.
            ValueError: If any of the parameters are not valid (e.g.
                negative widths).
        """

        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.

        Args:
            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.

        Args:
            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.

        Args:
            value (Numeric | None): 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('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.

        Args:
            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.


        Args:
            x (Numeric | list[Numeric] | 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 \
        area = {self.area},\n \
        center = {self.center},\n \
        gaussian_width = {self.gaussian_width},\n \
        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 Int | float

Total area under the curve.

1.0
center Int | float | None

Center of the Voigt profile.

None
gaussian_width Int | float

Standard deviation of the Gaussian part.

1.0
lorentzian_width Int | float

Half width at half max (HWHM) of the Lorentzian part.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV"

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

None

Raises:

Type Description
TypeError

If any of the parameters are not of the correct type.

ValueError

If any of the parameters are not valid (e.g. negative widths).

Source code in src/easydynamics/sample_model/components/voigt.py
 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
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,
):
    """Initialize a Voigt component.

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

    Raises:
        TypeError: If any of the parameters are not of the correct
            type.
        ValueError: If any of the parameters are not valid (e.g.
            negative widths).
    """

    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:

Name Type Description
str str

A string representation of the Voigt.

Source code in src/easydynamics/sample_model/components/voigt.py
240
241
242
243
244
245
246
247
248
249
250
251
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 \
    area = {self.area},\n \
    center = {self.center},\n \
    gaussian_width = {self.gaussian_width},\n \
    lorentzian_width = {self.lorentzian_width})'

area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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[Numeric] | ndarray | Variable | DataArray

The x values at which to evaluate the Voigt.

required

Returns:

Type Description
ndarray

np.ndarray: The intensity of the Voigt at the given x values.

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


    Args:
        x (Numeric | list[Numeric] | 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:

Name Type Description
Parameter Parameter

The Gaussian width parameter.

lorentzian_width property writable

Get the Lorentzian width parameter (HWHM).

Returns:

Name Type Description
Parameter Parameter

The Lorentzian width parameter.

background_model

BackgroundModel

Bases: ModelBase

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

Parameters:

Name Type Description Default
display_name str

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 | None

Unit of the model. Defaults to "meV".

'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

Attributes:

Name Type Description
unit str | Unit

Unit of the model.

components list[ModelComponent]

List of ModelComponents in the model.

Q ndarray | Numeric | list | ArrayLike | Variable | None

Q values of the model.

Source code in src/easydynamics/sample_model/background_model.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class BackgroundModel(ModelBase):
    """BackgroundModel represents a model of the background in an
    experiment at various Q.

    Args:
        display_name (str): Display name of the model.
        unique_name (str | None): Unique name of the model. If None, a
            unique name will be generated.
        unit (str | sc.Unit | None): Unit of the model. Defaults to
            "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.
        Q (Q_type | None): Q values for the model. If None, Q is not
            set.

    Attributes:
        unit (str | sc.Unit): Unit of the model.
        components (list[ModelComponent]): List of ModelComponents in
            the model.
        Q (np.ndarray | Numeric | list | ArrayLike | sc.Variable | None):
            Q values of the model.
    """

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

        Args:
            display_name (str): Display name of the model.
            unique_name (str | None): Unique name of the model. If None,
                a unique name will be generated.
            unit (str | sc.Unit | None): Unit of the model. Defaults to
                "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.
            Q (Q_type | 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

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 | None

Unit of the model. Defaults to "meV".

'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
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,
    display_name: str = 'MyBackgroundModel',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    components: ComponentCollection | ModelComponent | None = None,
    Q: Q_type | None = None,
):
    """Initialize the BackgroundModel.

    Args:
        display_name (str): Display name of the model.
        unique_name (str | None): Unique name of the model. If None,
            a unique name will be generated.
        unit (str | sc.Unit | None): Unit of the model. Defaults to
            "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.
        Q (Q_type | 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.

Parameters:

Name Type Description Default
unit str | Unit

Unit of the sample model. Defaults to "meV".

'meV'
display_name str

Display name of the sample model.

'MyComponentCollection'
unique_name str | None

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

None
components List[ModelComponent] | None

Initial model components to add to the ComponentCollection.

None

Attributes:

Name Type Description
components List[ModelComponent]

List of model components in the collection.

unit str | Unit

Unit of the sample model.

display_name str

Display name of the sample model.

unique_name str

Unique name of the sample model.

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

    Args:
        unit (str | sc.Unit): Unit of the sample model. Defaults to
            "meV".
        display_name (str): Display name of the sample model.
        unique_name (str | None): Unique name of the sample model.
            If None, a unique_name is automatically generated.
        components (List[ModelComponent] | None): Initial model
            components to add to the ComponentCollection.

    Attributes:
        components (List[ModelComponent]): List of model components in
            the collection.
        unit (str | sc.Unit): Unit of the sample model.
        display_name (str): Display name of the sample model.
        unique_name (str): Unique name of the sample model.
    """

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

        Args:
        unit (str | sc.Unit | None): Unit of the sample model.
            Defaults to "meV".
        display_name (str | None): Display name of the sample model.
        unique_name (str | None): Unique name of the sample model.
            Defaults to None.
        components (List[ModelComponent] | 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.
            ValueError: If components contains duplicate unique names.
        """

        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.

        Args:
            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.

        Args:
            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.

        Args:
            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.'
            )
        )  # noqa: E501

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

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

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

        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.

        Args:
        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.

        Args:
            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]:
        return list(self._components)

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

        self._components = components

    @property
    def is_empty(self) -> bool:
        return not self._components

    @is_empty.setter
    def is_empty(self, value: bool) -> None:
        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.
            ValueError: 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,
                )

        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[Parameter]: 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.

        Args:
            x (Number, list, np.ndarray, sc.Variable, or 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.

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

        Returns:
            np.ndarray: Evaluated values for the specified component.
        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.
        """
        if not self.components:
            raise ValueError('No components in the model to evaluate.')

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

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

        component = matches[0]

        result = component.evaluate(x)

        return result

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

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

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

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

        Args:
            item (str or ModelComponent): The component name or instance
                to check for.

        Returns:
            bool: True if the component exists, False otherwise.
        """

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

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

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

The component name or instance to check for.

required

Returns:

Name Type Description
bool bool

True if the component exists, False otherwise.

Source code in src/easydynamics/sample_model/component_collection.py
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
def __contains__(self, item: str | ModelComponent) -> bool:
    """Check if a component with the given name or instance exists
    in the ComponentCollection.

    Args:
        item (str or ModelComponent): The component name or instance
            to check for.

    Returns:
        bool: True if the component exists, False otherwise.
    """

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

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

Initialize a new ComponentCollection.

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

Raises:

Type Description
TypeError

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

ValueError

If components contains duplicate unique names.

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

    Args:
    unit (str | sc.Unit | None): Unit of the sample model.
        Defaults to "meV".
    display_name (str | None): Display name of the sample model.
    unique_name (str | None): Unique name of the sample model.
        Defaults to None.
    components (List[ModelComponent] | 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.
        ValueError: If components contains duplicate unique names.
    """

    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:

Name Type Description
str str

String representation of the ComponentCollection.

Source code in src/easydynamics/sample_model/component_collection.py
441
442
443
444
445
446
447
448
449
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.

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

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

    Args:
    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
301
302
303
def clear_components(self) -> None:
    """Remove all components."""
    self._components.clear()

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.

UnitError

If any component cannot be converted to the specified unit.

Source code in src/easydynamics/sample_model/component_collection.py
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 ComponentCollection and all its
    components.

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

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

    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 Number, list, np.ndarray, sc.Variable, or sc.DataArray

Energy axis.

required

Returns np.ndarray: Evaluated model values.

Source code in src/easydynamics/sample_model/component_collection.py
353
354
355
356
357
358
359
360
361
362
363
364
365
366
def evaluate(self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray) -> np.ndarray:
    """Evaluate the sum of all components.

    Args:
        x (Number, list, np.ndarray, sc.Variable, or 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 Number, list, np.ndarray, sc.Variable, or sc.DataArray

Energy axis.

required
unique_name str

Component unique name.

required

Returns:

Type Description
ndarray

np.ndarray: Evaluated values for the specified component.

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.

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

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

    Returns:
        np.ndarray: Evaluated values for the specified component.
    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.
    """
    if not self.components:
        raise ValueError('No components in the model to evaluate.')

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

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

    component = matches[0]

    result = component.evaluate(x)

    return result

fix_all_parameters()

Fix all free parameters in the model.

Source code in src/easydynamics/sample_model/component_collection.py
406
407
408
409
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
411
412
413
414
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[Parameter]: List of parameters in the component.

Source code in src/easydynamics/sample_model/component_collection.py
344
345
346
347
348
349
350
351
def get_all_variables(self) -> list[DescriptorBase]:
    """Get all parameters from the model component.

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

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

list_component_names()

List the names of all components in the model.

Returns:

Type Description
List[str]

List[str]: List of unique names of the components in the

List[str]

collection.

Source code in src/easydynamics/sample_model/component_collection.py
291
292
293
294
295
296
297
298
299
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.

ValueError

If the total area is zero or not finite, which would prevent normalization.

Source code in src/easydynamics/sample_model/component_collection.py
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
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.
        ValueError: 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,
            )

    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: 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
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 remove_component(self, unique_name: str) -> None:
    """Remove a component from the collection by its unique name.

    Args:
        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

str | sc.Unit | None: The unit of the ComponentCollection,

str | Unit | None

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.

Parameters:

Name Type Description Default
area Int | float

Area under the curve.

1.0
center Int | float

Resonance frequency, approximately the peak position.

1.0
width Int | float

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

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Attributes:

Name Type Description
area Parameter

Area under the curve.

center Parameter

Resonance frequency, approximately the peak position.

width Parameter

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

unit str | Unit

Unit of the parameters.

display_name str | None

Display name of the component.

unique_name str | None

Unique name of the component.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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
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.


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

    Attributes:
        area (Parameter): Area under the curve.
        center (Parameter): Resonance frequency, approximately the
            peak position.
        width (Parameter): Damping constant, approximately the
            half width at half max (HWHM) of the peaks.
        unit (str | sc.Unit): Unit of the parameters.
        display_name (str | None): Display name of the component.
        unique_name (str | None): Unique name of the component.
    """

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

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

        Raises:
            TypeError: If any of the parameters are not numbers or
                Parameters.
            ValueError: If center or width are not positive.
        """

        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.

        Args:
            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.

        Args:
            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.

        Args:
            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.

        Args:
            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}, unit = {self._unit},\n \
        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 Int | float

Area under the curve.

1.0
center Int | float

Resonance frequency, approximately the peak position.

1.0
width Int | float

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

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Raises:

Type Description
TypeError

If any of the parameters are not numbers or Parameters.

ValueError

If center or width are not positive.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
 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
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,
):
    """Initialize the Damped Harmonic Oscillator.

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

    Raises:
        TypeError: If any of the parameters are not numbers or
            Parameters.
        ValueError: If center or width are not positive.
    """

    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:

Name Type Description
str str

A string representation of the Damped Harmonic Oscillator.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
209
210
211
212
213
214
215
216
217
218
219
220
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}, unit = {self._unit},\n \
    area = {self.area},\n center = {self.center},\n width = {self.width})'
    )

area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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

np.ndarray: The intensity of the DHO at the given x values.

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

    Args:
        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:

Name Type Description
Parameter 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.

Parameters:

Name Type Description Default
center Int | float | None

Center of the delta function. If None, defaults to 0 and is fixed.

None
area Int | float

Total area under the curve.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Attributes:

Name Type Description
center Parameter

Center of the delta function.

area Parameter

Total area under the curve.

unit str | Unit

Unit of the parameters.

display_name str | None

Name of the component.

unique_name str | None

Unique name of the component.

Source code in src/easydynamics/sample_model/components/delta_function.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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
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.

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

    Attributes:
        center (Parameter): Center of the delta function.
        area (Parameter): Total area under the curve.
        unit (str | sc.Unit): Unit of the parameters.
        display_name (str | None): Name of the component.
        unique_name (str | None): Unique name of the component.
    """

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

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

        Raises:
            TypeError: If center is not a number or None.
            TypeError: If area is not a number.
            TypeError: If unit is not a string or sc.Unit.
        """
        # 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.

        Args:
            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.

        Args:
            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.

        Args:
            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:
                left = x[1] - x[0] if x.size > 1 else 0.5
            else:
                left = x[i] - x[i - 1]

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

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

            model[i] = area / bin_width

        return model

    def __repr__(self) -> 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 \
        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 Int | float | None

Center of the delta function. If None, defaults to 0 and is fixed.

None
area Int | float

Total area under the curve.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Raises:

Type Description
TypeError

If center is not a number or None.

TypeError

If area is not a number.

TypeError

If unit is not a string or sc.Unit.

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

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

    Raises:
        TypeError: If center is not a number or None.
        TypeError: If area is not a number.
        TypeError: If unit is not a string or sc.Unit.
    """
    # 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:

Name Type Description
str str

A string representation of the Delta function.

Source code in src/easydynamics/sample_model/components/delta_function.py
184
185
186
187
188
189
190
191
192
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 \
    area = {self.area},\n center = {self.center}'

area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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

np.ndarray: The evaluated Delta function at the given x values.

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

    Args:
        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:
            left = x[1] - x[0] if x.size > 1 else 0.5
        else:
            left = x[i] - x[i - 1]

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

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

        model[i] = area / bin_width

    return model

Gaussian

Bases: CreateParametersMixin, ModelComponent

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.

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

Attributes: area (Parameter): Area of the Gaussian. center (Parameter): Center of the Gaussian. width (Parameter): Standard deviation of the Gaussian. unit (str | sc.Unit): Unit of the parameters. display_name (str | None): Name of the component. unique_name (str | None): Unique name of the component.

Source code in src/easydynamics/sample_model/components/gaussian.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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
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.

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

     Attributes:
         area (Parameter): Area of the Gaussian.
         center (Parameter): Center of the Gaussian.
         width (Parameter): Standard deviation of the Gaussian.
         unit (str | sc.Unit): Unit of the parameters.
         display_name (str | None): Name of the component.
         unique_name (str | None): Unique name of the component.
    """

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

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

        Raises:
            TypeError: If area, center, or width are not numbers or
                Parameters.
            ValueError: If width is not positive.
            TypeError: If unit is not a string or sc.Unit.
        """
        # 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.

        Args:
            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 center parameter value.

        Args:
            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.

        Args:
            value (Numeric | None): 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.


        Args:
            x (Numeric or list or np.ndarray or sc.Variable or 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 \
            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 Int | float | Parameter | None

Area of the Gaussian.

1.0
center Int | float | Parameter | None

Center of the Gaussian. If None, defaults to 0 and is fixed.

None
width Int | float | Parameter | None

Standard deviation.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Raises:

Type Description
TypeError

If area, center, or width are not numbers or Parameters.

ValueError

If width is not positive.

TypeError

If unit is not a string or sc.Unit.

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

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

    Raises:
        TypeError: If area, center, or width are not numbers or
            Parameters.
        ValueError: If width is not positive.
        TypeError: If unit is not a string or sc.Unit.
    """
    # 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:

Name Type Description
str str

A string representation of the Gaussian.

Source code in src/easydynamics/sample_model/components/gaussian.py
221
222
223
224
225
226
227
228
229
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 \
        area = {self.area},\n center = {self.center},\n width = {self.width})'

area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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 or list or ndarray or Variable or DataArray

The x values at which to evaluate the Gaussian.

required

Returns:

Type Description
ndarray

np.ndarray: The intensity of the Gaussian at the given x values.

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


    Args:
        x (Numeric or list or np.ndarray or sc.Variable or 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:

Name Type Description
Parameter 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.

Parameters:

Name Type Description Default
area Int | float | Parameter

Area of the Lorentzian.

1.0
center Int | float | None | Parameter

Center of the Lorentzian. If None, defaults to 0 and is fixed

None
width Int | float | Parameter

Half width at half maximum (HWHM).

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Attributes:

Name Type Description
area Parameter

Area of the Lorentzian.

center Parameter

Center of the Lorentzian.

width Parameter

Half width at half maximum (HWHM) of the Lorentzian.

unit str | Unit

Unit of the parameters.

display_name str | None

Name of the component.

unique_name str | None

Unique name of the component.

Source code in src/easydynamics/sample_model/components/lorentzian.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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
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.

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

    Attributes:
        area (Parameter): Area of the Lorentzian.
        center (Parameter): Center of the Lorentzian.
        width (Parameter): Half width at half maximum (HWHM) of the
            Lorentzian.
        unit (str | sc.Unit): Unit of the parameters.
        display_name (str | None): Name of the component.
        unique_name (str | None): Unique name of the component.
    """

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

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

        Raises:
            TypeError: If any of the parameters are of the wrong type.
            ValueError: If width is not positive.
        """

        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.

        Args:
            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.

        Args:
            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).

        Args:
            value (Numeric | None): 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 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).

        Args:
            x (Numeric or list or np.ndarray or sc.Variable or 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 \
            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 Int | float | Parameter

Area of the Lorentzian.

1.0
center Int | float | None | Parameter

Center of the Lorentzian. If None, defaults to 0 and is fixed

None
width Int | float | Parameter

Half width at half maximum (HWHM).

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Raises:

Type Description
TypeError

If any of the parameters are of the wrong type.

ValueError

If width is not positive.

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

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

    Raises:
        TypeError: If any of the parameters are of the wrong type.
        ValueError: If width is not positive.
    """

    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:

Name Type Description
str str

A string representation of the Lorentzian.

Source code in src/easydynamics/sample_model/components/lorentzian.py
204
205
206
207
208
209
210
211
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 \
        area = {self.area},\n center = {self.center},\n width = {self.width})'

area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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 or list or ndarray or Variable or DataArray

The x values at which to evaluate the Lorentzian.

required

Returns:

Type Description
ndarray

np.ndarray: The intensity of the Lorentzian at the given x values.

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

    Args:
        x (Numeric or list or np.ndarray or sc.Variable or 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:

Name Type Description
Parameter 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.

Parameters:

Name Type Description Default
coefficients list or tuple

Coefficients c0, c1, ..., cN

(0.0,)
unit str or Unit

Unit of the Polynomial component.

'meV'
display_name str

Display name of the Polynomial component.

'Polynomial'
unique_name str or None

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

None

Attributes:

Name Type Description
coefficients list of Parameter

Coefficients of the polynomial as Parameters.

unit str

Unit of the Polynomial component.

display_name str

Display name of the Polynomial component.

unique_name str or None

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

Source code in src/easydynamics/sample_model/components/polynomial.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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
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.

    Args:
        coefficients (list or tuple): Coefficients c0, c1, ..., cN
        unit (str or sc.Unit): Unit of the Polynomial component.
        display_name (str): Display name of the Polynomial component.
        unique_name (str or None): Unique name of the component.
            If None, a unique_name is automatically generated.

    Attributes:
        coefficients (list of Parameter): Coefficients of the polynomial
            as Parameters.
        unit (str): Unit of the Polynomial component.
        display_name (str): Display name of the Polynomial component.
        unique_name (str or None): Unique name of the component.
            If None, a unique_name is automatically generated.
    """

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

        Args:
            coefficients (list or tuple): Coefficients c0, c1, ..., cN
            unit (str or sc.Unit): Unit of the Polynomial component.
            display_name (str): Display name of the Polynomial
                component.
            unique_name (str or None): Unique name of the component.
                If None, a unique_name is automatically generated.

        Raises:
            TypeError: If coefficients is not a sequence of numbers or
                Parameters.
            ValueError: If coefficients is an empty sequence.
            TypeError: If any item in coefficients is not a number or
                Parameter.
            UnitError: If unit is not a string or sc.Unit.
        """

        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.

        Args:
            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.
            ValueError: If the length of coeffs does not match the
                existing number of coefficients.
            TypeError: If any item in coeffs is not a number or
                Parameter.
        """
        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.
        """
        coefficient_list = [param.value for param in self._coefficients]
        return coefficient_list

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

        Args:
            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,
            )
        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.

        Args:
            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 parameters from the model component.

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

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

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

        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}, \
            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 list or tuple

Coefficients c0, c1, ..., cN

(0.0,)
unit str or Unit

Unit of the Polynomial component.

'meV'
display_name str

Display name of the Polynomial component.

'Polynomial'
unique_name str or None

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

None

Raises:

Type Description
TypeError

If coefficients is not a sequence of numbers or Parameters.

ValueError

If coefficients is an empty sequence.

TypeError

If any item in coefficients is not a number or Parameter.

UnitError

If unit is not a string or sc.Unit.

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

    Args:
        coefficients (list or tuple): Coefficients c0, c1, ..., cN
        unit (str or sc.Unit): Unit of the Polynomial component.
        display_name (str): Display name of the Polynomial
            component.
        unique_name (str or None): Unique name of the component.
            If None, a unique_name is automatically generated.

    Raises:
        TypeError: If coefficients is not a sequence of numbers or
            Parameters.
        ValueError: If coefficients is an empty sequence.
        TypeError: If any item in coefficients is not a number or
            Parameter.
        UnitError: If unit is not a string or sc.Unit.
    """

    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:

Name Type Description
str str

A string representation of the Polynomial.

Source code in src/easydynamics/sample_model/components/polynomial.py
244
245
246
247
248
249
250
251
252
253
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}, \
        unit = {self._unit},\n coefficients = [{coeffs_str}])'

coefficient_values()

Get the coefficients of the polynomial as a list.

Returns:

Type Description
list[float]

list[float]: The coefficient values of the polynomial.

Source code in src/easydynamics/sample_model/components/polynomial.py
144
145
146
147
148
149
150
151
def coefficient_values(self) -> list[float]:
    """Get the coefficients of the polynomial as a list.

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

coefficients property writable

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

Returns:

Type Description
list[Parameter]

list[Parameter]: The coefficients of the polynomial.

convert_unit(unit)

Convert the unit of the polynomial.

Parameters:

Name Type Description Default
unit str or Unit

The target unit to convert to.

required

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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
def convert_unit(self, unit: str | sc.Unit):
    """Convert the unit of the polynomial.

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

    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:

Name Type Description
int 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

np.ndarray: The evaluated Polynomial at the given x values.

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

    Args:
        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,
        )
    return result

get_all_variables()

Get all parameters from the model component.

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

Source code in src/easydynamics/sample_model/components/polynomial.py
211
212
213
214
215
216
217
def get_all_variables(self) -> list[DescriptorBase]:
    """Get all parameters from the model component.

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

Voigt

Bases: CreateParametersMixin, ModelComponent

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

Use scipy.special.voigt_profile to evaluate the Voigt profile.

Parameters:

Name Type Description Default
area Int | float

Total area under the curve.

1.0
center Int | float | None

Center of the Voigt profile.

None
gaussian_width Int | float

Standard deviation of the Gaussian part.

1.0
lorentzian_width Int | float

Half width at half max (HWHM) of the Lorentzian part.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV"

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

None

Attributes:

Name Type Description
area Parameter

Total area under the curve.

center Parameter

Center of the Voigt profile.

gaussian_width Parameter

Standard deviation of the Gaussian part.

lorentzian_width Parameter

Half width at half max (HWHM) of the Lorentzian part.

unit str | Unit

Unit of the parameters.

display_name str | None

Display name of the component.

unique_name str | None

Unique name of the component.

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

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

    Attributes:
        area (Parameter): Total area under the curve.
        center (Parameter): Center of the Voigt profile.
        gaussian_width (Parameter): Standard deviation of the Gaussian
            part.
        lorentzian_width (Parameter): Half width at half max (HWHM) of
            the Lorentzian part.
        unit (str | sc.Unit): Unit of the parameters.
        display_name (str | None): Display name of the component.
        unique_name (str | None): Unique name of the component.
    """

    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,
    ):
        """Initialize a Voigt component.

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

        Raises:
            TypeError: If any of the parameters are not of the correct
                type.
            ValueError: If any of the parameters are not valid (e.g.
                negative widths).
        """

        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.

        Args:
            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.

        Args:
            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.

        Args:
            value (Numeric | None): 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('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.

        Args:
            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.


        Args:
            x (Numeric | list[Numeric] | 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 \
        area = {self.area},\n \
        center = {self.center},\n \
        gaussian_width = {self.gaussian_width},\n \
        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 Int | float

Total area under the curve.

1.0
center Int | float | None

Center of the Voigt profile.

None
gaussian_width Int | float

Standard deviation of the Gaussian part.

1.0
lorentzian_width Int | float

Half width at half max (HWHM) of the Lorentzian part.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV"

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

None

Raises:

Type Description
TypeError

If any of the parameters are not of the correct type.

ValueError

If any of the parameters are not valid (e.g. negative widths).

Source code in src/easydynamics/sample_model/components/voigt.py
 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
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,
):
    """Initialize a Voigt component.

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

    Raises:
        TypeError: If any of the parameters are not of the correct
            type.
        ValueError: If any of the parameters are not valid (e.g.
            negative widths).
    """

    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:

Name Type Description
str str

A string representation of the Voigt.

Source code in src/easydynamics/sample_model/components/voigt.py
240
241
242
243
244
245
246
247
248
249
250
251
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 \
    area = {self.area},\n \
    center = {self.center},\n \
    gaussian_width = {self.gaussian_width},\n \
    lorentzian_width = {self.lorentzian_width})'

area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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[Numeric] | ndarray | Variable | DataArray

The x values at which to evaluate the Voigt.

required

Returns:

Type Description
ndarray

np.ndarray: The intensity of the Voigt at the given x values.

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


    Args:
        x (Numeric | list[Numeric] | 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:

Name Type Description
Parameter Parameter

The Gaussian width parameter.

lorentzian_width property writable

Get the Lorentzian width parameter (HWHM).

Returns:

Name Type Description
Parameter 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.

Parameters:

Name Type Description Default
area Int | float

Area under the curve.

1.0
center Int | float

Resonance frequency, approximately the peak position.

1.0
width Int | float

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

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Attributes:

Name Type Description
area Parameter

Area under the curve.

center Parameter

Resonance frequency, approximately the peak position.

width Parameter

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

unit str | Unit

Unit of the parameters.

display_name str | None

Display name of the component.

unique_name str | None

Unique name of the component.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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
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.


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

    Attributes:
        area (Parameter): Area under the curve.
        center (Parameter): Resonance frequency, approximately the
            peak position.
        width (Parameter): Damping constant, approximately the
            half width at half max (HWHM) of the peaks.
        unit (str | sc.Unit): Unit of the parameters.
        display_name (str | None): Display name of the component.
        unique_name (str | None): Unique name of the component.
    """

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

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

        Raises:
            TypeError: If any of the parameters are not numbers or
                Parameters.
            ValueError: If center or width are not positive.
        """

        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.

        Args:
            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.

        Args:
            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.

        Args:
            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.

        Args:
            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}, unit = {self._unit},\n \
        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 Int | float

Area under the curve.

1.0
center Int | float

Resonance frequency, approximately the peak position.

1.0
width Int | float

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

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Raises:

Type Description
TypeError

If any of the parameters are not numbers or Parameters.

ValueError

If center or width are not positive.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
 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
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,
):
    """Initialize the Damped Harmonic Oscillator.

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

    Raises:
        TypeError: If any of the parameters are not numbers or
            Parameters.
        ValueError: If center or width are not positive.
    """

    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:

Name Type Description
str str

A string representation of the Damped Harmonic Oscillator.

Source code in src/easydynamics/sample_model/components/damped_harmonic_oscillator.py
209
210
211
212
213
214
215
216
217
218
219
220
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}, unit = {self._unit},\n \
    area = {self.area},\n center = {self.center},\n width = {self.width})'
    )
area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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

np.ndarray: The intensity of the DHO at the given x values.

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

    Args:
        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:

Name Type Description
Parameter 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.

Parameters:

Name Type Description Default
center Int | float | None

Center of the delta function. If None, defaults to 0 and is fixed.

None
area Int | float

Total area under the curve.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Attributes:

Name Type Description
center Parameter

Center of the delta function.

area Parameter

Total area under the curve.

unit str | Unit

Unit of the parameters.

display_name str | None

Name of the component.

unique_name str | None

Unique name of the component.

Source code in src/easydynamics/sample_model/components/delta_function.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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
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.

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

    Attributes:
        center (Parameter): Center of the delta function.
        area (Parameter): Total area under the curve.
        unit (str | sc.Unit): Unit of the parameters.
        display_name (str | None): Name of the component.
        unique_name (str | None): Unique name of the component.
    """

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

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

        Raises:
            TypeError: If center is not a number or None.
            TypeError: If area is not a number.
            TypeError: If unit is not a string or sc.Unit.
        """
        # 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.

        Args:
            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.

        Args:
            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.

        Args:
            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:
                left = x[1] - x[0] if x.size > 1 else 0.5
            else:
                left = x[i] - x[i - 1]

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

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

            model[i] = area / bin_width

        return model

    def __repr__(self) -> 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 \
        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 Int | float | None

Center of the delta function. If None, defaults to 0 and is fixed.

None
area Int | float

Total area under the curve.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Raises:

Type Description
TypeError

If center is not a number or None.

TypeError

If area is not a number.

TypeError

If unit is not a string or sc.Unit.

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

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

    Raises:
        TypeError: If center is not a number or None.
        TypeError: If area is not a number.
        TypeError: If unit is not a string or sc.Unit.
    """
    # 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:

Name Type Description
str str

A string representation of the Delta function.

Source code in src/easydynamics/sample_model/components/delta_function.py
184
185
186
187
188
189
190
191
192
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 \
    area = {self.area},\n center = {self.center}'
area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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

np.ndarray: The evaluated Delta function at the given x values.

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

    Args:
        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:
            left = x[1] - x[0] if x.size > 1 else 0.5
        else:
            left = x[i] - x[i - 1]

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

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

        model[i] = area / bin_width

    return model

gaussian

Gaussian

Bases: CreateParametersMixin, ModelComponent

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.

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

Attributes: area (Parameter): Area of the Gaussian. center (Parameter): Center of the Gaussian. width (Parameter): Standard deviation of the Gaussian. unit (str | sc.Unit): Unit of the parameters. display_name (str | None): Name of the component. unique_name (str | None): Unique name of the component.

Source code in src/easydynamics/sample_model/components/gaussian.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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
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.

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

     Attributes:
         area (Parameter): Area of the Gaussian.
         center (Parameter): Center of the Gaussian.
         width (Parameter): Standard deviation of the Gaussian.
         unit (str | sc.Unit): Unit of the parameters.
         display_name (str | None): Name of the component.
         unique_name (str | None): Unique name of the component.
    """

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

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

        Raises:
            TypeError: If area, center, or width are not numbers or
                Parameters.
            ValueError: If width is not positive.
            TypeError: If unit is not a string or sc.Unit.
        """
        # 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.

        Args:
            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 center parameter value.

        Args:
            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.

        Args:
            value (Numeric | None): 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.


        Args:
            x (Numeric or list or np.ndarray or sc.Variable or 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 \
            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 Int | float | Parameter | None

Area of the Gaussian.

1.0
center Int | float | Parameter | None

Center of the Gaussian. If None, defaults to 0 and is fixed.

None
width Int | float | Parameter | None

Standard deviation.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Raises:

Type Description
TypeError

If area, center, or width are not numbers or Parameters.

ValueError

If width is not positive.

TypeError

If unit is not a string or sc.Unit.

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

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

    Raises:
        TypeError: If area, center, or width are not numbers or
            Parameters.
        ValueError: If width is not positive.
        TypeError: If unit is not a string or sc.Unit.
    """
    # 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:

Name Type Description
str str

A string representation of the Gaussian.

Source code in src/easydynamics/sample_model/components/gaussian.py
221
222
223
224
225
226
227
228
229
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 \
        area = {self.area},\n center = {self.center},\n width = {self.width})'
area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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 or list or ndarray or Variable or DataArray

The x values at which to evaluate the Gaussian.

required

Returns:

Type Description
ndarray

np.ndarray: The intensity of the Gaussian at the given x values.

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


    Args:
        x (Numeric or list or np.ndarray or sc.Variable or 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:

Name Type Description
Parameter 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.

Parameters:

Name Type Description Default
area Int | float | Parameter

Area of the Lorentzian.

1.0
center Int | float | None | Parameter

Center of the Lorentzian. If None, defaults to 0 and is fixed

None
width Int | float | Parameter

Half width at half maximum (HWHM).

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Attributes:

Name Type Description
area Parameter

Area of the Lorentzian.

center Parameter

Center of the Lorentzian.

width Parameter

Half width at half maximum (HWHM) of the Lorentzian.

unit str | Unit

Unit of the parameters.

display_name str | None

Name of the component.

unique_name str | None

Unique name of the component.

Source code in src/easydynamics/sample_model/components/lorentzian.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
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
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.

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

    Attributes:
        area (Parameter): Area of the Lorentzian.
        center (Parameter): Center of the Lorentzian.
        width (Parameter): Half width at half maximum (HWHM) of the
            Lorentzian.
        unit (str | sc.Unit): Unit of the parameters.
        display_name (str | None): Name of the component.
        unique_name (str | None): Unique name of the component.
    """

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

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

        Raises:
            TypeError: If any of the parameters are of the wrong type.
            ValueError: If width is not positive.
        """

        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.

        Args:
            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.

        Args:
            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).

        Args:
            value (Numeric | None): 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 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).

        Args:
            x (Numeric or list or np.ndarray or sc.Variable or 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 \
            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 Int | float | Parameter

Area of the Lorentzian.

1.0
center Int | float | None | Parameter

Center of the Lorentzian. If None, defaults to 0 and is fixed

None
width Int | float | Parameter

Half width at half maximum (HWHM).

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV".

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

None

Raises:

Type Description
TypeError

If any of the parameters are of the wrong type.

ValueError

If width is not positive.

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

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

    Raises:
        TypeError: If any of the parameters are of the wrong type.
        ValueError: If width is not positive.
    """

    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:

Name Type Description
str str

A string representation of the Lorentzian.

Source code in src/easydynamics/sample_model/components/lorentzian.py
204
205
206
207
208
209
210
211
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 \
        area = {self.area},\n center = {self.center},\n width = {self.width})'
area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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 or list or ndarray or Variable or DataArray

The x values at which to evaluate the Lorentzian.

required

Returns:

Type Description
ndarray

np.ndarray: The intensity of the Lorentzian at the given x values.

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

    Args:
        x (Numeric or list or np.ndarray or sc.Variable or 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:

Name Type Description
Parameter 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
class CreateParametersMixin:
    """Provides parameter creation and validation methods for model
    components.

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

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

        If the area is negative, a warning is raised.
        If the area is non-negative, its minimum is set to 0 to avoid it
        accidentally becoming negative during fitting.

        Args:
            area (Numeric | Parameter): The area value or Parameter.
            name (str): The name of the model component.
            unit (str | sc.Unit): The unit of the area Parameter.
            minimum_area (float): The minimum allowed area.

        Returns:
            Parameter: The validated area Parameter.

        Raises:
            TypeError: If area is not a number or a Parameter.
            Warning: If area is negative.
        """
        if not isinstance(area, (Parameter, Numeric)):
            raise TypeError('area must be a number or a Parameter.')

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

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

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

        return area

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

        Args:
            center (Numeric | Parameter | 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): The unit of the center Parameter.

        Returns:
            Parameter: The validated center Parameter.

        Raises:
            TypeError: If center is not None, a number, or a Parameter.
        """
        if center is not None and not isinstance(center, (Numeric, Parameter)):
            raise TypeError('center must be None, a number, or a Parameter.')

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

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

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

        Args:
            width (Numeric or Parameter): The width value or Parameter.
            name (str): The name of the model component.
            param_name (str): The name of the width parameter.
            unit (str or sc.Unit): The unit of the width Parameter.
            minimum_width (float): The minimum allowed width.

        Returns:
            Parameter: The validated width Parameter.

        Raises:
            TypeError: If width is not a number or a Parameter.
            ValueError: If width is non-positive.
        """
        if not isinstance(width, (Numeric, Parameter)):
            raise TypeError(f'{param_name} must be a number or a Parameter.')

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

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

        return width

model_component

ModelComponent

Bases: ModelBase

Abstract base class for all model components.

Parameters:

Name Type Description Default
unit str | Unit

The unit of the model component. Default is 'meV'.

'meV'
display_name str | None

A human-readable name for the component. Default is None.

None
unique_name str | None

A unique identifier for the component. Default is None.

None

Attributes:

Name Type Description
unit str

The unit of the model component.

display_name str | None

A human-readable name for the component.

unique_name str | None

A unique identifier for the component.

Source code in src/easydynamics/sample_model/components/model_component.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
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
class ModelComponent(ModelBase):
    """Abstract base class for all model components.

    Args:
        unit (str | sc.Unit): The unit of the model component.
            Default is 'meV'.
        display_name (str | None): A human-readable name for the
            component. Default is None.
        unique_name (str | None): A unique identifier for the
            component. Default is None.

    Attributes:
        unit (str): The unit of the model component.
        display_name (str | None): A human-readable name for the
            component.
        unique_name (str | None): A unique identifier for the
            component.
    """

    def __init__(
        self,
        unit: str | sc.Unit = 'meV',
        display_name: str | None = None,
        unique_name: str | None = None,
    ):
        """Initialize the ModelComponent.

        Args:
            unit (str | sc.Unit): The unit of the model component.
                Default is 'meV'.
            display_name (str | None): A human-readable name for the
                component. Default is None.
            unique_name (str | None): A unique identifier for the
                component. Default is None.

        Raises:
            TypeError: If unit is not a string or scipp Unit.
        """
        self.validate_unit(unit)
        super().__init__(display_name=display_name, unique_name=unique_name)
        self._unit = unit

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

        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.

        Args:
            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.'
            )
        )  # noqa: E501

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

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

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

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

        Args:
            x (Numeric | List[Numeric] | np.ndarray | sc.Variable |
            sc.DataArray): The input data to prepare.

        Returns:
            np.ndarray: The prepared input data as a numpy array.

        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.
        """

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

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

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

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

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

        return np.sort(x_in)

    @staticmethod
    def validate_unit(unit) -> None:
        """Validate that the unit is either a string or a scipp Unit.

        Raises:
            TypeError: If unit is not a string or scipp Unit.
        """
        if unit is not None and not isinstance(unit, (str, sc.Unit)):
            raise TypeError(
                f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}'
            )

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

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

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

    @abstractmethod
    def evaluate(
        self, x: Numeric | List[Numeric] | np.ndarray | sc.Variable | sc.DataArray
    ) -> np.ndarray:
        """Abstract method to evaluate the model component at input x.
        Must be implemented by subclasses.

        Args:
            x (Numeric | list[Numeric] | np.ndarray | sc.Variable | sc.DataArray):
                Input values.

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

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

'meV'
display_name str | None

A human-readable name for the component. Default is None.

None
unique_name str | None

A unique identifier for the component. Default is None.

None

Raises:

Type Description
TypeError

If unit is not a string or scipp Unit.

Source code in src/easydynamics/sample_model/components/model_component.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def __init__(
    self,
    unit: str | sc.Unit = 'meV',
    display_name: str | None = None,
    unique_name: str | None = None,
):
    """Initialize the ModelComponent.

    Args:
        unit (str | sc.Unit): The unit of the model component.
            Default is 'meV'.
        display_name (str | None): A human-readable name for the
            component. Default is None.
        unique_name (str | None): A unique identifier for the
            component. Default is None.

    Raises:
        TypeError: If unit is not a string or scipp Unit.
    """
    self.validate_unit(unit)
    super().__init__(display_name=display_name, unique_name=unique_name)
    self._unit = unit
__repr__()

Return a string representation of the ModelComponent.

Returns:

Name Type Description
str str

A string representation of the ModelComponent.

Source code in src/easydynamics/sample_model/components/model_component.py
224
225
226
227
228
229
230
231
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 or Unit

The new unit to convert to.

required
Source code in src/easydynamics/sample_model/components/model_component.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def convert_unit(self, unit: str | sc.Unit):
    """Convert the unit of the Parameters in the component.

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

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

Abstract method to evaluate the model component at input x. Must be implemented by subclasses.

Parameters:

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

Input values.

required

Returns:

Type Description
ndarray

np.ndarray: Evaluated function values.

Source code in src/easydynamics/sample_model/components/model_component.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
@abstractmethod
def evaluate(
    self, x: Numeric | List[Numeric] | np.ndarray | sc.Variable | sc.DataArray
) -> np.ndarray:
    """Abstract method to evaluate the model component at input x.
    Must be implemented by subclasses.

    Args:
        x (Numeric | list[Numeric] | np.ndarray | sc.Variable | sc.DataArray):
            Input values.

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

Fix all parameters in the model component.

Source code in src/easydynamics/sample_model/components/model_component.py
88
89
90
91
92
93
def fix_all_parameters(self):
    """Fix all parameters in the model component."""

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

Free all parameters in the model component.

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

Get the unit.

Returns:

Name Type Description
str str

The unit of the model component.

validate_unit(unit) staticmethod

Validate that the unit is either a string or a scipp Unit.

Raises:

Type Description
TypeError

If unit is not a string or scipp Unit.

Source code in src/easydynamics/sample_model/components/model_component.py
173
174
175
176
177
178
179
180
181
182
183
@staticmethod
def validate_unit(unit) -> None:
    """Validate that the unit is either a string or a scipp Unit.

    Raises:
        TypeError: If unit is not a string or scipp Unit.
    """
    if unit is not None and not isinstance(unit, (str, sc.Unit)):
        raise TypeError(
            f'unit must be None, a string, or a scipp Unit, got {type(unit).__name__}'
        )

polynomial

Polynomial

Bases: ModelComponent

Polynomial function component.

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
coefficients list or tuple

Coefficients c0, c1, ..., cN

(0.0,)
unit str or Unit

Unit of the Polynomial component.

'meV'
display_name str

Display name of the Polynomial component.

'Polynomial'
unique_name str or None

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

None

Attributes:

Name Type Description
coefficients list of Parameter

Coefficients of the polynomial as Parameters.

unit str

Unit of the Polynomial component.

display_name str

Display name of the Polynomial component.

unique_name str or None

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

Source code in src/easydynamics/sample_model/components/polynomial.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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
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.

    Args:
        coefficients (list or tuple): Coefficients c0, c1, ..., cN
        unit (str or sc.Unit): Unit of the Polynomial component.
        display_name (str): Display name of the Polynomial component.
        unique_name (str or None): Unique name of the component.
            If None, a unique_name is automatically generated.

    Attributes:
        coefficients (list of Parameter): Coefficients of the polynomial
            as Parameters.
        unit (str): Unit of the Polynomial component.
        display_name (str): Display name of the Polynomial component.
        unique_name (str or None): Unique name of the component.
            If None, a unique_name is automatically generated.
    """

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

        Args:
            coefficients (list or tuple): Coefficients c0, c1, ..., cN
            unit (str or sc.Unit): Unit of the Polynomial component.
            display_name (str): Display name of the Polynomial
                component.
            unique_name (str or None): Unique name of the component.
                If None, a unique_name is automatically generated.

        Raises:
            TypeError: If coefficients is not a sequence of numbers or
                Parameters.
            ValueError: If coefficients is an empty sequence.
            TypeError: If any item in coefficients is not a number or
                Parameter.
            UnitError: If unit is not a string or sc.Unit.
        """

        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.

        Args:
            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.
            ValueError: If the length of coeffs does not match the
                existing number of coefficients.
            TypeError: If any item in coeffs is not a number or
                Parameter.
        """
        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.
        """
        coefficient_list = [param.value for param in self._coefficients]
        return coefficient_list

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

        Args:
            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,
            )
        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.

        Args:
            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 parameters from the model component.

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

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

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

        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}, \
            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 list or tuple

Coefficients c0, c1, ..., cN

(0.0,)
unit str or Unit

Unit of the Polynomial component.

'meV'
display_name str

Display name of the Polynomial component.

'Polynomial'
unique_name str or None

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

None

Raises:

Type Description
TypeError

If coefficients is not a sequence of numbers or Parameters.

ValueError

If coefficients is an empty sequence.

TypeError

If any item in coefficients is not a number or Parameter.

UnitError

If unit is not a string or sc.Unit.

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

    Args:
        coefficients (list or tuple): Coefficients c0, c1, ..., cN
        unit (str or sc.Unit): Unit of the Polynomial component.
        display_name (str): Display name of the Polynomial
            component.
        unique_name (str or None): Unique name of the component.
            If None, a unique_name is automatically generated.

    Raises:
        TypeError: If coefficients is not a sequence of numbers or
            Parameters.
        ValueError: If coefficients is an empty sequence.
        TypeError: If any item in coefficients is not a number or
            Parameter.
        UnitError: If unit is not a string or sc.Unit.
    """

    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:

Name Type Description
str str

A string representation of the Polynomial.

Source code in src/easydynamics/sample_model/components/polynomial.py
244
245
246
247
248
249
250
251
252
253
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}, \
        unit = {self._unit},\n coefficients = [{coeffs_str}])'
coefficient_values()

Get the coefficients of the polynomial as a list.

Returns:

Type Description
list[float]

list[float]: The coefficient values of the polynomial.

Source code in src/easydynamics/sample_model/components/polynomial.py
144
145
146
147
148
149
150
151
def coefficient_values(self) -> list[float]:
    """Get the coefficients of the polynomial as a list.

    Returns:
        list[float]: The coefficient values of the polynomial.
    """
    coefficient_list = [param.value for param in self._coefficients]
    return coefficient_list
coefficients property writable

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

Returns:

Type Description
list[Parameter]

list[Parameter]: The coefficients of the polynomial.

convert_unit(unit)

Convert the unit of the polynomial.

Parameters:

Name Type Description Default
unit str or Unit

The target unit to convert to.

required

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
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
def convert_unit(self, unit: str | sc.Unit):
    """Convert the unit of the polynomial.

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

    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:

Name Type Description
int 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

np.ndarray: The evaluated Polynomial at the given x values.

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

    Args:
        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,
        )
    return result
get_all_variables()

Get all parameters from the model component.

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

Source code in src/easydynamics/sample_model/components/polynomial.py
211
212
213
214
215
216
217
def get_all_variables(self) -> list[DescriptorBase]:
    """Get all parameters from the model component.

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

voigt

Voigt

Bases: CreateParametersMixin, ModelComponent

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

Use scipy.special.voigt_profile to evaluate the Voigt profile.

Parameters:

Name Type Description Default
area Int | float

Total area under the curve.

1.0
center Int | float | None

Center of the Voigt profile.

None
gaussian_width Int | float

Standard deviation of the Gaussian part.

1.0
lorentzian_width Int | float

Half width at half max (HWHM) of the Lorentzian part.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV"

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

None

Attributes:

Name Type Description
area Parameter

Total area under the curve.

center Parameter

Center of the Voigt profile.

gaussian_width Parameter

Standard deviation of the Gaussian part.

lorentzian_width Parameter

Half width at half max (HWHM) of the Lorentzian part.

unit str | Unit

Unit of the parameters.

display_name str | None

Display name of the component.

unique_name str | None

Unique name of the component.

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

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

    Attributes:
        area (Parameter): Total area under the curve.
        center (Parameter): Center of the Voigt profile.
        gaussian_width (Parameter): Standard deviation of the Gaussian
            part.
        lorentzian_width (Parameter): Half width at half max (HWHM) of
            the Lorentzian part.
        unit (str | sc.Unit): Unit of the parameters.
        display_name (str | None): Display name of the component.
        unique_name (str | None): Unique name of the component.
    """

    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,
    ):
        """Initialize a Voigt component.

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

        Raises:
            TypeError: If any of the parameters are not of the correct
                type.
            ValueError: If any of the parameters are not valid (e.g.
                negative widths).
        """

        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.

        Args:
            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.

        Args:
            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.

        Args:
            value (Numeric | None): 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('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.

        Args:
            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.


        Args:
            x (Numeric | list[Numeric] | 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 \
        area = {self.area},\n \
        center = {self.center},\n \
        gaussian_width = {self.gaussian_width},\n \
        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 Int | float

Total area under the curve.

1.0
center Int | float | None

Center of the Voigt profile.

None
gaussian_width Int | float

Standard deviation of the Gaussian part.

1.0
lorentzian_width Int | float

Half width at half max (HWHM) of the Lorentzian part.

1.0
unit str | Unit

Unit of the parameters. Defaults to "meV"

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

None

Raises:

Type Description
TypeError

If any of the parameters are not of the correct type.

ValueError

If any of the parameters are not valid (e.g. negative widths).

Source code in src/easydynamics/sample_model/components/voigt.py
 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
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,
):
    """Initialize a Voigt component.

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

    Raises:
        TypeError: If any of the parameters are not of the correct
            type.
        ValueError: If any of the parameters are not valid (e.g.
            negative widths).
    """

    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:

Name Type Description
str str

A string representation of the Voigt.

Source code in src/easydynamics/sample_model/components/voigt.py
240
241
242
243
244
245
246
247
248
249
250
251
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 \
    area = {self.area},\n \
    center = {self.center},\n \
    gaussian_width = {self.gaussian_width},\n \
    lorentzian_width = {self.lorentzian_width})'
area property writable

Get the area parameter.

Returns:

Name Type Description
Parameter Parameter

The area parameter.

center property writable

Get the center parameter.

Returns:

Name Type Description
Parameter 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[Numeric] | ndarray | Variable | DataArray

The x values at which to evaluate the Voigt.

required

Returns:

Type Description
ndarray

np.ndarray: The intensity of the Voigt at the given x values.

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


    Args:
        x (Numeric | list[Numeric] | 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:

Name Type Description
Parameter Parameter

The Gaussian width parameter.

lorentzian_width property writable

Get the Lorentzian width parameter (HWHM).

Returns:

Name Type Description
Parameter 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.

Parameters:

Name Type Description Default
display_name str

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.

None
unit str | Unit

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

'meV'
scale Numeric

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

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s. Defaults to 1.0.

1.0

Attributes:

Name Type Description
unit str | Unit

Unit of the diffusion model.

scale Parameter

Scale parameter of the diffusion model.

diffusion_coefficient Parameter

Diffusion coefficient D in m^2/s.

Example usage:

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

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

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

    Attributes:
        unit (str | sc.Unit): Unit of the diffusion model.
        scale (Parameter): Scale parameter of the diffusion model.
        diffusion_coefficient (Parameter): Diffusion coefficient D in
            m^2/s.

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

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

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

        Raises:
            TypeError: If scale or diffusion_coefficient is not a
                number.
            ValueError: If scale is negative.
            UnitError: If unit is not a string or scipp Unit.
        """
        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 = DescriptorNumber.from_scipp('hbar', scipp_hbar)
        self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
        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.

        Args:
            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.

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

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

        Q = _validate_and_convert_Q(Q)

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

        return width

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

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

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

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

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

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

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

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

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

        Returns:
            List[ComponentCollection]: List of ComponentCollections with
                Lorentzian components 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).

        Raises:
            TypeError: If component_display_name is not a string.
        """
        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.

        Args:
         Q (float): Scattering vector in 1/angstrom

         Returns:
             str: Dependency expression for the width.

         Raises:
             TypeError: If Q is not a float.
        """
        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.

        Args:
            QISF (float): Quasielastic Incoherent Scattering Function.

        Returns:
            str: Dependency expression for the area.

        Raises:
            TypeError: If QISF is not a float.
        """
        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

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.

None
unit str | Unit

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

'meV'
scale Numeric

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

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s. Defaults to 1.0.

1.0

Raises:

Type Description
TypeError

If scale or diffusion_coefficient is not a number.

ValueError

If scale is negative.

UnitError

If unit is not a string or scipp Unit.

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

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

    Raises:
        TypeError: If scale or diffusion_coefficient is not a
            number.
        ValueError: If scale is negative.
        UnitError: If unit is not a string or scipp Unit.
    """
    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 = DescriptorNumber.from_scipp('hbar', scipp_hbar)
    self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
    self._diffusion_coefficient = diffusion_coefficient

__repr__()

String representation of the BrownianTranslationalDiffusion model.

Returns:

Name Type Description
str str

String representation of the BrownianTranslationalDiffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
333
334
335
336
337
338
339
340
341
342
343
344
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 ndarray | Numeric | list | ArrayLike

Scattering vector in 1/angstrom

required

Returns:

Type Description
ndarray

np.ndarray: EISF values (dimensionless).

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

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

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

calculate_QISF(Q)

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

Parameters:

Name Type Description Default
Q ndarray | Numeric | list | ArrayLike

Scattering vector in 1/angstrom

required

Returns:

Type Description
ndarray

np.ndarray: QISF values (dimensionless).

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

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

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

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

calculate_width(Q)

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

Parameters:

Name Type Description Default
Q ndarray | Numeric | list | ArrayLike

Scattering vector in 1/angstrom

required

Returns:

Type Description
ndarray

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

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

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

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

    Q = _validate_and_convert_Q(Q)

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

    return width

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

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

Parameters:

Name Type Description Default
Q Number, list, or np.ndarray

Scattering vector values.

required
component_display_name str

Name of the Lorentzian component.

'Brownian translational diffusion'

Returns:

Type Description
List[ComponentCollection]

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

Raises:

Type Description
TypeError

If component_display_name is not a string.

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

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

    Returns:
        List[ComponentCollection]: List of ComponentCollections with
            Lorentzian components 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).

    Raises:
        TypeError: If component_display_name is not a string.
    """
    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:

Name Type Description
Parameter 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.

Parameters:

Name Type Description Default
display_name str

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.

None
unit str | Unit

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

'meV'
scale Numeric

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

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s. Defaults to 1.0.

1.0
relaxation_time Numeric

Relaxation time t in ps. Defaults to 1.0.

1.0

Attributes:

Name Type Description
unit str | Unit

Unit of the diffusion model.

scale Parameter

Scale parameter of the diffusion model.

diffusion_coefficient Parameter

Diffusion coefficient D in m^2/s.

relaxation_time Parameter

Relaxation time t in ps.

Example usage:

Q = np.linspace(0.5, 2, 7) energy = np.linspace(-2, 2, 501) scale = 1.0 diffusion_coefficient = 2.4e-9 # m^2/s 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 examples.

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.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
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.

    Args:
        display_name (str): Display name of the diffusion model.
        unique_name (str | None): Unique name of the diffusion model. If
            None, a unique name will be generated.
        unit (str | sc.Unit): Unit of the diffusion model. Must be
            convertible to meV. Defaults to "meV".
        scale (Numeric): Scale factor for the diffusion model. Must be
            a non-negative number. Defaults to 1.0.
        diffusion_coefficient (Numeric): Diffusion coefficient D in
            m^2/s. Defaults to 1.0.
        relaxation_time (Numeric): Relaxation time t in ps. Defaults to
            1.0.

    Attributes:
        unit (str | sc.Unit): Unit of the diffusion model.
        scale (Parameter): Scale parameter of the diffusion model.
        diffusion_coefficient (Parameter): Diffusion coefficient D in
            m^2/s.
        relaxation_time (Parameter): Relaxation time t in ps.

    Example usage:
    >>> Q = np.linspace(0.5, 2, 7)
    >>> energy = np.linspace(-2, 2, 501)
    >>> scale = 1.0
    >>> diffusion_coefficient = 2.4e-9  # m^2/s
    >>> 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 examples.
    """

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

        Args:
            display_name (str): Display name of the diffusion model.
            unique_name (str | None): Unique name of the diffusion
                model. If None, a unique name will be generated.
            unit (str | sc.Unit): Unit of the diffusion model. Must be
                convertible to meV. Defaults to "meV".
            scale (Numeric): Scale factor for the diffusion model. Must
                be a non-negative number. Defaults to 1.0.
            diffusion_coefficient (Numeric): Diffusion coefficient D in
                m^2/s. Defaults to 1.0.
            relaxation_time (Numeric): Relaxation time t in ps. Defaults
                to 1.0.

        Raises:
            TypeError: If scale, diffusion_coefficient, or
                relaxation_time  are not numbers.
            ValueError: If scale is negative.
            UnitError: If unit is not a valid unit string or scipp Unit.
        """
        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 = DescriptorNumber.from_scipp('hbar', scipp_hbar)
        self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
        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.

        Args:
            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.

        Args:
            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.

        Args:
            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

        width = numerator / denominator
        return width

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

        Args:
            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)
        EISF = np.zeros_like(Q)
        return EISF

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

        Args:
            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)
        QISF = np.ones_like(Q)
        return QISF

    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.

        Args:
            Q (Q_type): Scattering vector in 1/angstrom. Can be a single
                value or an array of values.
            component_display_name (str): Name of the Jump Diffusion
                Lorentzian component.

        Returns:
            List[ComponentCollection]: List of ComponentCollections with
                Jump Diffusion Lorentzian components.

        Raises:
            TypeError: If component_display_name is not a string.
        """
        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.

        Args:
            Q (float): Scattering vector in 1/angstrom

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

        # Q is given as a float, so we need to add the units
        return f'hbar * D* {Q} **2/(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.

        Args:
            QISF (float): Q-dependent intermediate scattering function.

        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

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.

None
unit str | Unit

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

'meV'
scale Numeric

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

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s. Defaults to 1.0.

1.0
relaxation_time Numeric

Relaxation time t in ps. Defaults to 1.0.

1.0

Raises:

Type Description
TypeError

If scale, diffusion_coefficient, or relaxation_time are not numbers.

ValueError

If scale is negative.

UnitError

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

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

    Args:
        display_name (str): Display name of the diffusion model.
        unique_name (str | None): Unique name of the diffusion
            model. If None, a unique name will be generated.
        unit (str | sc.Unit): Unit of the diffusion model. Must be
            convertible to meV. Defaults to "meV".
        scale (Numeric): Scale factor for the diffusion model. Must
            be a non-negative number. Defaults to 1.0.
        diffusion_coefficient (Numeric): Diffusion coefficient D in
            m^2/s. Defaults to 1.0.
        relaxation_time (Numeric): Relaxation time t in ps. Defaults
            to 1.0.

    Raises:
        TypeError: If scale, diffusion_coefficient, or
            relaxation_time  are not numbers.
        ValueError: If scale is negative.
        UnitError: If unit is not a valid unit string or scipp Unit.
    """
    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 = DescriptorNumber.from_scipp('hbar', scipp_hbar)
    self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
    self._diffusion_coefficient = diffusion_coefficient
    self._relaxation_time = relaxation_time

__repr__()

String representation of the JumpTranslationalDiffusion model.

Returns:

Name Type Description
str str

String representation of the JumpTranslationalDiffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
382
383
384
385
386
387
388
389
390
391
392
393
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

np.ndarray: EISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
224
225
226
227
228
229
230
231
232
233
234
235
236
def calculate_EISF(self, Q: Q_type) -> np.ndarray:
    """Calculate the Elastic Incoherent Structure Factor (EISF).

    Args:
        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)
    EISF = np.zeros_like(Q)
    return EISF

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: np.ndarray: QISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
238
239
240
241
242
243
244
245
246
247
248
249
250
def calculate_QISF(self, Q: Q_type) -> np.ndarray:
    """Calculate the Quasi-Elastic Incoherent Structure Factor
    (QISF).

    Args:
        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)
    QISF = np.ones_like(Q)
    return QISF

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

np.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
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
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.

    Args:
        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

    width = numerator / denominator
    return width

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'

Returns:

Type Description
List[ComponentCollection]

List[ComponentCollection]: List of ComponentCollections with Jump Diffusion Lorentzian components.

Raises:

Type Description
TypeError

If component_display_name is not a string.

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

    Args:
        Q (Q_type): Scattering vector in 1/angstrom. Can be a single
            value or an array of values.
        component_display_name (str): Name of the Jump Diffusion
            Lorentzian component.

    Returns:
        List[ComponentCollection]: List of ComponentCollections with
            Jump Diffusion Lorentzian components.

    Raises:
        TypeError: If component_display_name is not a string.
    """
    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:

Name Type Description
Parameter Parameter

Diffusion coefficient D.

relaxation_time property writable

Get the relaxation time parameter t.

Returns:

Name Type Description
Parameter 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.

Parameters:

Name Type Description Default
display_name str

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.

None
unit str | Unit

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

'meV'
scale Numeric

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

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s. Defaults to 1.0.

1.0

Attributes:

Name Type Description
unit str | Unit

Unit of the diffusion model.

scale Parameter

Scale parameter of the diffusion model.

diffusion_coefficient Parameter

Diffusion coefficient D in m^2/s.

Example usage:

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

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

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

    Attributes:
        unit (str | sc.Unit): Unit of the diffusion model.
        scale (Parameter): Scale parameter of the diffusion model.
        diffusion_coefficient (Parameter): Diffusion coefficient D in
            m^2/s.

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

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

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

        Raises:
            TypeError: If scale or diffusion_coefficient is not a
                number.
            ValueError: If scale is negative.
            UnitError: If unit is not a string or scipp Unit.
        """
        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 = DescriptorNumber.from_scipp('hbar', scipp_hbar)
        self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
        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.

        Args:
            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.

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

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

        Q = _validate_and_convert_Q(Q)

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

        return width

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

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

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

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

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

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

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

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

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

        Returns:
            List[ComponentCollection]: List of ComponentCollections with
                Lorentzian components 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).

        Raises:
            TypeError: If component_display_name is not a string.
        """
        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.

        Args:
         Q (float): Scattering vector in 1/angstrom

         Returns:
             str: Dependency expression for the width.

         Raises:
             TypeError: If Q is not a float.
        """
        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.

        Args:
            QISF (float): Quasielastic Incoherent Scattering Function.

        Returns:
            str: Dependency expression for the area.

        Raises:
            TypeError: If QISF is not a float.
        """
        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

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.

None
unit str | Unit

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

'meV'
scale Numeric

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

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s. Defaults to 1.0.

1.0

Raises:

Type Description
TypeError

If scale or diffusion_coefficient is not a number.

ValueError

If scale is negative.

UnitError

If unit is not a string or scipp Unit.

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

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

    Raises:
        TypeError: If scale or diffusion_coefficient is not a
            number.
        ValueError: If scale is negative.
        UnitError: If unit is not a string or scipp Unit.
    """
    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 = DescriptorNumber.from_scipp('hbar', scipp_hbar)
    self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
    self._diffusion_coefficient = diffusion_coefficient
__repr__()

String representation of the BrownianTranslationalDiffusion model.

Returns:

Name Type Description
str str

String representation of the BrownianTranslationalDiffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/brownian_translational_diffusion.py
333
334
335
336
337
338
339
340
341
342
343
344
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 ndarray | Numeric | list | ArrayLike

Scattering vector in 1/angstrom

required

Returns:

Type Description
ndarray

np.ndarray: EISF values (dimensionless).

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

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

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

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

Parameters:

Name Type Description Default
Q ndarray | Numeric | list | ArrayLike

Scattering vector in 1/angstrom

required

Returns:

Type Description
ndarray

np.ndarray: QISF values (dimensionless).

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

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

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

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

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

Parameters:

Name Type Description Default
Q ndarray | Numeric | list | ArrayLike

Scattering vector in 1/angstrom

required

Returns:

Type Description
ndarray

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

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

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

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

    Q = _validate_and_convert_Q(Q)

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

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

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

Parameters:

Name Type Description Default
Q Number, list, or np.ndarray

Scattering vector values.

required
component_display_name str

Name of the Lorentzian component.

'Brownian translational diffusion'

Returns:

Type Description
List[ComponentCollection]

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

Raises:

Type Description
TypeError

If component_display_name is not a string.

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

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

    Returns:
        List[ComponentCollection]: List of ComponentCollections with
            Lorentzian components 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).

    Raises:
        TypeError: If component_display_name is not a string.
    """
    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:

Name Type Description
Parameter Parameter

Diffusion coefficient D in m^2/s.

diffusion_model_base

DiffusionModelBase

Bases: ModelBase

Base class for constructing diffusion models.

Parameters:

Name Type Description Default
display_name str

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.

None
scale Numeric

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

1.0
unit str | Unit

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

'meV'

Attributes:

Name Type Description
unit str | Unit

Unit of the diffusion model.

scale Parameter

Scale parameter of the diffusion model.

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
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
class DiffusionModelBase(ModelBase):
    """Base class for constructing diffusion models.

    Args:
        display_name (str): Display name of the diffusion model.
        unique_name (str | None): Unique name of the diffusion model.
            If None, a unique name will be generated.
        scale (Numeric): Scale factor for the diffusion model. Must be a
            non-negative number. Defaults to 1.0.
        unit (str | sc.Unit): Unit of the diffusion model. Must be
            convertible to meV. Defaults to "meV".

    Attributes:
        unit (str | sc.Unit): Unit of the diffusion model.
        scale (Parameter): Scale parameter of the diffusion model.
    """

    def __init__(
        self,
        display_name='MyDiffusionModel',
        unique_name: str | None = None,
        scale: Numeric = 1.0,
        unit: str | sc.Unit = 'meV',
    ):
        """Initialize a new DiffusionModel.

        Args:
            display_name (str): Display name of the diffusion model.
            unique_name (str | None): Unique name of the diffusion
                model. If None, a unique name will be generated.
            scale (Numeric): Scale factor for the diffusion model. Must
                be a non-negative number. Defaults to 1.0.
            unit (str | sc.Unit): Unit of the diffusion model. Must be
                convertible to meV. Defaults 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.
        """
        if not isinstance(scale, Numeric):
            raise TypeError('scale must be a number.')

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

        try:
            test = DescriptorNumber(name='test', value=1, unit=unit)
            test.convert_unit('meV')
        except Exception as e:
            raise UnitError(
                f'Invalid unit: {unit}. Unit must be a string or scipp Unit and convertible to meV.'  # noqa: E501
            ) from e

        super().__init__(display_name=display_name, unique_name=unique_name)
        self._unit = unit
        self._scale = scale

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

    @property
    def unit(self) -> str:
        """Get the unit of the energy axis of the DiffusionModel.

        Returns:
            (str | sc.Unit | None): Unit of the DiffusionModel.
        """
        return str(self._unit)

    @unit.setter
    def unit(self, unit_str: str) -> None:
        """The unit of the energy axis is read-only. To change the unit,
        use convert_unit or create a new DiffusionModel with the desired
        unit.

        Args:
            unit_str (str): The new unit to set (ignored)

        Raises:
            AttributeError: Always, since 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.'
            )
        )  # noqa: E501

    @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.

        Args:
            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

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.

None
scale Numeric

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

1.0
unit str | Unit

Unit of the diffusion model. Must be convertible to meV. Defaults 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
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
def __init__(
    self,
    display_name='MyDiffusionModel',
    unique_name: str | None = None,
    scale: Numeric = 1.0,
    unit: str | sc.Unit = 'meV',
):
    """Initialize a new DiffusionModel.

    Args:
        display_name (str): Display name of the diffusion model.
        unique_name (str | None): Unique name of the diffusion
            model. If None, a unique name will be generated.
        scale (Numeric): Scale factor for the diffusion model. Must
            be a non-negative number. Defaults to 1.0.
        unit (str | sc.Unit): Unit of the diffusion model. Must be
            convertible to meV. Defaults 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.
    """
    if not isinstance(scale, Numeric):
        raise TypeError('scale must be a number.')

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

    try:
        test = DescriptorNumber(name='test', value=1, unit=unit)
        test.convert_unit('meV')
    except Exception as e:
        raise UnitError(
            f'Invalid unit: {unit}. Unit must be a string or scipp Unit and convertible to meV.'  # noqa: E501
        ) from e

    super().__init__(display_name=display_name, unique_name=unique_name)
    self._unit = unit
    self._scale = scale
__repr__()

String representation of the Diffusion model.

Returns:

Name Type Description
str str

String representation of the DiffusionModel.

Source code in src/easydynamics/sample_model/diffusion_model/diffusion_model_base.py
134
135
136
137
138
139
140
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:

Name Type Description
Parameter Parameter

scale parameter of the diffusion model

unit property writable

Get the unit of the energy axis of the DiffusionModel.

Returns:

Type Description
str | Unit | None

Unit of the DiffusionModel.

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.

Parameters:

Name Type Description Default
display_name str

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.

None
unit str | Unit

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

'meV'
scale Numeric

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

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s. Defaults to 1.0.

1.0
relaxation_time Numeric

Relaxation time t in ps. Defaults to 1.0.

1.0

Attributes:

Name Type Description
unit str | Unit

Unit of the diffusion model.

scale Parameter

Scale parameter of the diffusion model.

diffusion_coefficient Parameter

Diffusion coefficient D in m^2/s.

relaxation_time Parameter

Relaxation time t in ps.

Example usage:

Q = np.linspace(0.5, 2, 7) energy = np.linspace(-2, 2, 501) scale = 1.0 diffusion_coefficient = 2.4e-9 # m^2/s 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 examples.

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.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
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.

    Args:
        display_name (str): Display name of the diffusion model.
        unique_name (str | None): Unique name of the diffusion model. If
            None, a unique name will be generated.
        unit (str | sc.Unit): Unit of the diffusion model. Must be
            convertible to meV. Defaults to "meV".
        scale (Numeric): Scale factor for the diffusion model. Must be
            a non-negative number. Defaults to 1.0.
        diffusion_coefficient (Numeric): Diffusion coefficient D in
            m^2/s. Defaults to 1.0.
        relaxation_time (Numeric): Relaxation time t in ps. Defaults to
            1.0.

    Attributes:
        unit (str | sc.Unit): Unit of the diffusion model.
        scale (Parameter): Scale parameter of the diffusion model.
        diffusion_coefficient (Parameter): Diffusion coefficient D in
            m^2/s.
        relaxation_time (Parameter): Relaxation time t in ps.

    Example usage:
    >>> Q = np.linspace(0.5, 2, 7)
    >>> energy = np.linspace(-2, 2, 501)
    >>> scale = 1.0
    >>> diffusion_coefficient = 2.4e-9  # m^2/s
    >>> 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 examples.
    """

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

        Args:
            display_name (str): Display name of the diffusion model.
            unique_name (str | None): Unique name of the diffusion
                model. If None, a unique name will be generated.
            unit (str | sc.Unit): Unit of the diffusion model. Must be
                convertible to meV. Defaults to "meV".
            scale (Numeric): Scale factor for the diffusion model. Must
                be a non-negative number. Defaults to 1.0.
            diffusion_coefficient (Numeric): Diffusion coefficient D in
                m^2/s. Defaults to 1.0.
            relaxation_time (Numeric): Relaxation time t in ps. Defaults
                to 1.0.

        Raises:
            TypeError: If scale, diffusion_coefficient, or
                relaxation_time  are not numbers.
            ValueError: If scale is negative.
            UnitError: If unit is not a valid unit string or scipp Unit.
        """
        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 = DescriptorNumber.from_scipp('hbar', scipp_hbar)
        self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
        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.

        Args:
            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.

        Args:
            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.

        Args:
            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

        width = numerator / denominator
        return width

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

        Args:
            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)
        EISF = np.zeros_like(Q)
        return EISF

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

        Args:
            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)
        QISF = np.ones_like(Q)
        return QISF

    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.

        Args:
            Q (Q_type): Scattering vector in 1/angstrom. Can be a single
                value or an array of values.
            component_display_name (str): Name of the Jump Diffusion
                Lorentzian component.

        Returns:
            List[ComponentCollection]: List of ComponentCollections with
                Jump Diffusion Lorentzian components.

        Raises:
            TypeError: If component_display_name is not a string.
        """
        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.

        Args:
            Q (float): Scattering vector in 1/angstrom

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

        # Q is given as a float, so we need to add the units
        return f'hbar * D* {Q} **2/(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.

        Args:
            QISF (float): Q-dependent intermediate scattering function.

        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

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.

None
unit str | Unit

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

'meV'
scale Numeric

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

1.0
diffusion_coefficient Numeric

Diffusion coefficient D in m^2/s. Defaults to 1.0.

1.0
relaxation_time Numeric

Relaxation time t in ps. Defaults to 1.0.

1.0

Raises:

Type Description
TypeError

If scale, diffusion_coefficient, or relaxation_time are not numbers.

ValueError

If scale is negative.

UnitError

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

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

    Args:
        display_name (str): Display name of the diffusion model.
        unique_name (str | None): Unique name of the diffusion
            model. If None, a unique name will be generated.
        unit (str | sc.Unit): Unit of the diffusion model. Must be
            convertible to meV. Defaults to "meV".
        scale (Numeric): Scale factor for the diffusion model. Must
            be a non-negative number. Defaults to 1.0.
        diffusion_coefficient (Numeric): Diffusion coefficient D in
            m^2/s. Defaults to 1.0.
        relaxation_time (Numeric): Relaxation time t in ps. Defaults
            to 1.0.

    Raises:
        TypeError: If scale, diffusion_coefficient, or
            relaxation_time  are not numbers.
        ValueError: If scale is negative.
        UnitError: If unit is not a valid unit string or scipp Unit.
    """
    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 = DescriptorNumber.from_scipp('hbar', scipp_hbar)
    self._angstrom = DescriptorNumber('angstrom', 1e-10, unit='m')
    self._diffusion_coefficient = diffusion_coefficient
    self._relaxation_time = relaxation_time
__repr__()

String representation of the JumpTranslationalDiffusion model.

Returns:

Name Type Description
str str

String representation of the JumpTranslationalDiffusion model.

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
382
383
384
385
386
387
388
389
390
391
392
393
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

np.ndarray: EISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
224
225
226
227
228
229
230
231
232
233
234
235
236
def calculate_EISF(self, Q: Q_type) -> np.ndarray:
    """Calculate the Elastic Incoherent Structure Factor (EISF).

    Args:
        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)
    EISF = np.zeros_like(Q)
    return EISF
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: np.ndarray: QISF values (dimensionless).

Source code in src/easydynamics/sample_model/diffusion_model/jump_translational_diffusion.py
238
239
240
241
242
243
244
245
246
247
248
249
250
def calculate_QISF(self, Q: Q_type) -> np.ndarray:
    """Calculate the Quasi-Elastic Incoherent Structure Factor
    (QISF).

    Args:
        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)
    QISF = np.ones_like(Q)
    return QISF
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

np.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
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
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.

    Args:
        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

    width = numerator / denominator
    return width
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'

Returns:

Type Description
List[ComponentCollection]

List[ComponentCollection]: List of ComponentCollections with Jump Diffusion Lorentzian components.

Raises:

Type Description
TypeError

If component_display_name is not a string.

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

    Args:
        Q (Q_type): Scattering vector in 1/angstrom. Can be a single
            value or an array of values.
        component_display_name (str): Name of the Jump Diffusion
            Lorentzian component.

    Returns:
        List[ComponentCollection]: List of ComponentCollections with
            Jump Diffusion Lorentzian components.

    Raises:
        TypeError: If component_display_name is not a string.
    """
    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:

Name Type Description
Parameter Parameter

Diffusion coefficient D.

relaxation_time property writable

Get the relaxation time parameter t.

Returns:

Name Type Description
Parameter 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.

Parameters:

Name Type Description Default
display_name str | None

The display name of the InstrumentModel. Default is "MyInstrumentModel".

'MyInstrumentModel'
unique_name str | None

The unique name of the InstrumentModel. Default is None.

None
Q ndarray | list | Variable | 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. Default is None.

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. Default is None.

None
energy_offset float | int | None

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

None
unit str | Unit

The unit of the energy axis. Default is 'meV'.

'meV'

Attributes:

Name Type Description
resolution_model ResolutionModel

The resolution model of the instrument.

background_model BackgroundModel

The background model of the instrument.

Q ndarray | None

The Q values where the instrument is modelled.

energy_offset Parameter

The template energy offset Parameter of the instrument. Will be copied to each Q value.

unit str | Unit

The unit of 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
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.

    Args:
        display_name (str | None): The display name of the
            InstrumentModel. Default is "MyInstrumentModel".
        unique_name (str | None): The unique name of the
            InstrumentModel. Default is None.
        Q (np.ndarray | list | sc.Variable | None): The Q values where
            the instrument is modelled.
        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.
            Default is 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. Default is None.
        energy_offset (float | int | None): Template energy offset of
            the instrument. Will be copied to each Q value. If None, the
            energy offset will be 0. Default is None.
        unit (str | sc.Unit): The unit of the energy axis. Default is
            'meV'.

    Attributes:
        resolution_model (ResolutionModel): The resolution model of the
            instrument.
        background_model (BackgroundModel): The background model of the
            instrument.
        Q (np.ndarray | None): The Q values where the instrument is
            modelled.
        energy_offset (Parameter): The template energy offset Parameter
            of the instrument. Will be copied to each Q value.
        unit (str | sc.Unit): The unit of 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',
    ):
        """Initialize an InstrumentModel.

        Args:
            display_name (str | None): The display name of the
                InstrumentModel. Default is "MyInstrumentModel".
            unique_name (str | None): The unique name of the
                InstrumentModel. Default is None.
            Q (np.ndarray | list | sc.Variable | None): The Q values
                where the instrument is modelled.
            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. Default is 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.
                Default is None.
            energy_offset (float | int | None): Template energy offset
                of the instrument. Will be copied to each Q value. If
                None, the energy offset will be 0. Default is None.
            unit (str | sc.Unit): The unit of the energy axis. Default
                is 'meV'.

        Raises:
            TypeError: If resolution_model is not a ResolutionModel or
                None
            TypeError: If background_model is not a BackgroundModel or
                None
            TypeError: If energy_offset is not a number or None
            UnitError: If unit is not a valid unit string or scipp Unit.
        """
        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):
        """Set the resolution model of the instrument.

        Args:
            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):
        """Set the background model of the instrument.

        Args:
            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 or 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.

        Args:
            value (Q_type | None): The new Q values for the
            InstrumentModel.

        Raises:
            TypeError: If value is not a valid Q_type or None.
        """
        self._Q = _validate_and_convert_Q(value)
        self._on_Q_change()

    @property
    def unit(self) -> 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.

        Args:
            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.'
            )
        )  # noqa: E501

    @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):
        """Set the offset parameter of the instrument model.

        Args:
            value (float | int): 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 convert_unit(self, unit_str: str | sc.Unit) -> None:
        """Convert the unit of the InstrumentModel.

        Args:
            unit_str (str | sc.Unit): The unit to convert to.

        Raises:
            TypeError: If unit_str is not a string or scipp Unit.
            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.

        Args:
            Q_index (int | None): The index of the Q value to get
                variables for. If None, get variables for all Q values.

        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.

        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 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 get_energy_offset_at_Q(self, Q_index: int) -> Parameter:
        """Get the energy offset Parameter at a specific Q index.

        Args:
            Q_index (int): The index of the Q value to get the energy
                offset for.

        Returns:
            Parameter: The energy offset Parameter at the specified Q
                index.

        Raises:
            ValueError: If no Q values are set in the InstrumentModel.
            IndexError: If Q_index is out of bounds.
        """
        if self._Q is None:
            raise ValueError('No Q values are set in the InstrumentModel.')

        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]

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

    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

np.ndarray or 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 | None

The display name of the InstrumentModel. Default is "MyInstrumentModel".

'MyInstrumentModel'
unique_name str | None

The unique name of the InstrumentModel. Default is None.

None
Q ndarray | list | Variable | 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. Default is None.

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. Default is None.

None
energy_offset float | int | None

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

None
unit str | Unit

The unit of the energy axis. Default is 'meV'.

'meV'

Raises:

Type Description
TypeError

If resolution_model is not a ResolutionModel or None

TypeError

If background_model is not a BackgroundModel or None

TypeError

If energy_offset is not a number or None

UnitError

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

Source code in src/easydynamics/sample_model/instrument_model.py
 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
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',
):
    """Initialize an InstrumentModel.

    Args:
        display_name (str | None): The display name of the
            InstrumentModel. Default is "MyInstrumentModel".
        unique_name (str | None): The unique name of the
            InstrumentModel. Default is None.
        Q (np.ndarray | list | sc.Variable | None): The Q values
            where the instrument is modelled.
        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. Default is 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.
            Default is None.
        energy_offset (float | int | None): Template energy offset
            of the instrument. Will be copied to each Q value. If
            None, the energy offset will be 0. Default is None.
        unit (str | sc.Unit): The unit of the energy axis. Default
            is 'meV'.

    Raises:
        TypeError: If resolution_model is not a ResolutionModel or
            None
        TypeError: If background_model is not a BackgroundModel or
            None
        TypeError: If energy_offset is not a number or None
        UnitError: If unit is not a valid unit string or scipp Unit.
    """
    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:

Name Type Description
str str

A string representation of the InstrumentModel.

Source code in src/easydynamics/sample_model/instrument_model.py
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
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:

Name Type Description
BackgroundModel BackgroundModel

The background model of the instrument.

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
TypeError

If unit_str is not a string or scipp Unit.

ValueError

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

Source code in src/easydynamics/sample_model/instrument_model.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
def convert_unit(self, unit_str: str | sc.Unit) -> None:
    """Convert the unit of the InstrumentModel.

    Args:
        unit_str (str | sc.Unit): The unit to convert to.

    Raises:
        TypeError: If unit_str is not a string or scipp Unit.
        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:

Name Type Description
Parameter Parameter

The energy offset template parameter of the instrument model.

fix_resolution_parameters()

Fix all parameters in the resolution model.

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

free_resolution_parameters()

Free all parameters in the resolution model.

Source code in src/easydynamics/sample_model/instrument_model.py
351
352
353
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

Returns:

Type Description
list[Parameter]

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.

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.

Source code in src/easydynamics/sample_model/instrument_model.py
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
def get_all_variables(self, Q_index: int | None = None) -> list[Parameter]:
    """Get all variables in the InstrumentModel.

    Args:
        Q_index (int | None): The index of the Q value to get
            variables for. If None, get variables for all Q values.

    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.

    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 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_at_Q(Q_index)

Get the energy offset Parameter at a specific Q index.

Parameters:

Name Type Description Default
Q_index int

The index of the Q value to get the energy offset for.

required

Returns:

Name Type Description
Parameter Parameter

The energy offset Parameter at the specified Q index.

Raises:

Type Description
ValueError

If no Q values are set in the InstrumentModel.

IndexError

If Q_index is out of bounds.

Source code in src/easydynamics/sample_model/instrument_model.py
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
def get_energy_offset_at_Q(self, Q_index: int) -> Parameter:
    """Get the energy offset Parameter at a specific Q index.

    Args:
        Q_index (int): The index of the Q value to get the energy
            offset for.

    Returns:
        Parameter: The energy offset Parameter at the specified Q
            index.

    Raises:
        ValueError: If no Q values are set in the InstrumentModel.
        IndexError: If Q_index is out of bounds.
    """
    if self._Q is None:
        raise ValueError('No Q values are set in the InstrumentModel.')

    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]

resolution_model property writable

Get the resolution model of the instrument.

Returns:

Name Type Description
ResolutionModel 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: ModelBase

Base class for Sample Models.

Contains common functionality for models with components and Q dependence.

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. Defaults to "meV".

'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 ArrayLike | Variable | None

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

None

Attributes:

Name Type Description
unit str | Unit

Unit of the model.

components list[ModelComponent]

List of ModelComponents in the model.

Q ArrayLike | Variable | None

Q values of the model.

Source code in src/easydynamics/sample_model/model_base.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
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
class ModelBase(EasyScienceModelBase):
    """Base class for Sample Models.

    Contains common functionality for models with components and
    Q dependence.

    Args:
        display_name (str): Display name of the model.
        unique_name (str | None): Unique name of the model. If None, a
            unique name will be generated.
        unit (str | sc.Unit | None): Unit of the model. Defaults to
            "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.
        Q (ArrayLike | sc.Variable | None): Q values for the model. If
            None, Q is not set.

    Attributes:
        unit (str | sc.Unit): Unit of the model.
        components (list[ModelComponent]): List of ModelComponents in
            the model.
        Q (ArrayLike | sc.Variable | None): Q values of the model.
    """

    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,
    ):
        """Initialize the ModelBase.

        Args:
            display_name (str): Display name of the model.
            unique_name (str | None): Unique name of the model. If None,
                a unique name will be generated.
            unit (str | sc.Unit | None): Unit of the model. Defaults to
                "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.
            Q (ArrayLike | sc.Variable | None): Q values for the model.
                If None, Q is not set.
        """
        super().__init__(
            display_name=display_name,
            unique_name=unique_name,
        )
        self._unit = _validate_unit(unit)
        self._Q = _validate_and_convert_Q(Q)

        if components is not None and not isinstance(
            components, (ModelComponent, ComponentCollection)
        ):
            raise TypeError(
                f'Components must be a ModelComponent, a ComponentCollection or None, '
                f'got {type(components).__name__}'
            )

        self._components = ComponentCollection()
        if isinstance(components, (ModelComponent, ComponentCollection)):
            self.append_component(components)

        self._generate_component_collections()

    def evaluate(
        self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray
    ) -> list[np.ndarray]:
        """Evaluate the sample model at all Q for the given x values.

        Args:
        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.

        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.

        Raises:
            ValueError: If there are no components in the model to
                evaluate.
        """

        if not self._component_collections:
            raise ValueError(
                'No components in the model to evaluate. '
                'Run generate_component_collections() first'
            )
        y = [collection.evaluate(x) for collection in self._component_collections]

        return y

    # ------------------------------------------------------------------
    # Component management
    # ------------------------------------------------------------------
    def append_component(self, component: ModelComponent | ComponentCollection) -> None:
        """Append a ModelComponent or ComponentCollection to the
        SampleModel.

        Args:
            component (ModelComponent | ComponentCollection): The
                ModelComponent or ComponentCollection to append.
        """
        self._components.append_component(component)
        self._on_components_change()

    def remove_component(self, unique_name: str) -> None:
        """Remove a ModelComponent from the SampleModel by its unique
        name.

        Args:
            unique_name (str): The unique name of the ModelComponent
                to remove.
        """
        self._components.remove_component(unique_name)
        self._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:
        """Get the unit of the ComponentCollection.

        Returns:
            str | sc.Unit |None: The unit of the ComponentCollection.
        """

        return self._unit

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

        Args:
            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.'
            )
        )  # noqa: E501

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

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

        Raises:
            TypeError: If the provided unit is not a string or sc.Unit.
            UnitError: If the provided unit is not compatible with the
                current unit.
        """

        old_unit = self._unit

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

    @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.

        Args:
            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.

        Args:
            value (Q_type | None): The new Q values to set. If None, Q
            will be unset.
        """
        old_Q = self._Q
        new_Q = _validate_and_convert_Q(value)

        if (
            old_Q is not None
            and new_Q is not None
            and len(old_Q) == len(new_Q)
            and all(np.isclose(old_Q, new_Q))
        ):
            return  # No change in Q, so do nothing
        self._Q = new_Q
        self._on_Q_change()

    # ------------------------------------------------------------------
    # Other methods
    # ------------------------------------------------------------------
    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.

        Args:
            Q_index  (int | None): If None, get variables for all
                ComponentCollections. If int, get variables for the
                ComponentCollection at this index. Defaults to None.

        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.

        Args:
            Q_index (int): The index of the desired ComponentCollection.

        Returns:
            ComponentCollection: The ComponentCollection at the
            specified Q index.

        Raises:
            TypeError: If Q_index is not an int.
            IndexError: If Q_index is out of bounds for the number of
                ComponentCollections.
        """
        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]

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

np.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. Defaults to "meV".

'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 ArrayLike | Variable | None

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

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

    Args:
        display_name (str): Display name of the model.
        unique_name (str | None): Unique name of the model. If None,
            a unique name will be generated.
        unit (str | sc.Unit | None): Unit of the model. Defaults to
            "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.
        Q (ArrayLike | sc.Variable | None): Q values for the model.
            If None, Q is not set.
    """
    super().__init__(
        display_name=display_name,
        unique_name=unique_name,
    )
    self._unit = _validate_unit(unit)
    self._Q = _validate_and_convert_Q(Q)

    if components is not None and not isinstance(
        components, (ModelComponent, ComponentCollection)
    ):
        raise TypeError(
            f'Components must be a ModelComponent, a ComponentCollection or None, '
            f'got {type(components).__name__}'
        )

    self._components = ComponentCollection()
    if isinstance(components, (ModelComponent, ComponentCollection)):
        self.append_component(components)

    self._generate_component_collections()

__repr__()

Return a string representation of the ModelBase.

Returns:

Name Type Description
str str

A string representation of the ModelBase.

Source code in src/easydynamics/sample_model/model_base.py
367
368
369
370
371
372
373
374
375
376
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
123
124
125
126
127
128
129
130
131
132
def append_component(self, component: ModelComponent | ComponentCollection) -> None:
    """Append a ModelComponent or ComponentCollection to the
    SampleModel.

    Args:
        component (ModelComponent | ComponentCollection): The
            ModelComponent or ComponentCollection to append.
    """
    self._components.append_component(component)
    self._on_components_change()

clear_components()

Clear all ModelComponents from the SampleModel.

Source code in src/easydynamics/sample_model/model_base.py
145
146
147
148
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]

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.

UnitError

If the provided unit is not compatible with the current unit.

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

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

    Raises:
        TypeError: If the provided unit is not a string or sc.Unit.
        UnitError: If the provided unit is not compatible with the
            current unit.
    """

    old_unit = self._unit

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

evaluate(x)

Evaluate the sample model at all Q for the given x values.

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.

Returns:

Type Description
list[ndarray]

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.

Raises:

Type Description
ValueError

If there are no components in the model to evaluate.

Source code in src/easydynamics/sample_model/model_base.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def 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.

    Args:
    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.

    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.

    Raises:
        ValueError: If there are no components in the model to
            evaluate.
    """

    if not self._component_collections:
        raise ValueError(
            'No components in the model to evaluate. '
            'Run generate_component_collections() first'
        )
    y = [collection.evaluate(x) for collection in self._component_collections]

    return y

fix_all_parameters()

Fix all Parameters in all ComponentCollections.

Source code in src/easydynamics/sample_model/model_base.py
273
274
275
276
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
278
279
280
281
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. Defaults to None.

None

Returns:

Type Description
list[Parameter]

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

    Args:
        Q_index  (int | None): If None, get variables for all
            ComponentCollections. If int, get variables for the
            ComponentCollection at this index. Defaults to None.

    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

Returns:

Name Type Description
ComponentCollection ComponentCollection

The ComponentCollection at the

ComponentCollection

specified Q index.

Raises:

Type Description
TypeError

If Q_index is not an int.

IndexError

If Q_index is out of bounds for the number of ComponentCollections.

Source code in src/easydynamics/sample_model/model_base.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
def get_component_collection(self, Q_index: int) -> ComponentCollection:
    """Get the ComponentCollection at the given Q index.

    Args:
        Q_index (int): The index of the desired ComponentCollection.

    Returns:
        ComponentCollection: The ComponentCollection at the
        specified Q index.

    Raises:
        TypeError: If Q_index is not an int.
        IndexError: If Q_index is out of bounds for the number of
            ComponentCollections.
    """
    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]

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
134
135
136
137
138
139
140
141
142
143
def remove_component(self, unique_name: str) -> None:
    """Remove a ModelComponent from the SampleModel by its unique
    name.

    Args:
        unique_name (str): The unique name of the ModelComponent
            to remove.
    """
    self._components.remove_component(unique_name)
    self._on_components_change()

unit property writable

Get the unit of the ComponentCollection.

Returns:

Type Description
str | Unit

str | sc.Unit |None: The unit of the ComponentCollection.

resolution_model

ResolutionModel

Bases: ModelBase

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

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 | None

Unit of the model. Defaults to "meV".

'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

Attributes:

Name Type Description
unit str | Unit

Unit of the model.

components list[ModelComponent]

List of ModelComponents in the model.

Q ndarray | Numeric | list | ArrayLike | Variable | None

Q values of the model.

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

    Args:
        display_name (str): Display name of the model.
        unique_name (str | None): Unique name of the model. If None, a
            unique name will be generated.
        unit (str | sc.Unit | None): Unit of the model. Defaults to
            "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.
        Q (Q_type | None): Q values for the model. If None, Q is not
            set.

    Attributes:
        unit (str | sc.Unit): Unit of the model.
        components (list[ModelComponent]): List of ModelComponents in
            the model.
        Q (np.ndarray | Numeric | list | ArrayLike | sc.Variable | None):
            Q values of the model.
    """

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

        Args:
            display_name (str): Display name of the model.
            unique_name (str | None): Unique name of the model. If None,
                a unique name will be generated.
            unit (str | sc.Unit | None): Unit of the model. Defaults to
                "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.
            Q (Q_type | None): Q values for the model. If None, Q is not
                set.

        Raises:
            TypeError: If components is not a ModelComponent or
                ComponentCollection.
            ValueError: If Q is not a valid Q_type.
        """

        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.

        Args:
            component (ModelComponent | ComponentCollection):
                Component(s) to append.

        Raises:
            TypeError: If the component is a DeltaFunction or Polynomial
        """
        if isinstance(component, ComponentCollection):
            components = component.components
        else:
            components = (component,)

        for comp in components:
            if isinstance(comp, (DeltaFunction, Polynomial)):
                raise TypeError(
                    f'Component in ResolutionModel cannot be a {comp.__class__.__name__}'
                )

        super().append_component(component)

__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 | None

Unit of the model. Defaults to "meV".

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

ValueError

If Q is not a valid Q_type.

Source code in src/easydynamics/sample_model/resolution_model.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def __init__(
    self,
    display_name: str = 'MyResolutionModel',
    unique_name: str | None = None,
    unit: str | sc.Unit = 'meV',
    components: ComponentCollection | ModelComponent | None = None,
    Q: Q_type | None = None,
):
    """Initialize a ResolutionModel.

    Args:
        display_name (str): Display name of the model.
        unique_name (str | None): Unique name of the model. If None,
            a unique name will be generated.
        unit (str | sc.Unit | None): Unit of the model. Defaults to
            "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.
        Q (Q_type | None): Q values for the model. If None, Q is not
            set.

    Raises:
        TypeError: If components is not a ModelComponent or
            ComponentCollection.
        ValueError: If Q is not a valid Q_type.
    """

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

    Args:
        component (ModelComponent | ComponentCollection):
            Component(s) to append.

    Raises:
        TypeError: If the component is a DeltaFunction or Polynomial
    """
    if isinstance(component, ComponentCollection):
        components = component.components
    else:
        components = (component,)

    for comp in components:
        if isinstance(comp, (DeltaFunction, Polynomial)):
            raise TypeError(
                f'Component in ResolutionModel cannot be a {comp.__class__.__name__}'
            )

    super().append_component(component)

sample_model

SampleModel

Bases: ModelBase

SampleModel represents a model of a sample with components and diffusion models, parameterized by Q and optionally temperature. Generates ComponentCollections for each Q value, combining components from the base model and diffusion models.

Applies detailed balancing based on temperature if provided.

Parameters:

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 | None

Unit of the model. If None, defaults to "meV".

'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 (Number, list, ndarray, array | 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.

None
temperature_unit str | Unit

Unit of the temperature. Defaults to "K".

'K'
divide_by_temperature bool

Whether to divide the detailed balance factor by temperature. Defaults to True.

True

Attributes:

Name Type Description
unit str | Unit

Unit of the model.

components list[ModelComponent]

List of ModelComponents in the model.

Q ndarray | Numeric | list | ArrayLike | Variable | None

Q values of the model.

diffusion_models list[DiffusionModelBase]

List of diffusion models in the SampleModel.

temperature Parameter | None

Temperature Parameter for detailed balancing, or None if not set.

divide_by_temperature bool

Whether to divide the detailed balance factor by temperature.

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

    Args:
        display_name (str): Display name of the model.
        unique_name (str | None): Unique name of the model. If None, a
            unique name will be generated.
        unit (str | sc.Unit | None): Unit of the model. If None,
            defaults to "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.
        Q (Number, list, np.ndarray, sc.array | None):
            Q values for the model. If None, Q is not set.
        diffusion_models (DiffusionModelBase | list[DiffusionModelBase] | None):
            Diffusion models to include in the SampleModel.
            If None, no diffusion models are added.
        temperature (float | None): Temperature for detailed balancing.
            If None, no detailed balancing is applied.
        temperature_unit (str | sc.Unit): Unit of the temperature.
            Defaults to "K".
        divide_by_temperature (bool): Whether to divide the detailed
            balance factor by temperature. Defaults to True.

    Attributes:
        unit (str | sc.Unit): Unit of the model.
        components (list[ModelComponent]): List of ModelComponents in
            the model.
        Q (np.ndarray | Numeric | list | ArrayLike | sc.Variable | None):
            Q values of the model.
        diffusion_models (list[DiffusionModelBase]): List of diffusion
            models in the SampleModel.
        temperature (Parameter | None): Temperature Parameter for
            detailed balancing, or None if not set.
        divide_by_temperature (bool): Whether to divide the detailed
            balance factor by temperature.
    """

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

        Args:
            display_name (str): Display name of the model.
            unique_name (str | None): Unique name of the model. If None,
                a unique name will be generated.
            unit (str | sc.Unit | None): Unit of the model. If None,
                defaults to "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.
            Q (Number, list, np.ndarray, sc.array | None):
                Q values for the model. If None, Q is not set.
            diffusion_models (DiffusionModelBase | list[DiffusionModelBase] | None):
                Diffusion models to include in the SampleModel. If None,
                no diffusion models are added.
            temperature (float | None): Temperature for detailed
                balancing. If None, no detailed balancing is applied.
            temperature_unit (str | sc.Unit): Unit of the temperature.
                Defaults to "K".
            divide_by_temperature (bool): Whether to divide the detailed
                balance factor by temperature. Defaults to True.

        Raises:
            TypeError: If diffusion_models is not a DiffusionModelBase,
                a list of DiffusionModelBase, or None.
            TypeError: If temperature is not a number or None.
            ValueError: If temperature is negative.
            TypeError: If divide_by_temperature is not a bool.
        """
        if diffusion_models is None:
            self._diffusion_models = []
        elif isinstance(diffusion_models, DiffusionModelBase):
            self._diffusion_models = [diffusion_models]
        else:
            if not isinstance(diffusion_models, list) or not all(
                isinstance(dm, DiffusionModelBase) for dm in diffusion_models
            ):
                raise TypeError(
                    'diffusion_models must be a DiffusionModelBase, '
                    'a list of DiffusionModelBase or None'
                )
            self._diffusion_models = diffusion_models

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

        if temperature is None:
            self._temperature = None
        else:
            if not isinstance(temperature, Numeric):
                raise TypeError('temperature must be a number or None')

            if temperature < 0:
                raise ValueError('temperature must be non-negative')
            self._temperature = Parameter(
                name='Temperature',
                value=temperature,
                unit=temperature_unit,
                display_name='Temperature',
                fixed=True,
            )
        self._temperature_unit = temperature_unit

        if not isinstance(divide_by_temperature, bool):
            raise TypeError('divide_by_temperature must be True or False')
        self._divide_by_temperature = divide_by_temperature

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

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

        Args:
            diffusion_model (DiffusionModelBase): The DiffusionModel
                to append.

        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.

        Args:
            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.

        Args:
            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.
        """

        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.

        Args:
            value (Numeric | None): The temperature value to set. Can be
                a number or None to unset the temperature.
        """
        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.

        Args:
            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.'
        )  # noqa: E501

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

        Args:
            unit (str | sc.Unit): The unit to convert the temperature
                Parameter to.

        Raises:
            ValueError: If temperature is not set or conversion fails.
        """

        if self._temperature is None:
            raise ValueError('Temperature is not set, cannot convert unit.')

        old_unit = self._temperature.unit

        try:
            self._temperature.convert_unit(unit)
            self._temperature_unit = unit
        except Exception as e:
            # Attempt to rollback on failure
            try:
                self._temperature.convert_unit(old_unit)
            except Exception:  # noqa: S110
                pass  # Best effort rollback
            raise e

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

        Returns:
            bool: True if the detailed balance factor is divided by
                temperature, False otherwise.
        """
        return self._divide_by_temperature

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

        Args:
            value (bool): True to divide the detailed balance factor by
                temperature, False otherwise.
        """
        if not isinstance(value, bool):
            raise TypeError('divide_by_temperature must be True or False')
        self._divide_by_temperature = value

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

    def evaluate(
        self, x: Numeric | list | np.ndarray | sc.Variable | sc.DataArray
    ) -> list[np.ndarray]:
        """Evaluate the sample model at all Q for the given x values.

        Args:
            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:
            DBF = _detailed_balance_factor(
                energy=x,
                temperature=self._temperature,
                divide_by_temperature=self._divide_by_temperature,
                energy_unit=self._unit,
            )
            y = [yi * DBF for yi in y]

        return y

    def get_all_variables(self, 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.

        Args:
            Q_index (int | None): If specified, only get variables from
                the ComponentCollection at the given Q index. If None,
                get variables from all ComponentCollections.
        """

        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.
        """
        # TODO regenerate automatically if Q, diffusion models
        # or components have changed
        super()._generate_component_collections()

        if self._Q is None:
            return
        # Generate components from diffusion models
        # and add to component collections
        for diffusion_model in self._diffusion_models:
            diffusion_collections = diffusion_model.create_component_collections(Q=self._Q)
            for target, source in zip(self._component_collections, diffusion_collections):
                for component in source.components:
                    target.append_component(component)

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

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

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 | None

Unit of the model. If None, defaults to "meV".

'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 (Number, list, ndarray, array | 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.

None
temperature_unit str | Unit

Unit of the temperature. Defaults to "K".

'K'
divide_by_temperature bool

Whether to divide the detailed balance factor by temperature. Defaults to True.

True

Raises:

Type Description
TypeError

If diffusion_models is not a DiffusionModelBase, a list of DiffusionModelBase, or None.

TypeError

If temperature is not a number or None.

ValueError

If temperature is negative.

TypeError

If divide_by_temperature is not a bool.

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

    Args:
        display_name (str): Display name of the model.
        unique_name (str | None): Unique name of the model. If None,
            a unique name will be generated.
        unit (str | sc.Unit | None): Unit of the model. If None,
            defaults to "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.
        Q (Number, list, np.ndarray, sc.array | None):
            Q values for the model. If None, Q is not set.
        diffusion_models (DiffusionModelBase | list[DiffusionModelBase] | None):
            Diffusion models to include in the SampleModel. If None,
            no diffusion models are added.
        temperature (float | None): Temperature for detailed
            balancing. If None, no detailed balancing is applied.
        temperature_unit (str | sc.Unit): Unit of the temperature.
            Defaults to "K".
        divide_by_temperature (bool): Whether to divide the detailed
            balance factor by temperature. Defaults to True.

    Raises:
        TypeError: If diffusion_models is not a DiffusionModelBase,
            a list of DiffusionModelBase, or None.
        TypeError: If temperature is not a number or None.
        ValueError: If temperature is negative.
        TypeError: If divide_by_temperature is not a bool.
    """
    if diffusion_models is None:
        self._diffusion_models = []
    elif isinstance(diffusion_models, DiffusionModelBase):
        self._diffusion_models = [diffusion_models]
    else:
        if not isinstance(diffusion_models, list) or not all(
            isinstance(dm, DiffusionModelBase) for dm in diffusion_models
        ):
            raise TypeError(
                'diffusion_models must be a DiffusionModelBase, '
                'a list of DiffusionModelBase or None'
            )
        self._diffusion_models = diffusion_models

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

    if temperature is None:
        self._temperature = None
    else:
        if not isinstance(temperature, Numeric):
            raise TypeError('temperature must be a number or None')

        if temperature < 0:
            raise ValueError('temperature must be non-negative')
        self._temperature = Parameter(
            name='Temperature',
            value=temperature,
            unit=temperature_unit,
            display_name='Temperature',
            fixed=True,
        )
    self._temperature_unit = temperature_unit

    if not isinstance(divide_by_temperature, bool):
        raise TypeError('divide_by_temperature must be True or False')
    self._divide_by_temperature = divide_by_temperature

__repr__()

Return a string representation of the SampleModel.

Returns:

Name Type Description
str str

A string representation of the SampleModel.

Source code in src/easydynamics/sample_model/sample_model.py
441
442
443
444
445
446
447
448
449
450
451
452
453
454
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'divide_by_temperature = {self._divide_by_temperature}'
    )

append_diffusion_model(diffusion_model)

Append a DiffusionModel to the SampleModel.

Parameters:

Name Type Description Default
diffusion_model DiffusionModelBase

The DiffusionModel 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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def append_diffusion_model(self, diffusion_model: DiffusionModelBase) -> None:
    """Append a DiffusionModel to the SampleModel.

    Args:
        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
192
193
194
195
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.

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

    Args:
        unit (str | sc.Unit): The unit to convert the temperature
            Parameter to.

    Raises:
        ValueError: If temperature is not set or conversion fails.
    """

    if self._temperature is None:
        raise ValueError('Temperature is not set, cannot convert unit.')

    old_unit = self._temperature.unit

    try:
        self._temperature.convert_unit(unit)
        self._temperature_unit = unit
    except Exception as e:
        # Attempt to rollback on failure
        try:
            self._temperature.convert_unit(old_unit)
        except Exception:  # noqa: S110
            pass  # Best effort rollback
        raise e

diffusion_models property writable

Get the diffusion models of the SampleModel.

Returns:

Type Description
list[DiffusionModelBase]

list[DiffusionModelBase]: The diffusion models of the SampleModel.

divide_by_temperature property writable

Get whether to divide the detailed balance factor by temperature.

Returns:

Name Type Description
bool bool

True if the detailed balance factor is divided by temperature, False otherwise.

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[np.ndarray]: List of evaluated model values for each Q.

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

    Args:
        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:
        DBF = _detailed_balance_factor(
            energy=x,
            temperature=self._temperature,
            divide_by_temperature=self._divide_by_temperature,
            energy_unit=self._unit,
        )
        y = [yi * DBF for yi in y]

    return y

get_all_variables(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
Source code in src/easydynamics/sample_model/sample_model.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
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.

    Args:
        Q_index (int | None): If specified, only get variables from
            the ComponentCollection at the given Q index. If None,
            get variables from all ComponentCollections.
    """

    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

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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def remove_diffusion_model(self, name: 'str') -> None:
    """Remove a DiffusionModel from the SampleModel by unique name.

    Args:
        name (str): The unique name of the DiffusionModel to remove.

    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

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

str | sc.Unit: The unit of the temperature Parameter.