Skip to content

experiment

categories

background

base

BackgroundBase

Bases: CategoryCollection

Abstract base for background subcategories in experiments.

Concrete implementations provide parameterized background models and compute background intensities on the experiment grid.

Source code in src/easydiffraction/datablocks/experiment/categories/background/base.py
11
12
13
14
15
16
17
18
19
20
21
22
class BackgroundBase(CategoryCollection):
    """
    Abstract base for background subcategories in experiments.

    Concrete implementations provide parameterized background models and
    compute background intensities on the experiment grid.
    """

    # TODO: Consider moving to CategoryCollection
    @abstractmethod
    def show(self) -> None:
        """Print a human-readable view of background components."""
show() abstractmethod

Print a human-readable view of background components.

Source code in src/easydiffraction/datablocks/experiment/categories/background/base.py
20
21
22
@abstractmethod
def show(self) -> None:
    """Print a human-readable view of background components."""

chebyshev

Chebyshev polynomial background model.

Provides a collection of polynomial terms and evaluation helpers.

ChebyshevPolynomialBackground

Bases: BackgroundBase

Chebyshev polynomial background model.

Source code in src/easydiffraction/datablocks/experiment/categories/background/chebyshev.py
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
@BackgroundFactory.register
class ChebyshevPolynomialBackground(BackgroundBase):
    """Chebyshev polynomial background model."""

    type_info = TypeInfo(
        tag='chebyshev',
        description='Chebyshev polynomial background',
    )
    compatibility = Compatibility(
        beam_mode=frozenset({
            BeamModeEnum.CONSTANT_WAVELENGTH,
            BeamModeEnum.TIME_OF_FLIGHT,
        }),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY}),
    )

    def __init__(self) -> None:
        super().__init__(item_type=PolynomialTerm)

    def _update(
        self,
        *,
        called_by_minimizer: bool = False,
    ) -> None:
        """Evaluate polynomial background over x data."""
        del called_by_minimizer

        data = self._parent.data
        x = data.x

        if not self._items:
            log.warning('No background points found. Setting background to zero.')
            data._set_intensity_bkg(np.zeros_like(x))
            return

        u = (x - x.min()) / (x.max() - x.min()) * 2 - 1
        coefs = [term.coef.value for term in self._items]

        y = chebval(u, coefs)
        data._set_intensity_bkg(y)

    def show(self) -> None:
        """Print a table of polynomial orders and coefficients."""
        columns_headers: list[str] = ['Order', 'Coefficient']
        columns_alignment = ['left', 'left']
        columns_data: list[list[int | float]] = [
            [t.order.value, t.coef.value] for t in self._items
        ]

        console.paragraph('Chebyshev polynomial background terms')
        render_table(
            columns_headers=columns_headers,
            columns_alignment=columns_alignment,
            columns_data=columns_data,
        )
show()

Print a table of polynomial orders and coefficients.

Source code in src/easydiffraction/datablocks/experiment/categories/background/chebyshev.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def show(self) -> None:
    """Print a table of polynomial orders and coefficients."""
    columns_headers: list[str] = ['Order', 'Coefficient']
    columns_alignment = ['left', 'left']
    columns_data: list[list[int | float]] = [
        [t.order.value, t.coef.value] for t in self._items
    ]

    console.paragraph('Chebyshev polynomial background terms')
    render_table(
        columns_headers=columns_headers,
        columns_alignment=columns_alignment,
        columns_data=columns_data,
    )
PolynomialTerm

Bases: CategoryItem

Chebyshev polynomial term.

New public attribute names: order and coef replacing the longer chebyshev_order / chebyshev_coef. Backward-compatible aliases are kept so existing serialized data / external code does not break immediately. Tests should migrate to the short names.

Source code in src/easydiffraction/datablocks/experiment/categories/background/chebyshev.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 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
class PolynomialTerm(CategoryItem):
    """
    Chebyshev polynomial term.

    New public attribute names: ``order`` and ``coef`` replacing the
    longer ``chebyshev_order`` / ``chebyshev_coef``. Backward-compatible
    aliases are kept so existing serialized data / external code does
    not break immediately. Tests should migrate to the short names.
    """

    def __init__(self) -> None:
        super().__init__()

        self._id = StringDescriptor(
            name='id',
            description='Identifier for this background polynomial term',
            value_spec=AttributeSpec(
                default='0',
                # TODO: the following pattern is valid for dict key
                #  (keywords are not checked). CIF label is less strict.
                #  Do we need conversion between CIF and internal label?
                validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'),
            ),
            cif_handler=CifHandler(names=['_pd_background.id']),
        )
        self._order = NumericDescriptor(
            name='order',
            description='Order used in a Chebyshev polynomial background term',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_pd_background.Chebyshev_order']),
        )
        self._coef = Parameter(
            name='coef',
            description='Coefficient used in a Chebyshev polynomial background term',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_pd_background.Chebyshev_coef']),
        )

        self._identity.category_code = 'background'
        self._identity.category_entry_name = lambda: str(self._id.value)

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def id(self) -> StringDescriptor:
        """
        Identifier for this background polynomial term.

        Reading this property returns the underlying
        ``StringDescriptor`` object. Assigning to it updates the
        parameter value.
        """
        return self._id

    @id.setter
    def id(self, value: str) -> None:
        self._id.value = value

    @property
    def order(self) -> NumericDescriptor:
        """
        Order used in a Chebyshev polynomial background term.

        Reading this property returns the underlying
        ``NumericDescriptor`` object. Assigning to it updates the
        parameter value.
        """
        return self._order

    @order.setter
    def order(self, value: float) -> None:
        self._order.value = value

    @property
    def coef(self) -> Parameter:
        """
        Coefficient used in a Chebyshev polynomial background term.

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._coef

    @coef.setter
    def coef(self, value: float) -> None:
        self._coef.value = value
coef property writable

Coefficient used in a Chebyshev polynomial background term.

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

id property writable

Identifier for this background polynomial term.

Reading this property returns the underlying StringDescriptor object. Assigning to it updates the parameter value.

order property writable

Order used in a Chebyshev polynomial background term.

Reading this property returns the underlying NumericDescriptor object. Assigning to it updates the parameter value.

enums

Enumerations for background model types.

BackgroundTypeEnum

Bases: StrEnum

Supported background model types.

Source code in src/easydiffraction/datablocks/experiment/categories/background/enums.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class BackgroundTypeEnum(StrEnum):
    """Supported background model types."""

    LINE_SEGMENT = 'line-segment'
    CHEBYSHEV = 'chebyshev'

    @classmethod
    def default(cls) -> BackgroundTypeEnum:
        """Return a default background type."""
        return cls.LINE_SEGMENT

    def description(self) -> str:
        """Human-friendly description for the enum value."""
        if self is BackgroundTypeEnum.LINE_SEGMENT:
            return 'Linear interpolation between points'
        if self is BackgroundTypeEnum.CHEBYSHEV:
            return 'Chebyshev polynomial background'
        return None
default() classmethod

Return a default background type.

Source code in src/easydiffraction/datablocks/experiment/categories/background/enums.py
17
18
19
20
@classmethod
def default(cls) -> BackgroundTypeEnum:
    """Return a default background type."""
    return cls.LINE_SEGMENT
description()

Human-friendly description for the enum value.

Source code in src/easydiffraction/datablocks/experiment/categories/background/enums.py
22
23
24
25
26
27
28
def description(self) -> str:
    """Human-friendly description for the enum value."""
    if self is BackgroundTypeEnum.LINE_SEGMENT:
        return 'Linear interpolation between points'
    if self is BackgroundTypeEnum.CHEBYSHEV:
        return 'Chebyshev polynomial background'
    return None

factory

Background factory — delegates entirely to FactoryBase.

BackgroundFactory

Bases: FactoryBase

Create background collections by tag.

Source code in src/easydiffraction/datablocks/experiment/categories/background/factory.py
13
14
15
16
17
18
class BackgroundFactory(FactoryBase):
    """Create background collections by tag."""

    _default_rules: ClassVar[dict] = {
        frozenset(): BackgroundTypeEnum.LINE_SEGMENT,
    }

line_segment

Line-segment background model.

Interpolate user-specified points to form a background curve.

LineSegment

Bases: CategoryItem

Single background control point for interpolation.

Source code in src/easydiffraction/datablocks/experiment/categories/background/line_segment.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 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
class LineSegment(CategoryItem):
    """Single background control point for interpolation."""

    def __init__(self) -> None:
        super().__init__()

        self._id = StringDescriptor(
            name='id',
            description='Identifier for this background line segment',
            value_spec=AttributeSpec(
                default='0',
                # TODO: the following pattern is valid for dict key
                #  (keywords are not checked). CIF label is less strict.
                #  Do we need conversion between CIF and internal label?
                validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'),
            ),
            cif_handler=CifHandler(names=['_pd_background.id']),
        )
        self._x = NumericDescriptor(
            name='x',
            description='X-coordinates used to create many straight-line segments',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(
                names=[
                    '_pd_background.line_segment_X',
                    '_pd_background_line_segment_X',
                ]
            ),
        )
        self._y = Parameter(
            name='y',  # TODO: rename to intensity
            description='Intensity used to create many straight-line segments',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),  # TODO: rename to intensity
            cif_handler=CifHandler(
                names=[
                    '_pd_background.line_segment_intensity',
                    '_pd_background_line_segment_intensity',
                ]
            ),
        )

        self._identity.category_code = 'background'
        self._identity.category_entry_name = lambda: str(self._id.value)

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def id(self) -> StringDescriptor:
        """
        Identifier for this background line segment.

        Reading this property returns the underlying
        ``StringDescriptor`` object. Assigning to it updates the
        parameter value.
        """
        return self._id

    @id.setter
    def id(self, value: str) -> None:
        self._id.value = value

    @property
    def x(self) -> NumericDescriptor:
        """
        X-coordinates used to create many straight-line segments.

        Reading this property returns the underlying
        ``NumericDescriptor`` object. Assigning to it updates the
        parameter value.
        """
        return self._x

    @x.setter
    def x(self, value: float) -> None:
        self._x.value = value

    @property
    def y(self) -> Parameter:
        """
        Intensity used to create many straight-line segments.

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._y

    @y.setter
    def y(self, value: float) -> None:
        self._y.value = value
id property writable

Identifier for this background line segment.

Reading this property returns the underlying StringDescriptor object. Assigning to it updates the parameter value.

x property writable

X-coordinates used to create many straight-line segments.

Reading this property returns the underlying NumericDescriptor object. Assigning to it updates the parameter value.

y property writable

Intensity used to create many straight-line segments.

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

LineSegmentBackground

Bases: BackgroundBase

Linear-interpolation background between user-defined points.

Source code in src/easydiffraction/datablocks/experiment/categories/background/line_segment.py
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
@BackgroundFactory.register
class LineSegmentBackground(BackgroundBase):
    """Linear-interpolation background between user-defined points."""

    type_info = TypeInfo(
        tag='line-segment',
        description='Linear interpolation between points',
    )
    compatibility = Compatibility(
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}),
    )

    def __init__(self) -> None:
        super().__init__(item_type=LineSegment)

    def _update(
        self,
        *,
        called_by_minimizer: bool = False,
    ) -> None:
        """Interpolate background points over x data."""
        del called_by_minimizer

        data = self._parent.data
        x = data.x

        if not self._items:
            log.debug('No background points found. Setting background to zero.')
            data._set_intensity_bkg(np.zeros_like(x))
            return

        segments_x = np.array([point.x.value for point in self._items])
        segments_y = np.array([point.y.value for point in self._items])
        interp_func = interp1d(
            segments_x,
            segments_y,
            kind='linear',
            bounds_error=False,
            fill_value=(segments_y[0], segments_y[-1]),
        )

        y = interp_func(x)
        data._set_intensity_bkg(y)

    def show(self) -> None:
        """Print a table of control points (x, intensity)."""
        columns_headers: list[str] = ['X', 'Intensity']
        columns_alignment = ['left', 'left']
        columns_data: list[list[float]] = [[p.x.value, p.y.value] for p in self._items]

        console.paragraph('Line-segment background points')
        render_table(
            columns_headers=columns_headers,
            columns_alignment=columns_alignment,
            columns_data=columns_data,
        )
show()

Print a table of control points (x, intensity).

Source code in src/easydiffraction/datablocks/experiment/categories/background/line_segment.py
180
181
182
183
184
185
186
187
188
189
190
191
def show(self) -> None:
    """Print a table of control points (x, intensity)."""
    columns_headers: list[str] = ['X', 'Intensity']
    columns_alignment = ['left', 'left']
    columns_data: list[list[float]] = [[p.x.value, p.y.value] for p in self._items]

    console.paragraph('Line-segment background points')
    render_table(
        columns_headers=columns_headers,
        columns_alignment=columns_alignment,
        columns_data=columns_data,
    )

data

bragg_pd

PdCwlData

Bases: PdDataBase

Bragg powder CWL data collection.

Source code in src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
@DataFactory.register
class PdCwlData(PdDataBase):
    """Bragg powder CWL data collection."""

    # TODO: ???
    # _description: str = 'Powder diffraction data points for
    # constant-wavelength experiments.'
    type_info = TypeInfo(tag='bragg-pd', description='Bragg powder CWL data')
    compatibility = Compatibility(
        sample_form=frozenset({SampleFormEnum.POWDER}),
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}),
    )

    def __init__(self) -> None:
        super().__init__(item_type=PdCwlDataPoint)

    #################
    # Private methods
    #################

    # Should be set only once

    def _create_items_set_xcoord_and_id(self, values: object) -> None:
        """Set 2θ values."""
        # TODO: split into multiple methods

        # Create items
        self._items = [self._item_type() for _ in range(values.size)]

        # Set two-theta values
        for p, v in zip(self._items, values, strict=True):
            p.two_theta._value = v

        # Set point IDs
        self._set_point_id([str(i + 1) for i in range(values.size)])

    # Misc

    def _update(
        self,
        *,
        called_by_minimizer: bool = False,
    ) -> None:
        super()._update(called_by_minimizer=called_by_minimizer)

        experiment = self._parent
        d_spacing = twotheta_to_d(
            self.x,
            experiment.instrument.setup_wavelength.value,
        )
        self._set_d_spacing(d_spacing)

    ###################
    # Public properties
    ###################

    @property
    def two_theta(self) -> np.ndarray:
        """Get 2θ values for data points included in calculations."""
        return np.fromiter(
            (p.two_theta.value for p in self._calc_items),
            dtype=float,  # TODO: needed? DataTypes.NUMERIC?
        )

    @property
    def x(self) -> np.ndarray:
        """Alias for two_theta."""
        return self.two_theta

    @property
    def unfiltered_x(self) -> np.ndarray:
        """Get the 2θ values for all data points in this collection."""
        return np.fromiter(
            (p.two_theta.value for p in self._items),
            dtype=float,  # TODO: needed? DataTypes.NUMERIC?
        )
two_theta property

Get 2θ values for data points included in calculations.

unfiltered_x property

Get the 2θ values for all data points in this collection.

x property

Alias for two_theta.

PdCwlDataPoint

Bases: PdDataPointBaseMixin, PdCwlDataPointMixin, CategoryItem

Powder diffraction data point for CWL experiments.

Source code in src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
class PdCwlDataPoint(
    PdDataPointBaseMixin,  # TODO: rename to BasePdDataPointMixin???
    PdCwlDataPointMixin,  # TODO: rename to CwlPdDataPointMixin???
    CategoryItem,  # Must be last to ensure mixins initialized first
    # TODO: Check this. AI suggest class
    #  CwlThompsonCoxHastings(
    #     PeakBase, # From CategoryItem
    #     CwlBroadeningMixin,
    #     FcjAsymmetryMixin,
    #  ):
    #  But also says, that in fact, it is just for consistency. And both
    #  orders work.
):
    """Powder diffraction data point for CWL experiments."""

    def __init__(self) -> None:
        super().__init__()
        self._identity.category_code = 'pd_data'
        self._identity.category_entry_name = lambda: str(self.point_id.value)
PdCwlDataPointMixin

Mixin for CWL powder diffraction data points.

Source code in src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
class PdCwlDataPointMixin:
    """Mixin for CWL powder diffraction data points."""

    def __init__(self) -> None:
        super().__init__()

        self._two_theta = NumericDescriptor(
            name='two_theta',
            description='Measured 2θ diffraction angle.',
            units='deg',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0, le=180),
            ),
            cif_handler=CifHandler(
                names=[
                    '_pd_proc.2theta_scan',
                    '_pd_meas.2theta_scan',
                ]
            ),
        )

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def two_theta(self) -> NumericDescriptor:
        """
        Measured 2θ diffraction angle (deg).

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._two_theta
two_theta property

Measured 2θ diffraction angle (deg).

Reading this property returns the underlying NumericDescriptor object.

PdDataBase

Bases: CategoryCollection

Base class for powder diffraction data collections.

Source code in src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
class PdDataBase(CategoryCollection):
    """Base class for powder diffraction data collections."""

    # TODO: ???

    # Redefine update priority to ensure data updated after other
    # categories. Higher number = runs later. Default for other
    # categories, e.g., background and excluded regions are 10 by
    # default
    _update_priority = 100

    #################
    # Private methods
    #################

    # Should be set only once

    def _set_point_id(self, values: object) -> None:
        """Set point IDs."""
        for p, v in zip(self._items, values, strict=True):
            p.point_id._value = v

    def _set_intensity_meas(self, values: object) -> None:
        """Set measured intensity."""
        for p, v in zip(self._items, values, strict=True):
            p.intensity_meas._value = v

    def _set_intensity_meas_su(self, values: object) -> None:
        """Set standard uncertainty of measured intensity values."""
        for p, v in zip(self._items, values, strict=True):
            p.intensity_meas_su._value = v

    # Can be set multiple times

    def _set_d_spacing(self, values: object) -> None:
        """Set d-spacing values."""
        for p, v in zip(self._calc_items, values, strict=True):
            p.d_spacing._value = v

    def _set_intensity_calc(self, values: object) -> None:
        """Set calculated intensity."""
        for p, v in zip(self._calc_items, values, strict=True):
            p.intensity_calc._value = v

    def _set_intensity_bkg(self, values: object) -> None:
        """Set background intensity."""
        for p, v in zip(self._calc_items, values, strict=True):
            p.intensity_bkg._value = v

    def _set_calc_status(self, values: object) -> None:
        """Set refinement status."""
        for p, v in zip(self._items, values, strict=True):
            if v:
                p.calc_status._value = 'incl'
            elif not v:
                p.calc_status._value = 'excl'
            else:
                msg = f'Invalid refinement status value: {v}. Expected boolean True/False.'
                raise ValueError(msg)

    @property
    def _calc_mask(self) -> np.ndarray:
        return self.calc_status == 'incl'

    @property
    def _calc_items(self) -> list:
        """Get only the items included in calculations."""
        return [item for item, mask in zip(self._items, self._calc_mask, strict=False) if mask]

    # Misc

    def _update(
        self,
        *,
        called_by_minimizer: bool = False,
    ) -> None:
        experiment = self._parent
        experiments = experiment._parent
        project = experiments._parent
        structures = project.structures
        calculator = experiment.calculator

        initial_calc = np.zeros_like(self.x)
        calc = initial_calc

        # TODO: refactor _get_valid_linked_phases to only be responsible
        #  for returning list. Warning message should be defined here,
        #  at least some of them.
        # TODO: Adapt following the _update method in bragg_sc.py
        for linked_phase in experiment._get_valid_linked_phases(structures):
            structure_id = linked_phase._identity.category_entry_name
            structure_scale = linked_phase.scale.value
            structure = structures[structure_id]

            structure_calc = calculator.calculate_pattern(
                structure,
                experiment,
                called_by_minimizer=called_by_minimizer,
            )

            structure_scaled_calc = structure_scale * structure_calc
            calc += structure_scaled_calc

        self._set_intensity_calc(calc + self.intensity_bkg)

    ###################
    # Public properties
    ###################

    @property
    def calc_status(self) -> np.ndarray:
        """Refinement-status flags for each data point as an array."""
        return np.fromiter(
            (p.calc_status.value for p in self._items),
            dtype=object,  # TODO: needed? DataTypes.NUMERIC?
        )

    @property
    def d_spacing(self) -> np.ndarray:
        """D-spacing values for active (non-excluded) data points."""
        return np.fromiter(
            (p.d_spacing.value for p in self._calc_items),
            dtype=float,  # TODO: needed? DataTypes.NUMERIC?
        )

    @property
    def intensity_meas(self) -> np.ndarray:
        """Measured intensities for active data points."""
        return np.fromiter(
            (p.intensity_meas.value for p in self._calc_items),
            dtype=float,  # TODO: needed? DataTypes.NUMERIC?
        )

    @property
    def intensity_meas_su(self) -> np.ndarray:
        """
        Standard uncertainties of the measured intensities.

        Values smaller than 0.0001 are replaced with 1.0 to prevent
        fitting failures.
        """
        # TODO: The following is a temporary workaround to handle zero
        #  or near-zero uncertainties in the data, when dats is loaded
        #  from CIF files. This is necessary because zero uncertainties
        #  cause fitting algorithms to fail.
        #  The current implementation is inefficient.
        #  In the future, we should extend the functionality of
        #  the NumericDescriptor to automatically replace the value
        #  outside of the valid range (`validator`) with a
        #  default value (`default`), when the value is set.
        #  BraggPdExperiment._load_ascii_data_to_experiment() handles
        #  this for ASCII data, but we also need to handle CIF data and
        #  come up with a consistent approach for both data sources.
        original = np.fromiter(
            (p.intensity_meas_su.value for p in self._calc_items),
            dtype=float,  # TODO: needed? DataTypes.NUMERIC?
        )
        # Replace values smaller than _MIN_UNCERTAINTY with 1.0
        return np.where(original < _MIN_UNCERTAINTY, 1.0, original)

    @property
    def intensity_calc(self) -> np.ndarray:
        """Calculated intensities for active data points."""
        return np.fromiter(
            (p.intensity_calc.value for p in self._calc_items),
            dtype=float,  # TODO: needed? DataTypes.NUMERIC?
        )

    @property
    def intensity_bkg(self) -> np.ndarray:
        """Background intensities for active data points."""
        return np.fromiter(
            (p.intensity_bkg.value for p in self._calc_items),
            dtype=float,  # TODO: needed? DataTypes.NUMERIC?
        )
calc_status property

Refinement-status flags for each data point as an array.

d_spacing property

D-spacing values for active (non-excluded) data points.

intensity_bkg property

Background intensities for active data points.

intensity_calc property

Calculated intensities for active data points.

intensity_meas property

Measured intensities for active data points.

intensity_meas_su property

Standard uncertainties of the measured intensities.

Values smaller than 0.0001 are replaced with 1.0 to prevent fitting failures.

PdDataPointBaseMixin

Single base data point mixin for powder diffraction data.

Source code in src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 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
class PdDataPointBaseMixin:
    """Single base data point mixin for powder diffraction data."""

    def __init__(self) -> None:
        super().__init__()

        self._point_id = StringDescriptor(
            name='point_id',
            description='Identifier for this data point in the dataset',
            value_spec=AttributeSpec(
                default='0',
                # TODO: the following pattern is valid for dict key
                #  (keywords are not checked). CIF label is less strict.
                #  Do we need conversion between CIF and internal label?
                validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'),
            ),
            cif_handler=CifHandler(
                names=[
                    '_pd_data.point_id',
                ]
            ),
        )
        self._d_spacing = NumericDescriptor(
            name='d_spacing',
            description='d-spacing value corresponding to this data point',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(names=['_pd_proc.d_spacing']),
        )
        self._intensity_meas = NumericDescriptor(
            name='intensity_meas',
            description='Intensity recorded at each measurement point (angle/time)',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(
                names=[
                    '_pd_meas.intensity_total',
                    '_pd_proc.intensity_norm',
                ]
            ),
        )
        self._intensity_meas_su = NumericDescriptor(
            name='intensity_meas_su',
            description='Standard uncertainty of the measured intensity at this point',
            value_spec=AttributeSpec(
                default=1.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(
                names=[
                    '_pd_meas.intensity_total_su',
                    '_pd_proc.intensity_norm_su',
                ]
            ),
        )
        self._intensity_calc = NumericDescriptor(
            name='intensity_calc',
            description='Intensity of a computed diffractogram at this point',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(names=['_pd_calc.intensity_total']),
        )
        self._intensity_bkg = NumericDescriptor(
            name='intensity_bkg',
            description='Intensity of a computed background at this point',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(names=['_pd_calc.intensity_bkg']),
        )
        self._calc_status = StringDescriptor(
            name='calc_status',
            description='Status code of the data point in the calculation process',
            value_spec=AttributeSpec(
                default='incl',  # TODO: Make Enum
                validator=MembershipValidator(allowed=['incl', 'excl']),
            ),
            cif_handler=CifHandler(
                names=[
                    '_pd_data.refinement_status',  # TODO: rename to calc_status
                ]
            ),
        )

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def point_id(self) -> StringDescriptor:
        """
        Identifier for this data point in the dataset.

        Reading this property returns the underlying
        ``StringDescriptor`` object.
        """
        return self._point_id

    @property
    def d_spacing(self) -> NumericDescriptor:
        """
        d-spacing value corresponding to this data point.

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._d_spacing

    @property
    def intensity_meas(self) -> NumericDescriptor:
        """
        Intensity recorded at each measurement point (angle/time).

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._intensity_meas

    @property
    def intensity_meas_su(self) -> NumericDescriptor:
        """
        Standard uncertainty of the measured intensity at this point.

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._intensity_meas_su

    @property
    def intensity_calc(self) -> NumericDescriptor:
        """
        Intensity of a computed diffractogram at this point.

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._intensity_calc

    @property
    def intensity_bkg(self) -> NumericDescriptor:
        """
        Intensity of a computed background at this point.

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._intensity_bkg

    @property
    def calc_status(self) -> StringDescriptor:
        """
        Status code of the data point in the calculation process.

        Reading this property returns the underlying
        ``StringDescriptor`` object.
        """
        return self._calc_status
calc_status property

Status code of the data point in the calculation process.

Reading this property returns the underlying StringDescriptor object.

d_spacing property

d-spacing value corresponding to this data point.

Reading this property returns the underlying NumericDescriptor object.

intensity_bkg property

Intensity of a computed background at this point.

Reading this property returns the underlying NumericDescriptor object.

intensity_calc property

Intensity of a computed diffractogram at this point.

Reading this property returns the underlying NumericDescriptor object.

intensity_meas property

Intensity recorded at each measurement point (angle/time).

Reading this property returns the underlying NumericDescriptor object.

intensity_meas_su property

Standard uncertainty of the measured intensity at this point.

Reading this property returns the underlying NumericDescriptor object.

point_id property

Identifier for this data point in the dataset.

Reading this property returns the underlying StringDescriptor object.

PdTofData

Bases: PdDataBase

Bragg powder TOF data collection.

Source code in src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
@DataFactory.register
class PdTofData(PdDataBase):
    """Bragg powder TOF data collection."""

    type_info = TypeInfo(tag='bragg-pd-tof', description='Bragg powder TOF data')
    compatibility = Compatibility(
        sample_form=frozenset({SampleFormEnum.POWDER}),
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}),
    )

    def __init__(self) -> None:
        super().__init__(item_type=PdTofDataPoint)

    #################
    # Private methods
    #################

    # Should be set only once

    def _create_items_set_xcoord_and_id(self, values: object) -> None:
        """Set time-of-flight values."""
        # TODO: split into multiple methods

        # Create items
        self._items = [self._item_type() for _ in range(values.size)]

        # Set time-of-flight values
        for p, v in zip(self._items, values, strict=True):
            p.time_of_flight._value = v

        # Set point IDs
        self._set_point_id([str(i + 1) for i in range(values.size)])

    # Misc

    def _update(
        self,
        *,
        called_by_minimizer: bool = False,
    ) -> None:
        super()._update(called_by_minimizer=called_by_minimizer)

        experiment = self._parent
        d_spacing = tof_to_d(
            self.x,
            experiment.instrument.calib_d_to_tof_offset.value,
            experiment.instrument.calib_d_to_tof_linear.value,
            experiment.instrument.calib_d_to_tof_quad.value,
        )
        self._set_d_spacing(d_spacing)

    ###################
    # Public properties
    ###################

    @property
    def time_of_flight(self) -> np.ndarray:
        """Get TOF values for data points included in calculations."""
        return np.fromiter(
            (p.time_of_flight.value for p in self._calc_items),
            dtype=float,  # TODO: needed? DataTypes.NUMERIC?
        )

    @property
    def x(self) -> np.ndarray:
        """Alias for time_of_flight."""
        return self.time_of_flight

    @property
    def unfiltered_x(self) -> np.ndarray:
        """Get the TOF values for all data points in this collection."""
        return np.fromiter(
            (p.time_of_flight.value for p in self._items),
            dtype=float,  # TODO: needed? DataTypes.NUMERIC?
        )
time_of_flight property

Get TOF values for data points included in calculations.

unfiltered_x property

Get the TOF values for all data points in this collection.

x property

Alias for time_of_flight.

PdTofDataPoint

Bases: PdDataPointBaseMixin, PdTofDataPointMixin, CategoryItem

Powder diffraction data point for time-of-flight experiments.

Source code in src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py
288
289
290
291
292
293
294
295
296
297
298
class PdTofDataPoint(
    PdDataPointBaseMixin,
    PdTofDataPointMixin,
    CategoryItem,  # Must be last to ensure mixins initialized first
):
    """Powder diffraction data point for time-of-flight experiments."""

    def __init__(self) -> None:
        super().__init__()
        self._identity.category_code = 'pd_data'
        self._identity.category_entry_name = lambda: str(self.point_id.value)
PdTofDataPointMixin

Mixin for powder diffraction data points with time-of-flight.

Source code in src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py
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
class PdTofDataPointMixin:
    """Mixin for powder diffraction data points with time-of-flight."""

    def __init__(self) -> None:
        super().__init__()

        self._time_of_flight = NumericDescriptor(
            name='time_of_flight',
            description='Measured time for time-of-flight neutron measurement.',
            units='μs',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(names=['_pd_meas.time_of_flight']),
        )

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def time_of_flight(self) -> NumericDescriptor:
        """
        Measured time for time-of-flight neutron measurement (μs).

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._time_of_flight
time_of_flight property

Measured time for time-of-flight neutron measurement (μs).

Reading this property returns the underlying NumericDescriptor object.

bragg_sc

Refln

Bases: CategoryItem

Single reflection for single-crystal diffraction data.

Source code in src/easydiffraction/datablocks/experiment/categories/data/bragg_sc.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
class Refln(CategoryItem):
    """Single reflection for single-crystal diffraction data."""

    def __init__(self) -> None:
        super().__init__()

        self._id = StringDescriptor(
            name='id',
            description='Identifier of the reflection',
            value_spec=AttributeSpec(
                default='0',
                # TODO: the following pattern is valid for dict key
                #  (keywords are not checked). CIF label is less strict.
                #  Do we need conversion between CIF and internal label?
                validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'),
            ),
            cif_handler=CifHandler(names=['_refln.id']),
        )
        self._d_spacing = NumericDescriptor(
            name='d_spacing',
            description='Distance between lattice planes for this reflection',
            units='Å',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(names=['_refln.d_spacing']),
        )
        self._sin_theta_over_lambda = NumericDescriptor(
            name='sin_theta_over_lambda',
            description='The sin(θ)/λ value for this reflection',
            units='Å⁻¹',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(names=['_refln.sin_theta_over_lambda']),
        )
        self._index_h = NumericDescriptor(
            name='index_h',
            description='Miller index h of a measured reflection',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_refln.index_h']),
        )
        self._index_k = NumericDescriptor(
            name='index_k',
            description='Miller index k of a measured reflection',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_refln.index_k']),
        )
        self._index_l = NumericDescriptor(
            name='index_l',
            description='Miller index l of a measured reflection',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_refln.index_l']),
        )
        self._intensity_meas = NumericDescriptor(
            name='intensity_meas',
            description=' The intensity of the reflection derived from the measurements.',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(names=['_refln.intensity_meas']),
        )
        self._intensity_meas_su = NumericDescriptor(
            name='intensity_meas_su',
            description='Standard uncertainty of the measured intensity.',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(names=['_refln.intensity_meas_su']),
        )
        self._intensity_calc = NumericDescriptor(
            name='intensity_calc',
            description='Intensity of the reflection calculated from atom site data',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(names=['_refln.intensity_calc']),
        )
        self._wavelength = NumericDescriptor(
            name='wavelength',
            description='Mean wavelength of radiation for this reflection',
            units='Å',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(names=['_refln.wavelength']),
        )

        self._identity.category_code = 'refln'
        self._identity.category_entry_name = lambda: str(self.id.value)

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def id(self) -> StringDescriptor:
        """
        Identifier of the reflection.

        Reading this property returns the underlying
        ``StringDescriptor`` object.
        """
        return self._id

    @property
    def d_spacing(self) -> NumericDescriptor:
        """
        Distance between lattice planes for this reflection (Å).

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._d_spacing

    @property
    def sin_theta_over_lambda(self) -> NumericDescriptor:
        """
        The sin(θ)/λ value for this reflection (Å⁻¹).

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._sin_theta_over_lambda

    @property
    def index_h(self) -> NumericDescriptor:
        """
        Miller index h of a measured reflection.

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._index_h

    @property
    def index_k(self) -> NumericDescriptor:
        """
        Miller index k of a measured reflection.

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._index_k

    @property
    def index_l(self) -> NumericDescriptor:
        """
        Miller index l of a measured reflection.

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._index_l

    @property
    def intensity_meas(self) -> NumericDescriptor:
        """
        The intensity of the reflection derived from the measurements.

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._intensity_meas

    @property
    def intensity_meas_su(self) -> NumericDescriptor:
        """
        Standard uncertainty of the measured intensity.

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._intensity_meas_su

    @property
    def intensity_calc(self) -> NumericDescriptor:
        """
        Intensity of the reflection calculated from atom site data.

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._intensity_calc

    @property
    def wavelength(self) -> NumericDescriptor:
        """
        Mean wavelength of radiation for this reflection (Å).

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._wavelength
d_spacing property

Distance between lattice planes for this reflection (Å).

Reading this property returns the underlying NumericDescriptor object.

id property

Identifier of the reflection.

Reading this property returns the underlying StringDescriptor object.

index_h property

Miller index h of a measured reflection.

Reading this property returns the underlying NumericDescriptor object.

index_k property

Miller index k of a measured reflection.

Reading this property returns the underlying NumericDescriptor object.

index_l property

Miller index l of a measured reflection.

Reading this property returns the underlying NumericDescriptor object.

intensity_calc property

Intensity of the reflection calculated from atom site data.

Reading this property returns the underlying NumericDescriptor object.

intensity_meas property

The intensity of the reflection derived from the measurements.

Reading this property returns the underlying NumericDescriptor object.

intensity_meas_su property

Standard uncertainty of the measured intensity.

Reading this property returns the underlying NumericDescriptor object.

sin_theta_over_lambda property

The sin(θ)/λ value for this reflection (Å⁻¹).

Reading this property returns the underlying NumericDescriptor object.

wavelength property

Mean wavelength of radiation for this reflection (Å).

Reading this property returns the underlying NumericDescriptor object.

ReflnData

Bases: CategoryCollection

Collection of reflections for single crystal diffraction data.

Source code in src/easydiffraction/datablocks/experiment/categories/data/bragg_sc.py
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
@DataFactory.register
class ReflnData(CategoryCollection):
    """Collection of reflections for single crystal diffraction data."""

    type_info = TypeInfo(tag='bragg-sc', description='Bragg single-crystal reflection data')
    compatibility = Compatibility(
        sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}),
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY}),
    )

    _update_priority = 100

    def __init__(self) -> None:
        super().__init__(item_type=Refln)

    #################
    # Private methods
    #################

    # Should be set only once

    def _create_items_set_hkl_and_id(
        self,
        indices_h: object,
        indices_k: object,
        indices_l: object,
    ) -> None:
        """Set Miller indices."""
        # TODO: split into multiple methods

        # Create items
        self._items = [self._item_type() for _ in range(indices_h.size)]

        # Set indices
        for item, index_h, index_k, index_l in zip(
            self._items, indices_h, indices_k, indices_l, strict=True
        ):
            item.index_h._value = index_h
            item.index_k._value = index_k
            item.index_l._value = index_l

        # Set reflection IDs
        self._set_id([str(i + 1) for i in range(indices_h.size)])

    def _set_id(self, values: object) -> None:
        """Set reflection IDs."""
        for p, v in zip(self._items, values, strict=True):
            p.id._value = v

    def _set_intensity_meas(self, values: object) -> None:
        """Set measured intensity."""
        for p, v in zip(self._items, values, strict=True):
            p.intensity_meas._value = v

    def _set_intensity_meas_su(self, values: object) -> None:
        """Set standard uncertainty of measured intensity values."""
        for p, v in zip(self._items, values, strict=True):
            p.intensity_meas_su._value = v

    def _set_wavelength(self, values: object) -> None:
        """Set wavelength."""
        for p, v in zip(self._items, values, strict=True):
            p.wavelength._value = v

    # Can be set multiple times

    def _set_d_spacing(self, values: object) -> None:
        """Set d-spacing values."""
        for p, v in zip(self._items, values, strict=True):
            p.d_spacing._value = v

    def _set_sin_theta_over_lambda(self, values: object) -> None:
        """Set sin(theta)/lambda values."""
        for p, v in zip(self._items, values, strict=True):
            p.sin_theta_over_lambda._value = v

    def _set_intensity_calc(self, values: object) -> None:
        """Set calculated intensity."""
        for p, v in zip(self._items, values, strict=True):
            p.intensity_calc._value = v

    # Misc

    def _update(
        self,
        *,
        called_by_minimizer: bool = False,
    ) -> None:
        experiment = self._parent
        experiments = experiment._parent
        project = experiments._parent
        structures = project.structures
        calculator = experiment.calculator

        linked_crystal = experiment.linked_crystal
        linked_crystal_id = experiment.linked_crystal.id.value

        if linked_crystal_id not in structures.names:
            log.error(
                f"Linked crystal ID '{linked_crystal_id}' not found in "
                f'structure IDs {structures.names}.'
            )
            return

        structure_id = linked_crystal_id
        structure_scale = linked_crystal.scale.value
        structure = structures[structure_id]

        stol, raw_calc = calculator.calculate_structure_factors(
            structure,
            experiment,
            called_by_minimizer=called_by_minimizer,
        )

        d_spacing = sin_theta_over_lambda_to_d_spacing(stol)
        calc = structure_scale * raw_calc

        self._set_d_spacing(d_spacing)
        self._set_sin_theta_over_lambda(stol)
        self._set_intensity_calc(calc)

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def d_spacing(self) -> np.ndarray:
        """D-spacing values for all reflection data points."""
        return np.fromiter(
            (p.d_spacing.value for p in self._items),
            dtype=float,
        )

    @property
    def sin_theta_over_lambda(self) -> np.ndarray:
        """sinθ/λ values for all reflection data points."""
        return np.fromiter(
            (p.sin_theta_over_lambda.value for p in self._items),
            dtype=float,
        )

    @property
    def index_h(self) -> np.ndarray:
        """Miller h indices for all reflection data points."""
        return np.fromiter(
            (p.index_h.value for p in self._items),
            dtype=float,
        )

    @property
    def index_k(self) -> np.ndarray:
        """Miller k indices for all reflection data points."""
        return np.fromiter(
            (p.index_k.value for p in self._items),
            dtype=float,
        )

    @property
    def index_l(self) -> np.ndarray:
        """Miller l indices for all reflection data points."""
        return np.fromiter(
            (p.index_l.value for p in self._items),
            dtype=float,
        )

    @property
    def intensity_meas(self) -> np.ndarray:
        """Measured structure-factor intensities for all reflections."""
        return np.fromiter(
            (p.intensity_meas.value for p in self._items),
            dtype=float,
        )

    @property
    def intensity_meas_su(self) -> np.ndarray:
        """Standard uncertainties of the measured intensities."""
        return np.fromiter(
            (p.intensity_meas_su.value for p in self._items),
            dtype=float,
        )

    @property
    def intensity_calc(self) -> np.ndarray:
        """Calculated intensities for all reflections."""
        return np.fromiter(
            (p.intensity_calc.value for p in self._items),
            dtype=float,
        )

    @property
    def wavelength(self) -> np.ndarray:
        """Wavelengths associated with each reflection."""
        return np.fromiter(
            (p.wavelength.value for p in self._items),
            dtype=float,
        )
d_spacing property

D-spacing values for all reflection data points.

index_h property

Miller h indices for all reflection data points.

index_k property

Miller k indices for all reflection data points.

index_l property

Miller l indices for all reflection data points.

intensity_calc property

Calculated intensities for all reflections.

intensity_meas property

Measured structure-factor intensities for all reflections.

intensity_meas_su property

Standard uncertainties of the measured intensities.

sin_theta_over_lambda property

sinθ/λ values for all reflection data points.

wavelength property

Wavelengths associated with each reflection.

factory

Data collection factory — delegates to FactoryBase.

DataFactory

Bases: FactoryBase

Factory for creating diffraction data collections.

Source code in src/easydiffraction/datablocks/experiment/categories/data/factory.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class DataFactory(FactoryBase):
    """Factory for creating diffraction data collections."""

    _default_rules: ClassVar[dict] = {
        frozenset({
            ('sample_form', SampleFormEnum.POWDER),
            ('scattering_type', ScatteringTypeEnum.BRAGG),
            ('beam_mode', BeamModeEnum.CONSTANT_WAVELENGTH),
        }): 'bragg-pd',
        frozenset({
            ('sample_form', SampleFormEnum.POWDER),
            ('scattering_type', ScatteringTypeEnum.BRAGG),
            ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT),
        }): 'bragg-pd-tof',
        frozenset({
            ('sample_form', SampleFormEnum.POWDER),
            ('scattering_type', ScatteringTypeEnum.TOTAL),
        }): 'total-pd',
        frozenset({
            ('sample_form', SampleFormEnum.SINGLE_CRYSTAL),
            ('scattering_type', ScatteringTypeEnum.BRAGG),
        }): 'bragg-sc',
    }

total_pd

Data categories for total scattering (PDF) experiments.

TotalData

Bases: TotalDataBase

Total scattering (PDF) data collection in r-space.

Note: Works for both CWL and TOF measurements as PDF data is always transformed to r-space.

Source code in src/easydiffraction/datablocks/experiment/categories/data/total_pd.py
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
@DataFactory.register
class TotalData(TotalDataBase):
    """
    Total scattering (PDF) data collection in r-space.

    Note: Works for both CWL and TOF measurements as PDF data is always
    transformed to r-space.
    """

    type_info = TypeInfo(
        tag='total-pd',
        description='Total scattering (PDF) data',
    )
    compatibility = Compatibility(
        sample_form=frozenset({SampleFormEnum.POWDER}),
        scattering_type=frozenset({ScatteringTypeEnum.TOTAL}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.PDFFIT}),
    )

    def __init__(self) -> None:
        super().__init__(item_type=TotalDataPoint)

    #################
    # Private methods
    #################

    # Should be set only once

    def _create_items_set_xcoord_and_id(self, values: object) -> None:
        """Set r values."""
        # TODO: split into multiple methods

        # Create items
        self._items = [self._item_type() for _ in range(values.size)]

        # Set r values
        for p, v in zip(self._items, values, strict=True):
            p.r._value = v

        # Set point IDs
        self._set_point_id([str(i + 1) for i in range(values.size)])

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def x(self) -> np.ndarray:
        """Get the r values for data points included in calculations."""
        return np.fromiter(
            (p.r.value for p in self._calc_items),
            dtype=float,  # TODO: needed? DataTypes.NUMERIC?
        )

    @property
    def unfiltered_x(self) -> np.ndarray:
        """Get the r values for all data points."""
        return np.fromiter(
            (p.r.value for p in self._items),
            dtype=float,  # TODO: needed? DataTypes.NUMERIC?
        )
unfiltered_x property

Get the r values for all data points.

x property

Get the r values for data points included in calculations.

TotalDataBase

Bases: CategoryCollection

Base class for total scattering data collections.

Source code in src/easydiffraction/datablocks/experiment/categories/data/total_pd.py
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
class TotalDataBase(CategoryCollection):
    """Base class for total scattering data collections."""

    _update_priority = 100

    #################
    # Private methods
    #################

    # Should be set only once

    def _set_point_id(self, values: object) -> None:
        """Set point IDs."""
        for p, v in zip(self._items, values, strict=True):
            p.point_id._value = v

    def _set_g_r_meas(self, values: object) -> None:
        """Set measured G(r)."""
        for p, v in zip(self._items, values, strict=True):
            p.g_r_meas._value = v

    def _set_g_r_meas_su(self, values: object) -> None:
        """Set standard uncertainty of measured G(r) values."""
        for p, v in zip(self._items, values, strict=True):
            p.g_r_meas_su._value = v

    # Can be set multiple times

    def _set_g_r_calc(self, values: object) -> None:
        """Set calculated G(r)."""
        for p, v in zip(self._calc_items, values, strict=True):
            p.g_r_calc._value = v

    def _set_calc_status(self, values: object) -> None:
        """Set calculation status."""
        for p, v in zip(self._items, values, strict=True):
            if v:
                p.calc_status._value = 'incl'
            elif not v:
                p.calc_status._value = 'excl'
            else:
                msg = f'Invalid calculation status value: {v}. Expected boolean True/False.'
                raise ValueError(msg)

    @property
    def _calc_mask(self) -> np.ndarray:
        return self.calc_status == 'incl'

    @property
    def _calc_items(self) -> list:
        """Get only the items included in calculations."""
        return [item for item, mask in zip(self._items, self._calc_mask, strict=False) if mask]

    # Misc

    def _update(
        self,
        *,
        called_by_minimizer: bool = False,
    ) -> None:
        experiment = self._parent
        experiments = experiment._parent
        project = experiments._parent
        structures = project.structures
        calculator = experiment.calculator

        initial_calc = np.zeros_like(self.x)
        calc = initial_calc

        # TODO: refactor _get_valid_linked_phases to only be responsible
        #  for returning list. Warning message should be defined here,
        #  at least some of them.
        # TODO: Adapt following the _update method in bragg_sc.py
        for linked_phase in experiment._get_valid_linked_phases(structures):
            structure_id = linked_phase._identity.category_entry_name
            structure_scale = linked_phase.scale.value
            structure = structures[structure_id]

            structure_calc = calculator.calculate_pattern(
                structure,
                experiment,
                called_by_minimizer=called_by_minimizer,
            )

            structure_scaled_calc = structure_scale * structure_calc
            calc += structure_scaled_calc

        self._set_g_r_calc(calc)

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def calc_status(self) -> np.ndarray:
        """Refinement-status flags for each data point as an array."""
        return np.fromiter(
            (p.calc_status.value for p in self._items),
            dtype=object,
        )

    @property
    def intensity_meas(self) -> np.ndarray:
        """Measured G(r) values for active data points."""
        return np.fromiter(
            (p.g_r_meas.value for p in self._calc_items),
            dtype=float,
        )

    @property
    def intensity_meas_su(self) -> np.ndarray:
        """Standard uncertainties of the measured G(r) values."""
        return np.fromiter(
            (p.g_r_meas_su.value for p in self._calc_items),
            dtype=float,
        )

    @property
    def intensity_calc(self) -> np.ndarray:
        """Calculated G(r) values for active data points."""
        return np.fromiter(
            (p.g_r_calc.value for p in self._calc_items),
            dtype=float,
        )

    @property
    def intensity_bkg(self) -> np.ndarray:
        """Background is always zero for PDF data."""
        return np.zeros_like(self.intensity_calc)
calc_status property

Refinement-status flags for each data point as an array.

intensity_bkg property

Background is always zero for PDF data.

intensity_calc property

Calculated G(r) values for active data points.

intensity_meas property

Measured G(r) values for active data points.

intensity_meas_su property

Standard uncertainties of the measured G(r) values.

TotalDataPoint

Bases: CategoryItem

Total scattering (PDF) data point in r-space (real space).

Note: PDF data is always in r-space regardless of whether the original measurement was CWL or TOF.

Source code in src/easydiffraction/datablocks/experiment/categories/data/total_pd.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
class TotalDataPoint(CategoryItem):
    """
    Total scattering (PDF) data point in r-space (real space).

    Note: PDF data is always in r-space regardless of whether the
    original measurement was CWL or TOF.
    """

    def __init__(self) -> None:
        super().__init__()

        self._point_id = StringDescriptor(
            name='point_id',
            description='Identifier for this data point in the dataset',
            value_spec=AttributeSpec(
                default='0',
                validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'),
            ),
            cif_handler=CifHandler(
                names=[
                    '_pd_data.point_id',  # TODO: Use total scattering CIF names
                ]
            ),
        )
        self._r = NumericDescriptor(
            name='r',
            description='Interatomic distance in real space',
            units='Å',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(
                names=[
                    '_pd_proc.r',  # TODO: Use PDF-specific CIF names
                ]
            ),
        )
        self._g_r_meas = NumericDescriptor(
            name='g_r_meas',
            description='Measured pair distribution function G(r)',
            value_spec=AttributeSpec(
                default=0.0,
            ),
            cif_handler=CifHandler(
                names=[
                    '_pd_meas.intensity_total',  # TODO: Use PDF-specific CIF names
                ]
            ),
        )
        self._g_r_meas_su = NumericDescriptor(
            name='g_r_meas_su',
            description='Standard uncertainty of measured G(r)',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(
                names=[
                    '_pd_meas.intensity_total_su',  # TODO: Use PDF-specific CIF names
                ]
            ),
        )
        self._g_r_calc = NumericDescriptor(
            name='g_r_calc',
            description='Calculated pair distribution function G(r)',
            value_spec=AttributeSpec(
                default=0.0,
            ),
            cif_handler=CifHandler(
                names=[
                    '_pd_calc.intensity_total',  # TODO: Use PDF-specific CIF names
                ]
            ),
        )
        self._calc_status = StringDescriptor(
            name='calc_status',
            description='Status code of the data point in calculation',
            value_spec=AttributeSpec(
                default='incl',
                validator=MembershipValidator(allowed=['incl', 'excl']),
            ),
            cif_handler=CifHandler(
                names=[
                    '_pd_data.refinement_status',  # TODO: Use PDF-specific CIF names
                ]
            ),
        )

        self._identity.category_code = 'total_data'
        self._identity.category_entry_name = lambda: str(self.point_id.value)

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def point_id(self) -> StringDescriptor:
        """
        Identifier for this data point in the dataset.

        Reading this property returns the underlying
        ``StringDescriptor`` object.
        """
        return self._point_id

    @property
    def r(self) -> NumericDescriptor:
        """
        Interatomic distance in real space (Å).

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._r

    @property
    def g_r_meas(self) -> NumericDescriptor:
        """
        Measured pair distribution function G(r).

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._g_r_meas

    @property
    def g_r_meas_su(self) -> NumericDescriptor:
        """
        Standard uncertainty of measured G(r).

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._g_r_meas_su

    @property
    def g_r_calc(self) -> NumericDescriptor:
        """
        Calculated pair distribution function G(r).

        Reading this property returns the underlying
        ``NumericDescriptor`` object.
        """
        return self._g_r_calc

    @property
    def calc_status(self) -> StringDescriptor:
        """
        Status code of the data point in calculation.

        Reading this property returns the underlying
        ``StringDescriptor`` object.
        """
        return self._calc_status
calc_status property

Status code of the data point in calculation.

Reading this property returns the underlying StringDescriptor object.

g_r_calc property

Calculated pair distribution function G(r).

Reading this property returns the underlying NumericDescriptor object.

g_r_meas property

Measured pair distribution function G(r).

Reading this property returns the underlying NumericDescriptor object.

g_r_meas_su property

Standard uncertainty of measured G(r).

Reading this property returns the underlying NumericDescriptor object.

point_id property

Identifier for this data point in the dataset.

Reading this property returns the underlying StringDescriptor object.

r property

Interatomic distance in real space (Å).

Reading this property returns the underlying NumericDescriptor object.

diffrn

default

Default diffraction ambient-conditions category.

DefaultDiffrn

Bases: CategoryItem

Ambient conditions recorded during diffraction measurement.

Source code in src/easydiffraction/datablocks/experiment/categories/diffrn/default.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
@DiffrnFactory.register
class DefaultDiffrn(CategoryItem):
    """Ambient conditions recorded during diffraction measurement."""

    type_info = TypeInfo(
        tag='default',
        description='Diffraction ambient conditions',
    )

    def __init__(self) -> None:
        super().__init__()

        self._ambient_temperature = NumericDescriptor(
            name='ambient_temperature',
            description='Mean temperature during measurement',
            units='K',
            value_spec=AttributeSpec(
                default=None,
                allow_none=True,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_diffrn.ambient_temperature']),
        )

        self._ambient_pressure = NumericDescriptor(
            name='ambient_pressure',
            description='Mean hydrostatic pressure during measurement',
            units='kPa',
            value_spec=AttributeSpec(
                default=None,
                allow_none=True,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_diffrn.ambient_pressure']),
        )

        self._ambient_magnetic_field = NumericDescriptor(
            name='ambient_magnetic_field',
            description='Mean magnetic field during measurement',
            units='T',
            value_spec=AttributeSpec(
                default=None,
                allow_none=True,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_diffrn.ambient_magnetic_field']),
        )

        self._ambient_electric_field = NumericDescriptor(
            name='ambient_electric_field',
            description='Mean electric field during measurement',
            units='V/m',
            value_spec=AttributeSpec(
                default=None,
                allow_none=True,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_diffrn.ambient_electric_field']),
        )

        self._identity.category_code = 'diffrn'

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def ambient_temperature(self) -> NumericDescriptor:
        """
        Mean temperature during measurement (K).

        Reading this property returns the underlying
        ``NumericDescriptor`` object. Assigning to it updates the value.
        """
        return self._ambient_temperature

    @ambient_temperature.setter
    def ambient_temperature(self, value: float) -> None:
        self._ambient_temperature.value = value

    @property
    def ambient_pressure(self) -> NumericDescriptor:
        """
        Mean hydrostatic pressure during measurement (kPa).

        Reading this property returns the underlying
        ``NumericDescriptor`` object. Assigning to it updates the value.
        """
        return self._ambient_pressure

    @ambient_pressure.setter
    def ambient_pressure(self, value: float) -> None:
        self._ambient_pressure.value = value

    @property
    def ambient_magnetic_field(self) -> NumericDescriptor:
        """
        Mean magnetic field during measurement (T).

        Reading this property returns the underlying
        ``NumericDescriptor`` object. Assigning to it updates the value.
        """
        return self._ambient_magnetic_field

    @ambient_magnetic_field.setter
    def ambient_magnetic_field(self, value: float) -> None:
        self._ambient_magnetic_field.value = value

    @property
    def ambient_electric_field(self) -> NumericDescriptor:
        """
        Mean electric field during measurement (V/m).

        Reading this property returns the underlying
        ``NumericDescriptor`` object. Assigning to it updates the value.
        """
        return self._ambient_electric_field

    @ambient_electric_field.setter
    def ambient_electric_field(self, value: float) -> None:
        self._ambient_electric_field.value = value
ambient_electric_field property writable

Mean electric field during measurement (V/m).

Reading this property returns the underlying NumericDescriptor object. Assigning to it updates the value.

ambient_magnetic_field property writable

Mean magnetic field during measurement (T).

Reading this property returns the underlying NumericDescriptor object. Assigning to it updates the value.

ambient_pressure property writable

Mean hydrostatic pressure during measurement (kPa).

Reading this property returns the underlying NumericDescriptor object. Assigning to it updates the value.

ambient_temperature property writable

Mean temperature during measurement (K).

Reading this property returns the underlying NumericDescriptor object. Assigning to it updates the value.

factory

Factory for diffraction ambient-conditions categories.

DiffrnFactory

Bases: FactoryBase

Create diffraction ambient-conditions category instances.

Source code in src/easydiffraction/datablocks/experiment/categories/diffrn/factory.py
12
13
14
15
16
17
class DiffrnFactory(FactoryBase):
    """Create diffraction ambient-conditions category instances."""

    _default_rules: ClassVar[dict] = {
        frozenset(): 'default',
    }

excluded_regions

default

Exclude ranges of x from fitting/plotting (masked regions).

ExcludedRegion

Bases: CategoryItem

Closed interval [start, end] to be excluded.

Source code in src/easydiffraction/datablocks/experiment/categories/excluded_regions/default.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
class ExcludedRegion(CategoryItem):
    """Closed interval [start, end] to be excluded."""

    def __init__(self) -> None:
        super().__init__()

        # TODO: Add point_id as for the background
        self._id = StringDescriptor(
            name='id',
            description='Identifier for this excluded region',
            value_spec=AttributeSpec(
                default='0',
                # TODO: the following pattern is valid for dict key
                #  (keywords are not checked). CIF label is less strict.
                #  Do we need conversion between CIF and internal label?
                validator=RegexValidator(pattern=r'^[A-Za-z0-9_]*$'),
            ),
            cif_handler=CifHandler(names=['_excluded_region.id']),
        )
        self._start = NumericDescriptor(
            name='start',
            description='Start of the excluded region.',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_excluded_region.start']),
        )
        self._end = NumericDescriptor(
            name='end',
            description='End of the excluded region.',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_excluded_region.end']),
        )
        self._identity.category_code = 'excluded_regions'
        self._identity.category_entry_name = lambda: str(self._id.value)

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def id(self) -> StringDescriptor:
        """
        Identifier for this excluded region.

        Reading this property returns the underlying
        ``StringDescriptor`` object. Assigning to it updates the
        parameter value.
        """
        return self._id

    @id.setter
    def id(self, value: str) -> None:
        self._id.value = value

    @property
    def start(self) -> NumericDescriptor:
        """
        Start of the excluded region.

        Reading this property returns the underlying
        ``NumericDescriptor`` object. Assigning to it updates the
        parameter value.
        """
        return self._start

    @start.setter
    def start(self, value: float) -> None:
        self._start.value = value

    @property
    def end(self) -> NumericDescriptor:
        """
        End of the excluded region.

        Reading this property returns the underlying
        ``NumericDescriptor`` object. Assigning to it updates the
        parameter value.
        """
        return self._end

    @end.setter
    def end(self, value: float) -> None:
        self._end.value = value
end property writable

End of the excluded region.

Reading this property returns the underlying NumericDescriptor object. Assigning to it updates the parameter value.

id property writable

Identifier for this excluded region.

Reading this property returns the underlying StringDescriptor object. Assigning to it updates the parameter value.

start property writable

Start of the excluded region.

Reading this property returns the underlying NumericDescriptor object. Assigning to it updates the parameter value.

ExcludedRegions

Bases: CategoryCollection

Collection of ExcludedRegion instances.

Excluded regions define closed intervals [start, end] on the x-axis that are to be excluded from calculations and, as a result, from fitting and plotting.

Source code in src/easydiffraction/datablocks/experiment/categories/excluded_regions/default.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
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
@ExcludedRegionsFactory.register
class ExcludedRegions(CategoryCollection):
    """
    Collection of ExcludedRegion instances.

    Excluded regions define closed intervals [start, end] on the x-axis
    that are to be excluded from calculations and, as a result, from
    fitting and plotting.
    """

    type_info = TypeInfo(
        tag='default',
        description='Excluded x-axis regions for fitting and plotting',
    )
    compatibility = Compatibility(
        sample_form=frozenset({SampleFormEnum.POWDER}),
    )

    def __init__(self) -> None:
        super().__init__(item_type=ExcludedRegion)

    def _update(
        self,
        *,
        called_by_minimizer: bool = False,
    ) -> None:
        del called_by_minimizer

        data = self._parent.data
        x = data.unfiltered_x

        # Start with a mask of all False (nothing excluded yet)
        combined_mask = np.full_like(x, fill_value=False, dtype=bool)

        # Combine masks for all excluded regions
        for region in self.values():
            start = region.start.value
            end = region.end.value
            region_mask = (x >= start) & (x <= end)
            combined_mask |= region_mask

        # Invert mask, as refinement status is opposite of excluded
        inverted_mask = ~combined_mask

        # Set refinement status in the data object
        data._set_calc_status(inverted_mask)

    def show(self) -> None:
        """Print a table of excluded [start, end] intervals."""
        # TODO: Consider moving this to the base class
        #  to avoid code duplication with implementations in Background,
        #  etc. Consider using parameter names as column headers
        columns_headers: list[str] = ['start', 'end']
        columns_alignment = ['left', 'left']
        columns_data: list[list[float]] = [[r.start.value, r.end.value] for r in self._items]

        console.paragraph('Excluded regions')
        render_table(
            columns_headers=columns_headers,
            columns_alignment=columns_alignment,
            columns_data=columns_data,
        )
show()

Print a table of excluded [start, end] intervals.

Source code in src/easydiffraction/datablocks/experiment/categories/excluded_regions/default.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
def show(self) -> None:
    """Print a table of excluded [start, end] intervals."""
    # TODO: Consider moving this to the base class
    #  to avoid code duplication with implementations in Background,
    #  etc. Consider using parameter names as column headers
    columns_headers: list[str] = ['start', 'end']
    columns_alignment = ['left', 'left']
    columns_data: list[list[float]] = [[r.start.value, r.end.value] for r in self._items]

    console.paragraph('Excluded regions')
    render_table(
        columns_headers=columns_headers,
        columns_alignment=columns_alignment,
        columns_data=columns_data,
    )

factory

Excluded-regions factory — delegates entirely to FactoryBase.

ExcludedRegionsFactory

Bases: FactoryBase

Create excluded-regions collections by tag.

Source code in src/easydiffraction/datablocks/experiment/categories/excluded_regions/factory.py
14
15
16
17
18
19
class ExcludedRegionsFactory(FactoryBase):
    """Create excluded-regions collections by tag."""

    _default_rules: ClassVar[dict] = {
        frozenset(): 'default',
    }

experiment_type

default

Experiment type descriptor (form, beam, probe, scattering).

This lightweight container stores the categorical attributes defining an experiment configuration and handles CIF serialization via CifHandler.

ExperimentType

Bases: CategoryItem

Container of attributes defining the experiment type.

Source code in src/easydiffraction/datablocks/experiment/categories/experiment_type/default.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
@ExperimentTypeFactory.register
class ExperimentType(CategoryItem):
    """Container of attributes defining the experiment type."""

    type_info = TypeInfo(
        tag='default',
        description='Experiment type descriptor',
    )

    def __init__(self) -> None:
        super().__init__()

        self._sample_form = StringDescriptor(
            name='sample_form',
            description='Powder diffraction or single crystal diffraction',
            value_spec=AttributeSpec(
                default=SampleFormEnum.default().value,
                validator=MembershipValidator(allowed=[member.value for member in SampleFormEnum]),
            ),
            cif_handler=CifHandler(names=['_expt_type.sample_form']),
        )

        self._beam_mode = StringDescriptor(
            name='beam_mode',
            description='Constant wavelength (CW) or time-of-flight (TOF) measurement',
            value_spec=AttributeSpec(
                default=BeamModeEnum.default().value,
                validator=MembershipValidator(allowed=[member.value for member in BeamModeEnum]),
            ),
            cif_handler=CifHandler(names=['_expt_type.beam_mode']),
        )
        self._radiation_probe = StringDescriptor(
            name='radiation_probe',
            description='Neutron or X-ray diffraction measurement',
            value_spec=AttributeSpec(
                default=RadiationProbeEnum.default().value,
                validator=MembershipValidator(
                    allowed=[member.value for member in RadiationProbeEnum]
                ),
            ),
            cif_handler=CifHandler(names=['_expt_type.radiation_probe']),
        )
        self._scattering_type = StringDescriptor(
            name='scattering_type',
            description='Conventional Bragg diffraction or total scattering (PDF)',
            value_spec=AttributeSpec(
                default=ScatteringTypeEnum.default().value,
                validator=MembershipValidator(
                    allowed=[member.value for member in ScatteringTypeEnum]
                ),
            ),
            cif_handler=CifHandler(names=['_expt_type.scattering_type']),
        )

        self._identity.category_code = 'expt_type'

    # ------------------------------------------------------------------
    #  Private setters (used by factories and loaders only)
    # ------------------------------------------------------------------

    def _set_sample_form(self, value: str) -> None:
        self._sample_form.value = value

    def _set_beam_mode(self, value: str) -> None:
        self._beam_mode.value = value

    def _set_radiation_probe(self, value: str) -> None:
        self._radiation_probe.value = value

    def _set_scattering_type(self, value: str) -> None:
        self._scattering_type.value = value

    # ------------------------------------------------------------------
    #  Public read-only properties
    # ------------------------------------------------------------------

    @property
    def sample_form(self) -> StringDescriptor:
        """
        Powder diffraction or single crystal diffraction.

        Reading this property returns the underlying
        ``StringDescriptor`` object.
        """
        return self._sample_form

    @property
    def beam_mode(self) -> StringDescriptor:
        """
        Constant wavelength (CW) or time-of-flight (TOF) measurement.

        Reading this property returns the underlying
        ``StringDescriptor`` object.
        """
        return self._beam_mode

    @property
    def radiation_probe(self) -> StringDescriptor:
        """
        Neutron or X-ray diffraction measurement.

        Reading this property returns the underlying
        ``StringDescriptor`` object.
        """
        return self._radiation_probe

    @property
    def scattering_type(self) -> StringDescriptor:
        """
        Conventional Bragg diffraction or total scattering (PDF).

        Reading this property returns the underlying
        ``StringDescriptor`` object.
        """
        return self._scattering_type
beam_mode property

Constant wavelength (CW) or time-of-flight (TOF) measurement.

Reading this property returns the underlying StringDescriptor object.

radiation_probe property

Neutron or X-ray diffraction measurement.

Reading this property returns the underlying StringDescriptor object.

sample_form property

Powder diffraction or single crystal diffraction.

Reading this property returns the underlying StringDescriptor object.

scattering_type property

Conventional Bragg diffraction or total scattering (PDF).

Reading this property returns the underlying StringDescriptor object.

factory

Experiment-type factory — delegates entirely to FactoryBase.

ExperimentTypeFactory

Bases: FactoryBase

Create experiment-type descriptors by tag.

Source code in src/easydiffraction/datablocks/experiment/categories/experiment_type/factory.py
12
13
14
15
16
17
class ExperimentTypeFactory(FactoryBase):
    """Create experiment-type descriptors by tag."""

    _default_rules: ClassVar[dict] = {
        frozenset(): 'default',
    }

extinction

becker_coppens

Becker-Coppens isotropic extinction correction for single crystals.

BeckerCoppensExtinction

Bases: CategoryItem

Becker-Coppens spherical extinction correction for single crystals.

Combines primary and secondary extinction into a single correction factor y = y_p * y_s, following the Becker-Coppens formalism. The mosaicity distribution for the secondary extinction can be either Gaussian ('gauss') or Lorentzian ('lorentz').

Parameters are the crystal radius (in μm) and the mosaicity (in arc-minutes, as expected by CrysPy).

Source code in src/easydiffraction/datablocks/experiment/categories/extinction/becker_coppens.py
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
@ExtinctionFactory.register
class BeckerCoppensExtinction(CategoryItem):
    """
    Becker-Coppens spherical extinction correction for single crystals.

    Combines primary and secondary extinction into a single correction
    factor ``y = y_p * y_s``, following the Becker-Coppens formalism.
    The mosaicity distribution for the secondary extinction can be
    either Gaussian (``'gauss'``) or Lorentzian (``'lorentz'``).

    Parameters are the crystal ``radius`` (in μm) and the ``mosaicity``
    (in arc-minutes, as expected by CrysPy).
    """

    type_info = TypeInfo(
        tag='becker-coppens',
        description='Becker-Coppens isotropic extinction correction',
    )
    compatibility = Compatibility(
        sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY}),
    )

    def __init__(self) -> None:
        super().__init__()

        self._model = StringDescriptor(
            name='model',
            description='Mosaicity distribution model (gauss or lorentz)',
            value_spec=AttributeSpec(
                default=ExtinctionModelEnum.default().value,
                validator=MembershipValidator(
                    allowed=[member.value for member in ExtinctionModelEnum],
                ),
            ),
            cif_handler=CifHandler(names=['_extinction.model']),
        )

        self._mosaicity = Parameter(
            name='mosaicity',
            description='Mosaicity of the crystal',
            units='arcmin',
            value_spec=AttributeSpec(
                default=1.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_extinction.mosaicity']),
        )
        self._radius = Parameter(
            name='radius',
            description='Mean radius of the crystal',
            units='μm',
            value_spec=AttributeSpec(
                default=1.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_extinction.radius']),
        )

        self._identity.category_code = 'extinction'

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def model(self) -> StringDescriptor:
        """
        Mosaicity distribution model (``'gauss'`` or ``'lorentz'``).

        Reading this property returns the underlying
        ``StringDescriptor`` object. Assigning to it updates the
        descriptor value.
        """
        return self._model

    @model.setter
    def model(self, value: str) -> None:
        self._model.value = value

    @property
    def mosaicity(self) -> Parameter:
        """
        Mosaicity of the crystal (arcmin).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._mosaicity

    @mosaicity.setter
    def mosaicity(self, value: float) -> None:
        self._mosaicity.value = value

    @property
    def radius(self) -> Parameter:
        """
        Mean radius of the crystal (μm).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._radius

    @radius.setter
    def radius(self, value: float) -> None:
        self._radius.value = value
model property writable

Mosaicity distribution model ('gauss' or 'lorentz').

Reading this property returns the underlying StringDescriptor object. Assigning to it updates the descriptor value.

mosaicity property writable

Mosaicity of the crystal (arcmin).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

radius property writable

Mean radius of the crystal (μm).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

factory

Extinction factory — delegates entirely to FactoryBase.

ExtinctionFactory

Bases: FactoryBase

Create extinction correction models by tag.

Source code in src/easydiffraction/datablocks/experiment/categories/extinction/factory.py
12
13
14
15
16
17
class ExtinctionFactory(FactoryBase):
    """Create extinction correction models by tag."""

    _default_rules: ClassVar[dict] = {
        frozenset(): 'becker-coppens',
    }

instrument

base

Instrument category base definitions for CWL/TOF instruments.

This module provides the shared parent used by concrete instrument implementations under the instrument category.

InstrumentBase

Bases: CategoryItem

Base class for instrument category items.

This class sets the common category_code and is used as a base for concrete CWL/TOF instrument definitions.

Source code in src/easydiffraction/datablocks/experiment/categories/instrument/base.py
15
16
17
18
19
20
21
22
23
24
25
26
class InstrumentBase(CategoryItem):
    """
    Base class for instrument category items.

    This class sets the common ``category_code`` and is used as a base
    for concrete CWL/TOF instrument definitions.
    """

    def __init__(self) -> None:
        """Initialize instrument base and set category code."""
        super().__init__()
        self._identity.category_code = 'instrument'
__init__()

Initialize instrument base and set category code.

Source code in src/easydiffraction/datablocks/experiment/categories/instrument/base.py
23
24
25
26
def __init__(self) -> None:
    """Initialize instrument base and set category code."""
    super().__init__()
    self._identity.category_code = 'instrument'

cwl

CwlInstrumentBase

Bases: InstrumentBase

Base class for constant-wavelength instruments.

Source code in src/easydiffraction/datablocks/experiment/categories/instrument/cwl.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
class CwlInstrumentBase(InstrumentBase):
    """Base class for constant-wavelength instruments."""

    def __init__(self) -> None:
        super().__init__()

        self._setup_wavelength: Parameter = Parameter(
            name='wavelength',
            description='Incident neutron or X-ray wavelength',
            units='Å',
            value_spec=AttributeSpec(
                default=1.5406,
                validator=RangeValidator(ge=0.0),
            ),
            cif_handler=CifHandler(
                names=[
                    '_instr.wavelength',
                ]
            ),
        )

    @property
    def setup_wavelength(self) -> Parameter:
        """
        Incident neutron or X-ray wavelength (Å).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._setup_wavelength

    @setup_wavelength.setter
    def setup_wavelength(self, value: float) -> None:
        self._setup_wavelength.value = value
setup_wavelength property writable

Incident neutron or X-ray wavelength (Å).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

CwlPdInstrument

Bases: CwlInstrumentBase

CW powder diffractometer.

Source code in src/easydiffraction/datablocks/experiment/categories/instrument/cwl.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
@InstrumentFactory.register
class CwlPdInstrument(CwlInstrumentBase):
    """CW powder diffractometer."""

    type_info = TypeInfo(
        tag='cwl-pd',
        description='CW powder diffractometer',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG, ScatteringTypeEnum.TOTAL}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}),
        sample_form=frozenset({SampleFormEnum.POWDER}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({
            CalculatorEnum.CRYSPY,
            CalculatorEnum.CRYSFML,
            CalculatorEnum.PDFFIT,
        }),
    )

    def __init__(self) -> None:
        super().__init__()

        self._calib_twotheta_offset: Parameter = Parameter(
            name='twotheta_offset',
            description='Instrument misalignment offset',
            units='deg',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(
                names=[
                    '_instr.2theta_offset',
                ]
            ),
        )

    @property
    def calib_twotheta_offset(self) -> Parameter:
        """
        Instrument misalignment offset (deg).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._calib_twotheta_offset

    @calib_twotheta_offset.setter
    def calib_twotheta_offset(self, value: float) -> None:
        self._calib_twotheta_offset.value = value
calib_twotheta_offset property writable

Instrument misalignment offset (deg).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

CwlScInstrument

Bases: CwlInstrumentBase

CW single-crystal diffractometer.

Source code in src/easydiffraction/datablocks/experiment/categories/instrument/cwl.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@InstrumentFactory.register
class CwlScInstrument(CwlInstrumentBase):
    """CW single-crystal diffractometer."""

    type_info = TypeInfo(
        tag='cwl-sc',
        description='CW single-crystal diffractometer',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}),
        sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY}),
    )

    def __init__(self) -> None:
        super().__init__()

factory

Instrument factory — delegates to FactoryBase.

InstrumentFactory

Bases: FactoryBase

Create instrument instances for supported modes.

Source code in src/easydiffraction/datablocks/experiment/categories/instrument/factory.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class InstrumentFactory(FactoryBase):
    """Create instrument instances for supported modes."""

    _default_rules: ClassVar[dict] = {
        frozenset({
            ('beam_mode', BeamModeEnum.CONSTANT_WAVELENGTH),
            ('sample_form', SampleFormEnum.POWDER),
        }): 'cwl-pd',
        frozenset({
            ('beam_mode', BeamModeEnum.CONSTANT_WAVELENGTH),
            ('sample_form', SampleFormEnum.SINGLE_CRYSTAL),
        }): 'cwl-sc',
        frozenset({
            ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT),
            ('sample_form', SampleFormEnum.POWDER),
        }): 'tof-pd',
        frozenset({
            ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT),
            ('sample_form', SampleFormEnum.SINGLE_CRYSTAL),
        }): 'tof-sc',
    }

tof

TofPdInstrument

Bases: InstrumentBase

TOF powder diffractometer.

Source code in src/easydiffraction/datablocks/experiment/categories/instrument/tof.py
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 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
@InstrumentFactory.register
class TofPdInstrument(InstrumentBase):
    """TOF powder diffractometer."""

    type_info = TypeInfo(
        tag='tof-pd',
        description='TOF powder diffractometer',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}),
        sample_form=frozenset({SampleFormEnum.POWDER}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}),
    )

    def __init__(self) -> None:
        super().__init__()

        self._setup_twotheta_bank: Parameter = Parameter(
            name='twotheta_bank',
            description='Detector bank position',
            units='deg',
            value_spec=AttributeSpec(
                default=150.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_instr.2theta_bank']),
        )
        self._calib_d_to_tof_offset: Parameter = Parameter(
            name='d_to_tof_offset',
            description='TOF offset',
            units='μs',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_instr.d_to_tof_offset']),
        )
        self._calib_d_to_tof_linear: Parameter = Parameter(
            name='d_to_tof_linear',
            description='TOF linear conversion',
            units='μs/Å',
            value_spec=AttributeSpec(
                default=10000.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_instr.d_to_tof_linear']),
        )
        self._calib_d_to_tof_quad: Parameter = Parameter(
            name='d_to_tof_quad',
            description='TOF quadratic correction',
            units='μs/Ų',
            value_spec=AttributeSpec(
                default=-0.00001,  # TODO: Fix CrysPy to accept 0
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_instr.d_to_tof_quad']),
        )
        self._calib_d_to_tof_recip: Parameter = Parameter(
            name='d_to_tof_recip',
            description='TOF reciprocal velocity correction',
            units='μs·Å',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_instr.d_to_tof_recip']),
        )

    @property
    def setup_twotheta_bank(self) -> Parameter:
        """
        Detector bank position (deg).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._setup_twotheta_bank

    @setup_twotheta_bank.setter
    def setup_twotheta_bank(self, value: float) -> None:
        self._setup_twotheta_bank.value = value

    @property
    def calib_d_to_tof_offset(self) -> Parameter:
        """
        TOF offset (μs).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._calib_d_to_tof_offset

    @calib_d_to_tof_offset.setter
    def calib_d_to_tof_offset(self, value: float) -> None:
        self._calib_d_to_tof_offset.value = value

    @property
    def calib_d_to_tof_linear(self) -> Parameter:
        """
        TOF linear conversion (μs/Å).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._calib_d_to_tof_linear

    @calib_d_to_tof_linear.setter
    def calib_d_to_tof_linear(self, value: float) -> None:
        self._calib_d_to_tof_linear.value = value

    @property
    def calib_d_to_tof_quad(self) -> Parameter:
        """
        TOF quadratic correction (μs/Ų).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._calib_d_to_tof_quad

    @calib_d_to_tof_quad.setter
    def calib_d_to_tof_quad(self, value: float) -> None:
        self._calib_d_to_tof_quad.value = value

    @property
    def calib_d_to_tof_recip(self) -> Parameter:
        """
        TOF reciprocal velocity correction (μs·Å).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._calib_d_to_tof_recip

    @calib_d_to_tof_recip.setter
    def calib_d_to_tof_recip(self, value: float) -> None:
        self._calib_d_to_tof_recip.value = value
calib_d_to_tof_linear property writable

TOF linear conversion (μs/Å).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

calib_d_to_tof_offset property writable

TOF offset (μs).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

calib_d_to_tof_quad property writable

TOF quadratic correction (μs/Ų).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

calib_d_to_tof_recip property writable

TOF reciprocal velocity correction (μs·Å).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

setup_twotheta_bank property writable

Detector bank position (deg).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

TofScInstrument

Bases: InstrumentBase

TOF single-crystal diffractometer.

Source code in src/easydiffraction/datablocks/experiment/categories/instrument/tof.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@InstrumentFactory.register
class TofScInstrument(InstrumentBase):
    """TOF single-crystal diffractometer."""

    type_info = TypeInfo(
        tag='tof-sc',
        description='TOF single-crystal diffractometer',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}),
        sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY}),
    )

    def __init__(self) -> None:
        super().__init__()

linked_crystal

default

Default linked-crystal reference (id + scale).

LinkedCrystal

Bases: CategoryItem

Linked crystal reference for single-crystal diffraction.

Source code in src/easydiffraction/datablocks/experiment/categories/linked_crystal/default.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@LinkedCrystalFactory.register
class LinkedCrystal(CategoryItem):
    """Linked crystal reference for single-crystal diffraction."""

    type_info = TypeInfo(
        tag='default',
        description='Crystal reference with id and scale factor',
    )
    compatibility = Compatibility(
        sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}),
    )

    def __init__(self) -> None:
        super().__init__()

        self._id = StringDescriptor(
            name='id',
            description='Identifier of the linked crystal',
            value_spec=AttributeSpec(
                default='Si',
                validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'),
            ),
            cif_handler=CifHandler(names=['_sc_crystal_block.id']),
        )
        self._scale = Parameter(
            name='scale',
            description='Scale factor of the linked crystal',
            value_spec=AttributeSpec(
                default=1.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_sc_crystal_block.scale']),
        )

        self._identity.category_code = 'linked_crystal'

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def id(self) -> StringDescriptor:
        """
        Identifier of the linked crystal.

        Reading this property returns the underlying
        ``StringDescriptor`` object. Assigning to it updates the
        parameter value.
        """
        return self._id

    @id.setter
    def id(self, value: str) -> None:
        self._id.value = value

    @property
    def scale(self) -> Parameter:
        """
        Scale factor of the linked crystal.

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._scale

    @scale.setter
    def scale(self, value: float) -> None:
        self._scale.value = value
id property writable

Identifier of the linked crystal.

Reading this property returns the underlying StringDescriptor object. Assigning to it updates the parameter value.

scale property writable

Scale factor of the linked crystal.

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

factory

Linked-crystal factory — delegates entirely to FactoryBase.

LinkedCrystalFactory

Bases: FactoryBase

Create linked-crystal references by tag.

Source code in src/easydiffraction/datablocks/experiment/categories/linked_crystal/factory.py
12
13
14
15
16
17
class LinkedCrystalFactory(FactoryBase):
    """Create linked-crystal references by tag."""

    _default_rules: ClassVar[dict] = {
        frozenset(): 'default',
    }

linked_phases

default

Linked phases allow combining phases with scale factors.

LinkedPhase

Bases: CategoryItem

Link to a phase by id with a scale factor.

Source code in src/easydiffraction/datablocks/experiment/categories/linked_phases/default.py
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
class LinkedPhase(CategoryItem):
    """Link to a phase by id with a scale factor."""

    def __init__(self) -> None:
        super().__init__()

        self._id = StringDescriptor(
            name='id',
            description='Identifier of the linked phase',
            value_spec=AttributeSpec(
                default='Si',
                validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'),
            ),
            cif_handler=CifHandler(names=['_pd_phase_block.id']),
        )
        self._scale = Parameter(
            name='scale',
            description='Scale factor of the linked phase.',
            value_spec=AttributeSpec(
                default=1.0,
                validator=RangeValidator(ge=0.0),
            ),
            cif_handler=CifHandler(names=['_pd_phase_block.scale']),
        )

        self._identity.category_code = 'linked_phases'
        self._identity.category_entry_name = lambda: str(self.id.value)

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def id(self) -> StringDescriptor:
        """
        Identifier of the linked phase.

        Reading this property returns the underlying
        ``StringDescriptor`` object. Assigning to it updates the
        parameter value.
        """
        return self._id

    @id.setter
    def id(self, value: str) -> None:
        self._id.value = value

    @property
    def scale(self) -> Parameter:
        """
        Scale factor of the linked phase.

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._scale

    @scale.setter
    def scale(self, value: float) -> None:
        self._scale.value = value
id property writable

Identifier of the linked phase.

Reading this property returns the underlying StringDescriptor object. Assigning to it updates the parameter value.

scale property writable

Scale factor of the linked phase.

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

LinkedPhases

Bases: CategoryCollection

Collection of LinkedPhase instances.

Source code in src/easydiffraction/datablocks/experiment/categories/linked_phases/default.py
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
@LinkedPhasesFactory.register
class LinkedPhases(CategoryCollection):
    """Collection of LinkedPhase instances."""

    type_info = TypeInfo(
        tag='default',
        description='Phase references with scale factors',
    )
    compatibility = Compatibility(
        sample_form=frozenset({SampleFormEnum.POWDER}),
    )

    def __init__(self) -> None:
        """Create an empty collection of linked phases."""
        super().__init__(item_type=LinkedPhase)
__init__()

Create an empty collection of linked phases.

Source code in src/easydiffraction/datablocks/experiment/categories/linked_phases/default.py
97
98
99
def __init__(self) -> None:
    """Create an empty collection of linked phases."""
    super().__init__(item_type=LinkedPhase)

factory

Linked-phases factory — delegates entirely to FactoryBase.

LinkedPhasesFactory

Bases: FactoryBase

Create linked-phases collections by tag.

Source code in src/easydiffraction/datablocks/experiment/categories/linked_phases/factory.py
12
13
14
15
16
17
class LinkedPhasesFactory(FactoryBase):
    """Create linked-phases collections by tag."""

    _default_rules: ClassVar[dict] = {
        frozenset(): 'default',
    }

peak

base

Base class for peak profile categories.

PeakBase

Bases: CategoryItem

Base class for peak profile categories.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/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
class PeakBase(CategoryItem):
    """Base class for peak profile categories."""

    def __init__(self) -> None:
        super().__init__()
        self._identity.category_code = 'peak'

        type_info = getattr(type(self), 'type_info', None)
        default_tag = type_info.tag if type_info is not None else ''
        self._profile_type: StringDescriptor = StringDescriptor(
            name='profile_type',
            description='Active peak profile type tag',
            value_spec=AttributeSpec(default=default_tag),
            cif_handler=CifHandler(names=['_peak.profile_type']),
        )

    @property
    def profile_type(self) -> StringDescriptor:
        """
        CIF identifier for the active peak profile type.

        Returns
        -------
        StringDescriptor
            The descriptor holding the profile type tag string.
        """
        return self._profile_type
profile_type property

CIF identifier for the active peak profile type.

Returns:

Type Description
StringDescriptor

The descriptor holding the profile type tag string.

cwl

Constant-wavelength peak profile classes.

CwlPseudoVoigt

Bases: PeakBase, CwlBroadeningMixin

Constant-wavelength pseudo-Voigt peak shape.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/cwl.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@PeakFactory.register
class CwlPseudoVoigt(
    PeakBase,
    CwlBroadeningMixin,
):
    """Constant-wavelength pseudo-Voigt peak shape."""

    type_info = TypeInfo(
        tag='pseudo-voigt',
        description='Pseudo-Voigt profile',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}),
    )

    def __init__(self) -> None:
        super().__init__()
CwlPseudoVoigtEmpiricalAsymmetry

Bases: PeakBase, CwlBroadeningMixin, EmpiricalAsymmetryMixin

Pseudo-Voigt with empirical asymmetry correction for CWL mode.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/cwl.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@PeakFactory.register
class CwlPseudoVoigtEmpiricalAsymmetry(
    PeakBase,
    CwlBroadeningMixin,
    EmpiricalAsymmetryMixin,
):
    """Pseudo-Voigt with empirical asymmetry correction for CWL mode."""

    type_info = TypeInfo(
        tag='pseudo-voigt + empirical asymmetry',
        description='Pseudo-Voigt with empirical asymmetry correction',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY}),
    )

    def __init__(self) -> None:
        super().__init__()
CwlThompsonCoxHastings

Bases: PeakBase, CwlBroadeningMixin, FcjAsymmetryMixin

Thompson-Cox-Hastings with FCJ asymmetry for CWL mode.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/cwl.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@PeakFactory.register
class CwlThompsonCoxHastings(
    PeakBase,
    CwlBroadeningMixin,
    FcjAsymmetryMixin,
):
    """Thompson-Cox-Hastings with FCJ asymmetry for CWL mode."""

    type_info = TypeInfo(
        tag='thompson-cox-hastings',
        description='Thompson-Cox-Hastings with FCJ asymmetry correction',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSFML}),
    )

    def __init__(self) -> None:
        super().__init__()

cwl_mixins

Constant-wavelength (CWL) peak-profile component classes.

This module provides classes that add broadening and asymmetry parameters. They are composed into concrete peak classes elsewhere via multiple inheritance.

CwlBroadeningMixin

CWL Gaussian and Lorentz broadening parameters.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/cwl_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
class CwlBroadeningMixin:
    """CWL Gaussian and Lorentz broadening parameters."""

    def __init__(self) -> None:
        super().__init__()

        self._broad_gauss_u: Parameter = Parameter(
            name='broad_gauss_u',
            description='Gaussian broadening from sample size and resolution',
            units='deg²',
            value_spec=AttributeSpec(
                default=0.01,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.broad_gauss_u']),
        )
        self._broad_gauss_v: Parameter = Parameter(
            name='broad_gauss_v',
            description='Gaussian broadening instrumental contribution',
            units='deg²',
            value_spec=AttributeSpec(
                default=-0.01,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.broad_gauss_v']),
        )
        self._broad_gauss_w: Parameter = Parameter(
            name='broad_gauss_w',
            description='Gaussian broadening instrumental contribution',
            units='deg²',
            value_spec=AttributeSpec(
                default=0.02,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.broad_gauss_w']),
        )
        self._broad_lorentz_x: Parameter = Parameter(
            name='broad_lorentz_x',
            description='Lorentzian broadening from sample strain effects',
            units='deg',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.broad_lorentz_x']),
        )
        self._broad_lorentz_y: Parameter = Parameter(
            name='broad_lorentz_y',
            description='Lorentzian broadening from microstructural defects',
            units='deg',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.broad_lorentz_y']),
        )

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def broad_gauss_u(self) -> Parameter:
        """
        Gaussian broadening from sample size and resolution (deg²).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._broad_gauss_u

    @broad_gauss_u.setter
    def broad_gauss_u(self, value: float) -> None:
        self._broad_gauss_u.value = value

    @property
    def broad_gauss_v(self) -> Parameter:
        """
        Gaussian broadening instrumental contribution (deg²).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._broad_gauss_v

    @broad_gauss_v.setter
    def broad_gauss_v(self, value: float) -> None:
        self._broad_gauss_v.value = value

    @property
    def broad_gauss_w(self) -> Parameter:
        """
        Gaussian broadening instrumental contribution (deg²).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._broad_gauss_w

    @broad_gauss_w.setter
    def broad_gauss_w(self, value: float) -> None:
        self._broad_gauss_w.value = value

    @property
    def broad_lorentz_x(self) -> Parameter:
        """
        Lorentzian broadening (sample strain effects) (deg).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._broad_lorentz_x

    @broad_lorentz_x.setter
    def broad_lorentz_x(self, value: float) -> None:
        self._broad_lorentz_x.value = value

    @property
    def broad_lorentz_y(self) -> Parameter:
        """
        Lorentzian broadening from microstructural defects (deg).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._broad_lorentz_y

    @broad_lorentz_y.setter
    def broad_lorentz_y(self, value: float) -> None:
        self._broad_lorentz_y.value = value
broad_gauss_u property writable

Gaussian broadening from sample size and resolution (deg²).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

broad_gauss_v property writable

Gaussian broadening instrumental contribution (deg²).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

broad_gauss_w property writable

Gaussian broadening instrumental contribution (deg²).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

broad_lorentz_x property writable

Lorentzian broadening (sample strain effects) (deg).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

broad_lorentz_y property writable

Lorentzian broadening from microstructural defects (deg).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

EmpiricalAsymmetryMixin

Empirical CWL peak asymmetry parameters.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/cwl_mixins.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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
class EmpiricalAsymmetryMixin:
    """Empirical CWL peak asymmetry parameters."""

    def __init__(self) -> None:
        super().__init__()

        self._asym_empir_1: Parameter = Parameter(
            name='asym_empir_1',
            description='Empirical asymmetry coefficient p1',
            units='',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.asym_empir_1']),
        )
        self._asym_empir_2: Parameter = Parameter(
            name='asym_empir_2',
            description='Empirical asymmetry coefficient p2',
            units='',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.asym_empir_2']),
        )
        self._asym_empir_3: Parameter = Parameter(
            name='asym_empir_3',
            description='Empirical asymmetry coefficient p3',
            units='',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.asym_empir_3']),
        )
        self._asym_empir_4: Parameter = Parameter(
            name='asym_empir_4',
            description='Empirical asymmetry coefficient p4',
            units='',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.asym_empir_4']),
        )

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def asym_empir_1(self) -> Parameter:
        """
        Empirical asymmetry coefficient p1.

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._asym_empir_1

    @asym_empir_1.setter
    def asym_empir_1(self, value: float) -> None:
        self._asym_empir_1.value = value

    @property
    def asym_empir_2(self) -> Parameter:
        """
        Empirical asymmetry coefficient p2.

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._asym_empir_2

    @asym_empir_2.setter
    def asym_empir_2(self, value: float) -> None:
        self._asym_empir_2.value = value

    @property
    def asym_empir_3(self) -> Parameter:
        """
        Empirical asymmetry coefficient p3.

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._asym_empir_3

    @asym_empir_3.setter
    def asym_empir_3(self, value: float) -> None:
        self._asym_empir_3.value = value

    @property
    def asym_empir_4(self) -> Parameter:
        """
        Empirical asymmetry coefficient p4.

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._asym_empir_4

    @asym_empir_4.setter
    def asym_empir_4(self, value: float) -> None:
        self._asym_empir_4.value = value
asym_empir_1 property writable

Empirical asymmetry coefficient p1.

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

asym_empir_2 property writable

Empirical asymmetry coefficient p2.

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

asym_empir_3 property writable

Empirical asymmetry coefficient p3.

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

asym_empir_4 property writable

Empirical asymmetry coefficient p4.

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

FcjAsymmetryMixin

Finger-Cox-Jephcoat (FCJ) asymmetry parameters.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/cwl_mixins.py
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
class FcjAsymmetryMixin:
    """Finger-Cox-Jephcoat (FCJ) asymmetry parameters."""

    def __init__(self) -> None:
        super().__init__()

        self._asym_fcj_1: Parameter = Parameter(
            name='asym_fcj_1',
            description='Finger-Cox-Jephcoat asymmetry parameter 1',
            units='',
            value_spec=AttributeSpec(
                default=0.01,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.asym_fcj_1']),
        )
        self._asym_fcj_2: Parameter = Parameter(
            name='asym_fcj_2',
            description='Finger-Cox-Jephcoat asymmetry parameter 2',
            units='',
            value_spec=AttributeSpec(
                default=0.02,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.asym_fcj_2']),
        )

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def asym_fcj_1(self) -> Parameter:
        """
        Finger-Cox-Jephcoat asymmetry parameter 1.

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._asym_fcj_1

    @asym_fcj_1.setter
    def asym_fcj_1(self, value: float) -> None:
        self._asym_fcj_1.value = value

    @property
    def asym_fcj_2(self) -> Parameter:
        """
        Finger-Cox-Jephcoat asymmetry parameter 2.

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._asym_fcj_2

    @asym_fcj_2.setter
    def asym_fcj_2(self, value: float) -> None:
        self._asym_fcj_2.value = value
asym_fcj_1 property writable

Finger-Cox-Jephcoat asymmetry parameter 1.

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

asym_fcj_2 property writable

Finger-Cox-Jephcoat asymmetry parameter 2.

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

factory

Peak profile factory — delegates to FactoryBase.

PeakFactory

Bases: FactoryBase

Factory for creating peak profile objects.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/factory.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class PeakFactory(FactoryBase):
    """Factory for creating peak profile objects."""

    _default_rules: ClassVar[dict] = {
        frozenset({
            ('scattering_type', ScatteringTypeEnum.BRAGG),
            ('beam_mode', BeamModeEnum.CONSTANT_WAVELENGTH),
        }): PeakProfileTypeEnum.PSEUDO_VOIGT,
        frozenset({
            ('scattering_type', ScatteringTypeEnum.BRAGG),
            ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT),
        }): PeakProfileTypeEnum.JORGENSEN,
        frozenset({
            ('scattering_type', ScatteringTypeEnum.TOTAL),
        }): PeakProfileTypeEnum.GAUSSIAN_DAMPED_SINC,
    }

tof

Time-of-flight peak profile classes.

Jorgensen: BBE ⊗ Gaussian (CrysPy peak_shape="Gauss"). Jorgensen-Von Dreele: BBE ⊗ pseudo-Voigt (CrysPy peak_shape="pseudo-Voigt"). Double-Jorgensen-Von Dreele: double BBE ⊗ pseudo-Voigt (CrysPy peak_shape="type0m", Z-Rietveld).

TofDoubleJorgensenVonDreele

Bases: PeakBase, TofGaussianBroadeningMixin, TofLorentzianBroadeningMixin, TofDoubleExponentialMixin

Double-Jorgensen-Von Dreele TOF profile: double BBE ⊗ pV.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof.py
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
@PeakFactory.register
class TofDoubleJorgensenVonDreele(
    PeakBase,
    TofGaussianBroadeningMixin,
    TofLorentzianBroadeningMixin,
    TofDoubleExponentialMixin,
):
    """Double-Jorgensen-Von Dreele TOF profile: double BBE ⊗ pV."""

    type_info = TypeInfo(
        tag='double-jorgensen-von-dreele',
        description='Double-exp ⊗ pseudo-Voigt profile (Z-Rietveld type0m)',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY}),
    )

    def __init__(self) -> None:
        super().__init__()
TofJorgensen

Bases: PeakBase, TofGaussianBroadeningMixin, TofBackToBackExponentialMixin

Jorgensen TOF profile: back-to-back exponentials ⊗ Gaussian.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@PeakFactory.register
class TofJorgensen(
    PeakBase,
    TofGaussianBroadeningMixin,
    TofBackToBackExponentialMixin,
):
    """Jorgensen TOF profile: back-to-back exponentials ⊗ Gaussian."""

    type_info = TypeInfo(
        tag='jorgensen',
        description='Jorgensen BBE ⊗ Gaussian profile',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}),
    )

    def __init__(self) -> None:
        super().__init__()
TofJorgensenVonDreele

Bases: PeakBase, TofGaussianBroadeningMixin, TofLorentzianBroadeningMixin, TofBackToBackExponentialMixin

Jorgensen-Von Dreele TOF profile: BBE ⊗ pseudo-Voigt.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
@PeakFactory.register
class TofJorgensenVonDreele(
    PeakBase,
    TofGaussianBroadeningMixin,
    TofLorentzianBroadeningMixin,
    TofBackToBackExponentialMixin,
):
    """Jorgensen-Von Dreele TOF profile: BBE ⊗ pseudo-Voigt."""

    type_info = TypeInfo(
        tag='jorgensen-von-dreele',
        description='Jorgensen-Von Dreele BBE ⊗ pseudo-Voigt profile',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}),
    )

    def __init__(self) -> None:
        super().__init__()

tof_mixins

Time-of-flight (TOF) peak-profile mixin classes.

Defines parameter mixins for TOF peak shapes based on the Jorgensen back-to-back exponential (BBE) formalism:

  • TofGaussianBroadeningMixin — σ₀, σ₁, σ₂
  • TofLorentzianBroadeningMixin — γ₀, γ₁, γ₂
  • TofBackToBackExponentialMixin — α₀, α₁ (rise), β₀, β₁ (decay)
  • TofDoubleExponentialMixin — α₁, α₂ (rise), β₀₀, β₀₁, β₁₀ (decay), r₀₁, r₀₂, r₀₃ (switching function) for double-BBE (Z-Rietveld type0m)

These are composed into concrete peak classes in tof.py.

TofBackToBackExponentialMixin

Back-to-back exponential (BBE) rise and decay parameters.

Rise parameters α₀, α₁ and decay parameters β₀, β₁ follow Von Dreele, Jorgensen & Windsor, J. Appl. Cryst. 15, 581 (1982).

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.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
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
class TofBackToBackExponentialMixin:
    """
    Back-to-back exponential (BBE) rise and decay parameters.

    Rise parameters α₀, α₁ and decay parameters β₀, β₁ follow Von
    Dreele, Jorgensen & Windsor, J. Appl. Cryst. 15, 581 (1982).
    """

    def __init__(self) -> None:
        super().__init__()

        self._exp_rise_alpha_0 = Parameter(
            name='rise_alpha_0',
            description='Back-to-back exponential rise α₀',
            units='μs',
            value_spec=AttributeSpec(
                default=0.01,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.rise_alpha_0']),
        )
        self._exp_rise_alpha_1 = Parameter(
            name='rise_alpha_1',
            description='Back-to-back exponential rise α₁',
            units='μs/Å',
            value_spec=AttributeSpec(
                default=0.02,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.rise_alpha_1']),
        )
        self._exp_decay_beta_0 = Parameter(
            name='decay_beta_0',
            description='Back-to-back exponential decay β₀',
            units='μs',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.decay_beta_0']),
        )
        self._exp_decay_beta_1 = Parameter(
            name='decay_beta_1',
            description='Back-to-back exponential decay β₁',
            units='μs/Å',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.decay_beta_1']),
        )

    @property
    def exp_rise_alpha_0(self) -> Parameter:
        """
        Back-to-back exponential rise α₀ (μs).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._exp_rise_alpha_0

    @exp_rise_alpha_0.setter
    def exp_rise_alpha_0(self, value: float) -> None:
        self._exp_rise_alpha_0.value = value

    @property
    def exp_rise_alpha_1(self) -> Parameter:
        """
        Back-to-back exponential rise α₁ (μs/Å).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._exp_rise_alpha_1

    @exp_rise_alpha_1.setter
    def exp_rise_alpha_1(self, value: float) -> None:
        self._exp_rise_alpha_1.value = value

    @property
    def exp_decay_beta_0(self) -> Parameter:
        """
        Back-to-back exponential decay β₀ (μs).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._exp_decay_beta_0

    @exp_decay_beta_0.setter
    def exp_decay_beta_0(self, value: float) -> None:
        self._exp_decay_beta_0.value = value

    @property
    def exp_decay_beta_1(self) -> Parameter:
        """
        Back-to-back exponential decay β₁ (μs/Å).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._exp_decay_beta_1

    @exp_decay_beta_1.setter
    def exp_decay_beta_1(self, value: float) -> None:
        self._exp_decay_beta_1.value = value
exp_decay_beta_0 property writable

Back-to-back exponential decay β₀ (μs).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

exp_decay_beta_1 property writable

Back-to-back exponential decay β₁ (μs/Å).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

exp_rise_alpha_0 property writable

Back-to-back exponential rise α₀ (μs).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

exp_rise_alpha_1 property writable

Back-to-back exponential rise α₁ (μs/Å).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

TofDoubleExponentialMixin

Double back-to-back exponential parameters for Z-Rietveld type0m.

Rise parameters α₁, α₂, decay parameters β₀₀, β₀₁, β₁₀ for two exponential regimes, and switching-function parameters r₀₁, r₀₂, r₀₃.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.py
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
class TofDoubleExponentialMixin:
    """
    Double back-to-back exponential parameters for Z-Rietveld type0m.

    Rise parameters α₁, α₂, decay parameters β₀₀, β₀₁, β₁₀ for two
    exponential regimes, and switching-function parameters r₀₁, r₀₂,
    r₀₃.
    """

    def __init__(self) -> None:
        super().__init__()

        self._dexp_rise_alpha_1 = Parameter(
            name='dexp_rise_alpha_1',
            description='Double-exp rise parameter α₁',
            units='μs',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.dexp_rise_alpha_1']),
        )
        self._dexp_rise_alpha_2 = Parameter(
            name='dexp_rise_alpha_2',
            description='Double-exp rise parameter α₂',
            units='μs/Å',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.dexp_rise_alpha_2']),
        )
        self._dexp_decay_beta_00 = Parameter(
            name='dexp_decay_beta_00',
            description='Double-exp first-regime decay β₀₀',
            units='μs',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.dexp_decay_beta_00']),
        )
        self._dexp_decay_beta_01 = Parameter(
            name='dexp_decay_beta_01',
            description='Double-exp first-regime decay β₀₁',
            units='μs/Å',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.dexp_decay_beta_01']),
        )
        self._dexp_decay_beta_10 = Parameter(
            name='dexp_decay_beta_10',
            description='Double-exp second-regime decay β₁₀',
            units='μs',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.dexp_decay_beta_10']),
        )
        self._dexp_switch_r_01 = Parameter(
            name='dexp_switch_r_01',
            description='Double-exp switching function r₀₁',
            units='',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.dexp_switch_r_01']),
        )
        self._dexp_switch_r_02 = Parameter(
            name='dexp_switch_r_02',
            description='Double-exp switching function r₀₂',
            units='',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.dexp_switch_r_02']),
        )
        self._dexp_switch_r_03 = Parameter(
            name='dexp_switch_r_03',
            description='Double-exp switching function r₀₃',
            units='',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.dexp_switch_r_03']),
        )

    @property
    def dexp_rise_alpha_1(self) -> Parameter:
        """
        Double-exp rise parameter α₁ (μs).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._dexp_rise_alpha_1

    @dexp_rise_alpha_1.setter
    def dexp_rise_alpha_1(self, value: float) -> None:
        self._dexp_rise_alpha_1.value = value

    @property
    def dexp_rise_alpha_2(self) -> Parameter:
        """
        Double-exp rise parameter α₂ (μs/Å).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._dexp_rise_alpha_2

    @dexp_rise_alpha_2.setter
    def dexp_rise_alpha_2(self, value: float) -> None:
        self._dexp_rise_alpha_2.value = value

    @property
    def dexp_decay_beta_00(self) -> Parameter:
        """
        Double-exp first-regime decay β₀₀ (μs).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._dexp_decay_beta_00

    @dexp_decay_beta_00.setter
    def dexp_decay_beta_00(self, value: float) -> None:
        self._dexp_decay_beta_00.value = value

    @property
    def dexp_decay_beta_01(self) -> Parameter:
        """
        Double-exp first-regime decay β₀₁ (μs/Å).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._dexp_decay_beta_01

    @dexp_decay_beta_01.setter
    def dexp_decay_beta_01(self, value: float) -> None:
        self._dexp_decay_beta_01.value = value

    @property
    def dexp_decay_beta_10(self) -> Parameter:
        """
        Double-exp second-regime decay β₁₀ (μs).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._dexp_decay_beta_10

    @dexp_decay_beta_10.setter
    def dexp_decay_beta_10(self, value: float) -> None:
        self._dexp_decay_beta_10.value = value

    @property
    def dexp_switch_r_01(self) -> Parameter:
        """
        Double-exp switching function r₀₁.

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._dexp_switch_r_01

    @dexp_switch_r_01.setter
    def dexp_switch_r_01(self, value: float) -> None:
        self._dexp_switch_r_01.value = value

    @property
    def dexp_switch_r_02(self) -> Parameter:
        """
        Double-exp switching function r₀₂.

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._dexp_switch_r_02

    @dexp_switch_r_02.setter
    def dexp_switch_r_02(self, value: float) -> None:
        self._dexp_switch_r_02.value = value

    @property
    def dexp_switch_r_03(self) -> Parameter:
        """
        Double-exp switching function r₀₃.

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._dexp_switch_r_03

    @dexp_switch_r_03.setter
    def dexp_switch_r_03(self, value: float) -> None:
        self._dexp_switch_r_03.value = value
dexp_decay_beta_00 property writable

Double-exp first-regime decay β₀₀ (μs).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

dexp_decay_beta_01 property writable

Double-exp first-regime decay β₀₁ (μs/Å).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

dexp_decay_beta_10 property writable

Double-exp second-regime decay β₁₀ (μs).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

dexp_rise_alpha_1 property writable

Double-exp rise parameter α₁ (μs).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

dexp_rise_alpha_2 property writable

Double-exp rise parameter α₂ (μs/Å).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

dexp_switch_r_01 property writable

Double-exp switching function r₀₁.

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

dexp_switch_r_02 property writable

Double-exp switching function r₀₂.

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

dexp_switch_r_03 property writable

Double-exp switching function r₀₃.

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

TofGaussianBroadeningMixin

TOF Gaussian broadening parameters σ₀, σ₁, σ₂.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
class TofGaussianBroadeningMixin:
    """TOF Gaussian broadening parameters σ₀, σ₁, σ₂."""

    def __init__(self) -> None:
        super().__init__()

        self._broad_gauss_sigma_0 = Parameter(
            name='gauss_sigma_0',
            description='Gaussian broadening (instrumental resolution)',
            units='μs²',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.gauss_sigma_0']),
        )
        self._broad_gauss_sigma_1 = Parameter(
            name='gauss_sigma_1',
            description='Gaussian broadening (dependent on d-spacing)',
            units='μs/Å',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.gauss_sigma_1']),
        )
        self._broad_gauss_sigma_2 = Parameter(
            name='gauss_sigma_2',
            description='Gaussian broadening (instrument-dependent term)',
            units='μs²/Ų',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.gauss_sigma_2']),
        )

    @property
    def broad_gauss_sigma_0(self) -> Parameter:
        """
        Gaussian broadening (instrumental resolution) (μs²).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._broad_gauss_sigma_0

    @broad_gauss_sigma_0.setter
    def broad_gauss_sigma_0(self, value: float) -> None:
        self._broad_gauss_sigma_0.value = value

    @property
    def broad_gauss_sigma_1(self) -> Parameter:
        """
        Gaussian broadening (dependent on d-spacing) (μs/Å).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._broad_gauss_sigma_1

    @broad_gauss_sigma_1.setter
    def broad_gauss_sigma_1(self, value: float) -> None:
        self._broad_gauss_sigma_1.value = value

    @property
    def broad_gauss_sigma_2(self) -> Parameter:
        """
        Gaussian broadening (instrument-dependent term) (μs²/Ų).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._broad_gauss_sigma_2

    @broad_gauss_sigma_2.setter
    def broad_gauss_sigma_2(self, value: float) -> None:
        self._broad_gauss_sigma_2.value = value
broad_gauss_sigma_0 property writable

Gaussian broadening (instrumental resolution) (μs²).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

broad_gauss_sigma_1 property writable

Gaussian broadening (dependent on d-spacing) (μs/Å).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

broad_gauss_sigma_2 property writable

Gaussian broadening (instrument-dependent term) (μs²/Ų).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

TofLorentzianBroadeningMixin

TOF Lorentzian broadening parameters γ₀, γ₁, γ₂.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.py
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
class TofLorentzianBroadeningMixin:
    """TOF Lorentzian broadening parameters γ₀, γ₁, γ₂."""

    def __init__(self) -> None:
        super().__init__()

        self._broad_lorentz_gamma_0 = Parameter(
            name='lorentz_gamma_0',
            description='Lorentzian broadening (microstrain effects)',
            units='μs',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.lorentz_gamma_0']),
        )
        self._broad_lorentz_gamma_1 = Parameter(
            name='lorentz_gamma_1',
            description='Lorentzian broadening (dependent on d-spacing)',
            units='μs/Å',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.lorentz_gamma_1']),
        )
        self._broad_lorentz_gamma_2 = Parameter(
            name='lorentz_gamma_2',
            description='Lorentzian broadening (instrument-dependent term)',
            units='μs²/Ų',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.lorentz_gamma_2']),
        )

    @property
    def broad_lorentz_gamma_0(self) -> Parameter:
        """
        Lorentzian broadening (microstrain effects) (μs).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._broad_lorentz_gamma_0

    @broad_lorentz_gamma_0.setter
    def broad_lorentz_gamma_0(self, value: float) -> None:
        self._broad_lorentz_gamma_0.value = value

    @property
    def broad_lorentz_gamma_1(self) -> Parameter:
        """
        Lorentzian broadening (dependent on d-spacing) (μs/Å).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._broad_lorentz_gamma_1

    @broad_lorentz_gamma_1.setter
    def broad_lorentz_gamma_1(self, value: float) -> None:
        self._broad_lorentz_gamma_1.value = value

    @property
    def broad_lorentz_gamma_2(self) -> Parameter:
        """
        Lorentzian broadening (instrument-dependent term) (μs²/Ų).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._broad_lorentz_gamma_2

    @broad_lorentz_gamma_2.setter
    def broad_lorentz_gamma_2(self, value: float) -> None:
        self._broad_lorentz_gamma_2.value = value
broad_lorentz_gamma_0 property writable

Lorentzian broadening (microstrain effects) (μs).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

broad_lorentz_gamma_1 property writable

Lorentzian broadening (dependent on d-spacing) (μs/Å).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

broad_lorentz_gamma_2 property writable

Lorentzian broadening (instrument-dependent term) (μs²/Ų).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

total

Total-scattering (PDF) peak profile classes.

TotalGaussianDampedSinc

Bases: PeakBase, TotalBroadeningMixin

Gaussian-damped sinc peak for total scattering (PDF).

Source code in src/easydiffraction/datablocks/experiment/categories/peak/total.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@PeakFactory.register
class TotalGaussianDampedSinc(
    PeakBase,
    TotalBroadeningMixin,
):
    """Gaussian-damped sinc peak for total scattering (PDF)."""

    type_info = TypeInfo(
        tag='gaussian-damped-sinc',
        description='Gaussian-damped sinc for pair distribution function analysis',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.TOTAL}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.PDFFIT}),
    )

    def __init__(self) -> None:
        super().__init__()

total_mixins

Total scattering / PDF peak-profile component classes.

This module provides classes that add broadening and asymmetry parameters. They are composed into concrete peak classes elsewhere via multiple inheritance.

TotalBroadeningMixin

PDF broadening/damping/sharpening parameters.

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

    def __init__(self) -> None:
        super().__init__()

        self._damp_q = Parameter(
            name='damp_q',
            description='Q-resolution damping for high-r PDF peak amplitude',
            units='Å⁻¹',
            value_spec=AttributeSpec(
                default=0.05,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.damp_q']),
        )
        self._broad_q = Parameter(
            name='broad_q',
            description='Quadratic peak broadening from thermal uncertainty',
            units='Å⁻²',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.broad_q']),
        )
        self._cutoff_q = Parameter(
            name='cutoff_q',
            description='Q-value cutoff for Fourier transform',
            units='Å⁻¹',
            value_spec=AttributeSpec(
                default=25.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.cutoff_q']),
        )
        self._sharp_delta_1 = Parameter(
            name='sharp_delta_1',
            description='Peak sharpening coefficient (1/r dependence)',
            units='Å',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.sharp_delta_1']),
        )
        self._sharp_delta_2 = Parameter(
            name='sharp_delta_2',
            description='Peak sharpening coefficient (1/r² dependence)',
            units='Ų',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.sharp_delta_2']),
        )
        self._damp_particle_diameter = Parameter(
            name='damp_particle_diameter',
            description='Particle diameter for spherical envelope damping correction',
            units='Å',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.damp_particle_diameter']),
        )

    # ------------------------------------------------------------------
    #  Public properties
    # ------------------------------------------------------------------

    @property
    def damp_q(self) -> Parameter:
        """
        Q-resolution damping for high-r PDF peak amplitude (Å⁻¹).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._damp_q

    @damp_q.setter
    def damp_q(self, value: float) -> None:
        self._damp_q.value = value

    @property
    def broad_q(self) -> Parameter:
        """
        Quadratic peak broadening from thermal uncertainty (Å⁻²).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._broad_q

    @broad_q.setter
    def broad_q(self, value: float) -> None:
        self._broad_q.value = value

    @property
    def cutoff_q(self) -> Parameter:
        """
        Q-value cutoff for Fourier transform (Å⁻¹).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._cutoff_q

    @cutoff_q.setter
    def cutoff_q(self, value: float) -> None:
        self._cutoff_q.value = value

    @property
    def sharp_delta_1(self) -> Parameter:
        """
        PDF peak sharpening coefficient (1/r dependence) (Å).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._sharp_delta_1

    @sharp_delta_1.setter
    def sharp_delta_1(self, value: float) -> None:
        self._sharp_delta_1.value = value

    @property
    def sharp_delta_2(self) -> Parameter:
        """
        PDF peak sharpening coefficient (1/r² dependence) (Ų).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._sharp_delta_2

    @sharp_delta_2.setter
    def sharp_delta_2(self, value: float) -> None:
        self._sharp_delta_2.value = value

    @property
    def damp_particle_diameter(self) -> Parameter:
        """
        Particle diameter for spherical envelope damping correction (Å).

        Reading this property returns the underlying ``Parameter``
        object. Assigning to it updates the parameter value.
        """
        return self._damp_particle_diameter

    @damp_particle_diameter.setter
    def damp_particle_diameter(self, value: float) -> None:
        self._damp_particle_diameter.value = value
broad_q property writable

Quadratic peak broadening from thermal uncertainty (Å⁻²).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

cutoff_q property writable

Q-value cutoff for Fourier transform (Å⁻¹).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

damp_particle_diameter property writable

Particle diameter for spherical envelope damping correction (Å).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

damp_q property writable

Q-resolution damping for high-r PDF peak amplitude (Å⁻¹).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

sharp_delta_1 property writable

PDF peak sharpening coefficient (1/r dependence) (Å).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

sharp_delta_2 property writable

PDF peak sharpening coefficient (1/r² dependence) (Ų).

Reading this property returns the underlying Parameter object. Assigning to it updates the parameter value.

collection

Collection of experiment data blocks.

Experiments

Bases: DatablockCollection

Collection of Experiment data blocks.

Provides convenience constructors for common creation patterns and helper methods for simple presentation of collection contents.

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

    Provides convenience constructors for common creation patterns and
    helper methods for simple presentation of collection contents.
    """

    def __init__(self) -> None:
        super().__init__(item_type=ExperimentBase)

    # ------------------------------------------------------------------
    # Public methods
    # ------------------------------------------------------------------

    # TODO: Make abstract in DatablockCollection?
    @typechecked
    def create(
        self,
        *,
        name: str,
        sample_form: str | None = None,
        beam_mode: str | None = None,
        radiation_probe: str | None = None,
        scattering_type: str | None = None,
    ) -> None:
        """
        Add an experiment without associating a data file.

        Parameters
        ----------
        name : str
            Experiment identifier.
        sample_form : str | None, default=None
            Sample form (e.g. ``'powder'``).
        beam_mode : str | None, default=None
            Beam mode (e.g. ``'constant wavelength'``).
        radiation_probe : str | None, default=None
            Radiation probe (e.g. ``'neutron'``).
        scattering_type : str | None, default=None
            Scattering type (e.g. ``'bragg'``).
        """
        experiment = ExperimentFactory.from_scratch(
            name=name,
            sample_form=sample_form,
            beam_mode=beam_mode,
            radiation_probe=radiation_probe,
            scattering_type=scattering_type,
        )
        self.add(experiment)

    # TODO: Move to DatablockCollection?
    @typechecked
    def add_from_cif_str(
        self,
        cif_str: str,
    ) -> None:
        """
        Add an experiment from a CIF string.

        Parameters
        ----------
        cif_str : str
            Full CIF document as a string.
        """
        experiment = ExperimentFactory.from_cif_str(cif_str)
        self.add(experiment)

    # TODO: Move to DatablockCollection?
    @typechecked
    def add_from_cif_path(
        self,
        cif_path: str,
    ) -> None:
        """
        Add an experiment from a CIF file path.

        Parameters
        ----------
        cif_path : str
            Path to a CIF document.
        """
        experiment = ExperimentFactory.from_cif_path(cif_path)
        self.add(experiment)

    @typechecked
    def add_from_data_path(
        self,
        *,
        name: str,
        data_path: str,
        sample_form: str | None = None,
        beam_mode: str | None = None,
        radiation_probe: str | None = None,
        scattering_type: str | None = None,
    ) -> None:
        """
        Add an experiment from a data file path.

        Parameters
        ----------
        name : str
            Experiment identifier.
        data_path : str
            Path to the measured data file.
        sample_form : str | None, default=None
            Sample form (e.g. ``'powder'``).
        beam_mode : str | None, default=None
            Beam mode (e.g. ``'constant wavelength'``).
        radiation_probe : str | None, default=None
            Radiation probe (e.g. ``'neutron'``).
        scattering_type : str | None, default=None
            Scattering type (e.g. ``'bragg'``).
        """
        verbosity = self._parent.verbosity if self._parent is not None else None
        verb = VerbosityEnum(verbosity) if verbosity is not None else VerbosityEnum.FULL
        experiment = ExperimentFactory.from_scratch(
            name=name,
            sample_form=sample_form,
            beam_mode=beam_mode,
            radiation_probe=radiation_probe,
            scattering_type=scattering_type,
        )
        num_points = experiment._load_ascii_data_to_experiment(data_path)
        if verb is VerbosityEnum.FULL:
            console.paragraph('Data loaded successfully')
            console.print(f"Experiment 🔬 '{name}'. Number of data points: {num_points}.")
        elif verb is VerbosityEnum.SHORT:
            console.print(f"✅ Data loaded: Experiment 🔬 '{name}'. {num_points} points.")
        self.add(experiment)

    # TODO: Move to DatablockCollection?
    def show_names(self) -> None:
        """List all experiment names in the collection."""
        console.paragraph('Defined experiments' + ' 🔬')
        console.print(self.names)

    # TODO: Move to DatablockCollection?
    def show_params(self) -> None:
        """Show parameters of all experiments in the collection."""
        for experiment in self.values():
            experiment.show_params()

add_from_cif_path(cif_path)

Add an experiment from a CIF file path.

Parameters:

Name Type Description Default
cif_path str

Path to a CIF document.

required
Source code in src/easydiffraction/datablocks/experiment/collection.py
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
@typechecked
def add_from_cif_path(
    self,
    cif_path: str,
) -> None:
    """
    Add an experiment from a CIF file path.

    Parameters
    ----------
    cif_path : str
        Path to a CIF document.
    """
    experiment = ExperimentFactory.from_cif_path(cif_path)
    self.add(experiment)

add_from_cif_str(cif_str)

Add an experiment from a CIF string.

Parameters:

Name Type Description Default
cif_str str

Full CIF document as a string.

required
Source code in src/easydiffraction/datablocks/experiment/collection.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
@typechecked
def add_from_cif_str(
    self,
    cif_str: str,
) -> None:
    """
    Add an experiment from a CIF string.

    Parameters
    ----------
    cif_str : str
        Full CIF document as a string.
    """
    experiment = ExperimentFactory.from_cif_str(cif_str)
    self.add(experiment)

add_from_data_path(*, name, data_path, sample_form=None, beam_mode=None, radiation_probe=None, scattering_type=None)

Add an experiment from a data file path.

Parameters:

Name Type Description Default
name str

Experiment identifier.

required
data_path str

Path to the measured data file.

required
sample_form str | None

Sample form (e.g. 'powder').

None
beam_mode str | None

Beam mode (e.g. 'constant wavelength').

None
radiation_probe str | None

Radiation probe (e.g. 'neutron').

None
scattering_type str | None

Scattering type (e.g. 'bragg').

None
Source code in src/easydiffraction/datablocks/experiment/collection.py
 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
@typechecked
def add_from_data_path(
    self,
    *,
    name: str,
    data_path: str,
    sample_form: str | None = None,
    beam_mode: str | None = None,
    radiation_probe: str | None = None,
    scattering_type: str | None = None,
) -> None:
    """
    Add an experiment from a data file path.

    Parameters
    ----------
    name : str
        Experiment identifier.
    data_path : str
        Path to the measured data file.
    sample_form : str | None, default=None
        Sample form (e.g. ``'powder'``).
    beam_mode : str | None, default=None
        Beam mode (e.g. ``'constant wavelength'``).
    radiation_probe : str | None, default=None
        Radiation probe (e.g. ``'neutron'``).
    scattering_type : str | None, default=None
        Scattering type (e.g. ``'bragg'``).
    """
    verbosity = self._parent.verbosity if self._parent is not None else None
    verb = VerbosityEnum(verbosity) if verbosity is not None else VerbosityEnum.FULL
    experiment = ExperimentFactory.from_scratch(
        name=name,
        sample_form=sample_form,
        beam_mode=beam_mode,
        radiation_probe=radiation_probe,
        scattering_type=scattering_type,
    )
    num_points = experiment._load_ascii_data_to_experiment(data_path)
    if verb is VerbosityEnum.FULL:
        console.paragraph('Data loaded successfully')
        console.print(f"Experiment 🔬 '{name}'. Number of data points: {num_points}.")
    elif verb is VerbosityEnum.SHORT:
        console.print(f"✅ Data loaded: Experiment 🔬 '{name}'. {num_points} points.")
    self.add(experiment)

create(*, name, sample_form=None, beam_mode=None, radiation_probe=None, scattering_type=None)

Add an experiment without associating a data file.

Parameters:

Name Type Description Default
name str

Experiment identifier.

required
sample_form str | None

Sample form (e.g. 'powder').

None
beam_mode str | None

Beam mode (e.g. 'constant wavelength').

None
radiation_probe str | None

Radiation probe (e.g. 'neutron').

None
scattering_type str | None

Scattering type (e.g. 'bragg').

None
Source code in src/easydiffraction/datablocks/experiment/collection.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
@typechecked
def create(
    self,
    *,
    name: str,
    sample_form: str | None = None,
    beam_mode: str | None = None,
    radiation_probe: str | None = None,
    scattering_type: str | None = None,
) -> None:
    """
    Add an experiment without associating a data file.

    Parameters
    ----------
    name : str
        Experiment identifier.
    sample_form : str | None, default=None
        Sample form (e.g. ``'powder'``).
    beam_mode : str | None, default=None
        Beam mode (e.g. ``'constant wavelength'``).
    radiation_probe : str | None, default=None
        Radiation probe (e.g. ``'neutron'``).
    scattering_type : str | None, default=None
        Scattering type (e.g. ``'bragg'``).
    """
    experiment = ExperimentFactory.from_scratch(
        name=name,
        sample_form=sample_form,
        beam_mode=beam_mode,
        radiation_probe=radiation_probe,
        scattering_type=scattering_type,
    )
    self.add(experiment)

show_names()

List all experiment names in the collection.

Source code in src/easydiffraction/datablocks/experiment/collection.py
146
147
148
149
def show_names(self) -> None:
    """List all experiment names in the collection."""
    console.paragraph('Defined experiments' + ' 🔬')
    console.print(self.names)

show_params()

Show parameters of all experiments in the collection.

Source code in src/easydiffraction/datablocks/experiment/collection.py
152
153
154
155
def show_params(self) -> None:
    """Show parameters of all experiments in the collection."""
    for experiment in self.values():
        experiment.show_params()

item

base

Base classes for experiment datablock items.

ExperimentBase

Bases: DatablockItem

Base class for all experiment datablock items.

Source code in src/easydiffraction/datablocks/experiment/item/base.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
 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
class ExperimentBase(DatablockItem):
    """Base class for all experiment datablock items."""

    def __init__(
        self,
        *,
        name: str,
        type: ExperimentType,
    ) -> None:
        super().__init__()
        self._name = name
        self._type = type
        self._calculator = None
        self._calculator_type: str | None = None
        self._identity.datablock_entry_name = lambda: self.name

        self._diffrn_type: str = DiffrnFactory.default_tag()
        self._diffrn = DiffrnFactory.create(self._diffrn_type)

    @property
    def name(self) -> str:
        """Human-readable name of the experiment."""
        return self._name

    @name.setter
    def name(self, new: str) -> None:
        """
        Rename the experiment.

        Parameters
        ----------
        new : str
            New name for this experiment.
        """
        self._name = new

    @property
    def type(self) -> object:  # TODO: Consider another name
        """Experiment type: sample form, probe, beam mode."""
        return self._type

    # ------------------------------------------------------------------
    #  Diffrn conditions (read-only, single type)
    # ------------------------------------------------------------------

    @property
    def diffrn(self) -> object:
        """Ambient conditions recorded during measurement."""
        return self._diffrn

    def _restore_switchable_types(self, block: object) -> None:
        """
        Restore switchable category types from a parsed CIF block.

        Called by the factory immediately after the experiment object is
        created and before any category parameters are loaded from CIF.
        Subclasses with switchable categories must override this method
        and call their ``_set_<type>`` private setter for each category
        whose active implementation is identified by a CIF type tag.

        Parameters
        ----------
        block : object
            Parsed ``gemmi.cif.Block`` to read type tags from.
        """

    @property
    def as_cif(self) -> str:
        """Serialize this experiment to a CIF fragment."""
        return experiment_to_cif(self)

    def show_as_cif(self) -> None:
        """Pretty-print the experiment as CIF text."""
        paragraph_title: str = f"Experiment 🔬 '{self.name}' as cif"
        console.paragraph(paragraph_title)
        render_cif(self._cif_for_display())

    @abstractmethod
    def _load_ascii_data_to_experiment(self, data_path: str) -> None:
        """
        Load ASCII data from file into the experiment data category.

        Parameters
        ----------
        data_path : str
            Path to the ASCII file to load.

        Raises
        ------
        NotImplementedError
            Subclasses must implement this method.
        """
        raise NotImplementedError

    # ------------------------------------------------------------------
    #  Calculator (switchable-category pattern)
    # ------------------------------------------------------------------

    @property
    def calculator(self) -> object:
        """
        The active calculator instance for this experiment.

        Auto-resolved on first access from the experiment's data
        category ``calculator_support`` and
        ``CalculatorFactory._default_rules``.
        """
        if self._calculator is None:
            self._resolve_calculator()
        return self._calculator

    @property
    def calculator_type(self) -> str:
        """Tag of the active calculator backend (e.g. ``'cryspy'``)."""
        if self._calculator_type is None:
            self._resolve_calculator()
        return self._calculator_type

    @calculator_type.setter
    def calculator_type(self, tag: str) -> None:
        """
        Switch to a different calculator backend.

        Parameters
        ----------
        tag : str
            Calculator tag (e.g. ``'cryspy'``, ``'crysfml'``,
            ``'pdffit'``).
        """
        from easydiffraction.analysis.calculators.factory import CalculatorFactory  # noqa: PLC0415

        supported = self._supported_calculator_tags()
        if tag not in supported:
            log.warning(
                f"Unsupported calculator '{tag}' for experiment "
                f"'{self.name}'. Supported: {supported}. "
                f"For more information, use 'show_supported_calculator_types()'",
            )
            return
        self._calculator = CalculatorFactory.create(tag)
        self._calculator_type = tag
        console.paragraph(f"Calculator for experiment '{self.name}' changed to")
        console.print(tag)

    def show_supported_calculator_types(self) -> None:
        """Print a table of supported calculator backends."""
        from easydiffraction.analysis.calculators.factory import CalculatorFactory  # noqa: PLC0415

        supported_tags = self._supported_calculator_tags()
        all_classes = CalculatorFactory._supported_map()
        columns_headers = ['Type', 'Description']
        columns_alignment = ['left', 'left']
        columns_data = [
            [cls.type_info.tag, cls.type_info.description]
            for tag, cls in all_classes.items()
            if tag in supported_tags
        ]

        console.paragraph('Supported calculator types')
        render_table(
            columns_headers=columns_headers,
            columns_alignment=columns_alignment,
            columns_data=columns_data,
        )

    def show_current_calculator_type(self) -> None:
        """Print the name of the currently active calculator."""
        console.paragraph('Current calculator type')
        console.print(self.calculator_type)

    def _resolve_calculator(self) -> None:
        """Auto-resolve the default calculator from data category."""
        from easydiffraction.analysis.calculators.factory import CalculatorFactory  # noqa: PLC0415

        tag = CalculatorFactory.default_tag(
            scattering_type=self.type.scattering_type.value,
        )
        supported = self._supported_calculator_tags()
        if supported and tag not in supported:
            tag = supported[0]
        self._calculator = CalculatorFactory.create(tag)
        self._calculator_type = tag

    def _supported_calculator_tags(self) -> list[str]:
        """
        Return calculator tags supported by this experiment.

        Intersects the data category's ``calculator_support`` with
        calculators whose engines are importable.
        """
        from easydiffraction.analysis.calculators.factory import CalculatorFactory  # noqa: PLC0415

        available = CalculatorFactory.supported_tags()
        data = getattr(self, '_data', None)
        if data is not None:
            data_support = getattr(data, 'calculator_support', None)
            if data_support and data_support.calculators:
                return [t for t in available if t in data_support.calculators]
        return available
as_cif property

Serialize this experiment to a CIF fragment.

calculator property

The active calculator instance for this experiment.

Auto-resolved on first access from the experiment's data category calculator_support and CalculatorFactory._default_rules.

calculator_type property writable

Tag of the active calculator backend (e.g. 'cryspy').

diffrn property

Ambient conditions recorded during measurement.

name property writable

Human-readable name of the experiment.

show_as_cif()

Pretty-print the experiment as CIF text.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
109
110
111
112
113
def show_as_cif(self) -> None:
    """Pretty-print the experiment as CIF text."""
    paragraph_title: str = f"Experiment 🔬 '{self.name}' as cif"
    console.paragraph(paragraph_title)
    render_cif(self._cif_for_display())
show_current_calculator_type()

Print the name of the currently active calculator.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
203
204
205
206
def show_current_calculator_type(self) -> None:
    """Print the name of the currently active calculator."""
    console.paragraph('Current calculator type')
    console.print(self.calculator_type)
show_supported_calculator_types()

Print a table of supported calculator backends.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def show_supported_calculator_types(self) -> None:
    """Print a table of supported calculator backends."""
    from easydiffraction.analysis.calculators.factory import CalculatorFactory  # noqa: PLC0415

    supported_tags = self._supported_calculator_tags()
    all_classes = CalculatorFactory._supported_map()
    columns_headers = ['Type', 'Description']
    columns_alignment = ['left', 'left']
    columns_data = [
        [cls.type_info.tag, cls.type_info.description]
        for tag, cls in all_classes.items()
        if tag in supported_tags
    ]

    console.paragraph('Supported calculator types')
    render_table(
        columns_headers=columns_headers,
        columns_alignment=columns_alignment,
        columns_data=columns_data,
    )
type property

Experiment type: sample form, probe, beam mode.

PdExperimentBase

Bases: ExperimentBase

Base class for all powder experiments.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
class PdExperimentBase(ExperimentBase):
    """Base class for all powder experiments."""

    def __init__(
        self,
        *,
        name: str,
        type: ExperimentType,
    ) -> None:
        super().__init__(name=name, type=type)

        self._linked_phases_type: str = LinkedPhasesFactory.default_tag()
        self._linked_phases = LinkedPhasesFactory.create(self._linked_phases_type)
        self._excluded_regions_type: str = ExcludedRegionsFactory.default_tag()
        self._excluded_regions = ExcludedRegionsFactory.create(self._excluded_regions_type)
        self._peak_profile_type: str = PeakFactory.default_tag(
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
        )
        self._data_type: str = DataFactory.default_tag(
            sample_form=self.type.sample_form.value,
            beam_mode=self.type.beam_mode.value,
            scattering_type=self.type.scattering_type.value,
        )
        self._data = DataFactory.create(self._data_type)
        self._peak = PeakFactory.create(self._peak_profile_type)

    def _get_valid_linked_phases(
        self,
        structures: Structures,
    ) -> list[Any]:
        """
        Get valid linked phases for this experiment.

        Parameters
        ----------
        structures : Structures
            Collection of structures.

        Returns
        -------
        list[Any]
            A list of valid linked phases.
        """
        if not self.linked_phases:
            print('Warning: No linked phases defined. Returning empty pattern.')
            return []

        valid_linked_phases = []
        for linked_phase in self.linked_phases:
            if linked_phase._identity.category_entry_name not in structures.names:
                print(
                    f"Warning: Linked phase '{linked_phase.id.value}' not "
                    f'found in Structures {structures.names}. Skipping it.'
                )
                continue
            valid_linked_phases.append(linked_phase)

        if not valid_linked_phases:
            print(
                'Warning: None of the linked phases found in Structures. Returning empty pattern.'
            )

        return valid_linked_phases

    @abstractmethod
    def _load_ascii_data_to_experiment(self, data_path: str) -> int:
        """
        Load powder diffraction data from an ASCII file.

        Parameters
        ----------
        data_path : str
            Path to data file with columns compatible with the beam mode
            (e.g. 2theta/I/sigma for CWL, TOF/I/sigma for TOF).

        Returns
        -------
        int
            Number of loaded data points.
        """

    @property
    def linked_phases(self) -> object:
        """Collection of phases linked to this experiment."""
        return self._linked_phases

    @property
    def excluded_regions(self) -> object:
        """Collection of excluded regions for the x-grid."""
        return self._excluded_regions

    # ------------------------------------------------------------------
    #  Data (fixed at creation)
    # ------------------------------------------------------------------

    @property
    def data(self) -> object:
        """Data collection for this experiment."""
        return self._data

    @property
    def peak(self) -> object:
        """Peak category object with profile parameters and mixins."""
        return self._peak

    @property
    def peak_profile_type(self) -> object:
        """Currently selected peak profile type enum."""
        return self._peak_profile_type

    @peak_profile_type.setter
    def peak_profile_type(self, new_type: str) -> None:
        """
        Change the active peak profile type, if supported.

        Parameters
        ----------
        new_type : str
            New profile type as tag string.
        """
        supported = PeakFactory.supported_for(
            calculator=self.calculator_type,
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
        )
        supported_tags = [k.type_info.tag for k in supported]

        if new_type not in supported_tags:
            log.warning(
                f"Unsupported peak profile '{new_type}'. "
                f'Supported peak profiles: {supported_tags}. '
                f"For more information, use 'show_supported_peak_profile_types()'",
            )
            return

        if self._peak is not None:
            log.warning(
                'Switching peak profile type discards existing peak parameters.',
            )

        self._peak = PeakFactory.create(new_type)
        self._peak_profile_type = new_type
        console.paragraph(f"Peak profile type for experiment '{self.name}' changed to")
        console.print(new_type)

    def show_supported_peak_profile_types(self) -> None:
        """Print available peak profile types for this experiment."""
        PeakFactory.show_supported(
            calculator=self.calculator_type,
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
        )

    def show_current_peak_profile_type(self) -> None:
        """Print the currently selected peak profile type."""
        console.paragraph('Current peak profile type')
        console.print(self.peak_profile_type)

    def _set_peak_profile_type(self, new_type: str) -> None:
        """
        Switch the peak profile type without console output.

        Used internally by the factory when restoring state from CIF so
        that no user-facing warnings or progress messages are emitted.
        Invalid type tags are logged as warnings and ignored.

        Parameters
        ----------
        new_type : str
            Peak profile type tag (e.g. ``'pseudo-voigt + empirical
            asymmetry'``).
        """
        supported = PeakFactory.supported_for(
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
        )
        supported_tags = [k.type_info.tag for k in supported]
        if new_type not in supported_tags:
            log.warning(
                f"Unsupported peak profile '{new_type}' in CIF. "
                f'Supported: {supported_tags}. Keeping default.',
            )
            return
        self._peak = PeakFactory.create(new_type)
        self._peak_profile_type = new_type

    def _restore_switchable_types(self, block: object) -> None:
        """
        Restore switchable category types for powder experiments.

        Reads ``_peak.profile_type`` from the CIF block and switches to
        the matching peak implementation before category parameters are
        loaded, ensuring profile-specific descriptors are present.

        Parameters
        ----------
        block : object
            Parsed ``gemmi.cif.Block`` to read type tags from.
        """
        peak_type = read_cif_str(block, '_peak.profile_type')
        if peak_type is not None:
            self._set_peak_profile_type(peak_type)
data property

Data collection for this experiment.

excluded_regions property

Collection of excluded regions for the x-grid.

linked_phases property

Collection of phases linked to this experiment.

peak property

Peak category object with profile parameters and mixins.

peak_profile_type property writable

Currently selected peak profile type enum.

show_current_peak_profile_type()

Print the currently selected peak profile type.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
512
513
514
515
def show_current_peak_profile_type(self) -> None:
    """Print the currently selected peak profile type."""
    console.paragraph('Current peak profile type')
    console.print(self.peak_profile_type)
show_supported_peak_profile_types()

Print available peak profile types for this experiment.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
504
505
506
507
508
509
510
def show_supported_peak_profile_types(self) -> None:
    """Print available peak profile types for this experiment."""
    PeakFactory.show_supported(
        calculator=self.calculator_type,
        scattering_type=self.type.scattering_type.value,
        beam_mode=self.type.beam_mode.value,
    )

ScExperimentBase

Bases: ExperimentBase

Base class for all single crystal experiments.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
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
class ScExperimentBase(ExperimentBase):
    """Base class for all single crystal experiments."""

    def __init__(
        self,
        *,
        name: str,
        type: ExperimentType,
    ) -> None:
        super().__init__(name=name, type=type)

        self._extinction_type: str = ExtinctionFactory.default_tag()
        self._extinction = ExtinctionFactory.create(self._extinction_type)
        self._linked_crystal_type: str = LinkedCrystalFactory.default_tag()
        self._linked_crystal = LinkedCrystalFactory.create(self._linked_crystal_type)
        self._instrument_type: str = InstrumentFactory.default_tag(
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
            sample_form=self.type.sample_form.value,
        )
        self._instrument = InstrumentFactory.create(self._instrument_type)
        self._data_type: str = DataFactory.default_tag(
            sample_form=self.type.sample_form.value,
            beam_mode=self.type.beam_mode.value,
            scattering_type=self.type.scattering_type.value,
        )
        self._data = DataFactory.create(self._data_type)

    @abstractmethod
    def _load_ascii_data_to_experiment(self, data_path: str) -> None:
        """
        Load single crystal data from an ASCII file.

        Parameters
        ----------
        data_path : str
            Path to data file with columns compatible with the beam
            mode.
        """

    # ------------------------------------------------------------------
    #  Extinction (switchable-category pattern)
    # ------------------------------------------------------------------

    @property
    def extinction(self) -> object:
        """Active extinction correction model."""
        return self._extinction

    @property
    def extinction_type(self) -> str:
        """Tag of the active extinction correction model."""
        return self._extinction_type

    @extinction_type.setter
    def extinction_type(self, new_type: str) -> None:
        """
        Switch to a different extinction correction model.

        Parameters
        ----------
        new_type : str
            Extinction tag (e.g. ``'becker-coppens'``).
        """
        supported = ExtinctionFactory.supported_for(
            calculator=self.calculator_type,
        )
        supported_tags = [k.type_info.tag for k in supported]
        if new_type not in supported_tags:
            log.warning(
                f"Unsupported extinction type '{new_type}'. "
                f'Supported: {supported_tags}. '
                f"For more information, use 'show_supported_extinction_types()'",
            )
            return
        self._extinction = ExtinctionFactory.create(new_type)
        self._extinction_type = new_type
        console.paragraph('Extinction type changed to')
        console.print(new_type)

    def show_supported_extinction_types(self) -> None:
        """Print a table of supported extinction correction types."""
        ExtinctionFactory.show_supported(
            calculator=self.calculator_type,
        )

    def show_current_extinction_type(self) -> None:
        """Print the currently used extinction correction type."""
        console.paragraph('Current extinction type')
        console.print(self._extinction_type)

    # ------------------------------------------------------------------
    #  Linked crystal (read-only, single type)
    # ------------------------------------------------------------------

    @property
    def linked_crystal(self) -> object:
        """Linked crystal model for this experiment."""
        return self._linked_crystal

    # ------------------------------------------------------------------
    #  Instrument (fixed at creation)
    # ------------------------------------------------------------------

    @property
    def instrument(self) -> object:
        """Active instrument model for this experiment."""
        return self._instrument

    # ------------------------------------------------------------------
    #  Data (fixed at creation)
    # ------------------------------------------------------------------

    @property
    def data(self) -> object:
        """Data collection for this experiment."""
        return self._data
data property

Data collection for this experiment.

extinction property

Active extinction correction model.

extinction_type property writable

Tag of the active extinction correction model.

instrument property

Active instrument model for this experiment.

linked_crystal property

Linked crystal model for this experiment.

show_current_extinction_type()

Print the currently used extinction correction type.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
325
326
327
328
def show_current_extinction_type(self) -> None:
    """Print the currently used extinction correction type."""
    console.paragraph('Current extinction type')
    console.print(self._extinction_type)
show_supported_extinction_types()

Print a table of supported extinction correction types.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
319
320
321
322
323
def show_supported_extinction_types(self) -> None:
    """Print a table of supported extinction correction types."""
    ExtinctionFactory.show_supported(
        calculator=self.calculator_type,
    )

bragg_pd

BraggPdExperiment

Bases: PdExperimentBase

Standard Bragg powder diffraction experiment.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_pd.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 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
@ExperimentFactory.register
class BraggPdExperiment(PdExperimentBase):
    """Standard Bragg powder diffraction experiment."""

    type_info = TypeInfo(
        tag='bragg-pd',
        description='Bragg powder diffraction experiment',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        sample_form=frozenset({SampleFormEnum.POWDER}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}),
    )

    def __init__(
        self,
        *,
        name: str,
        type: ExperimentType,
    ) -> None:
        super().__init__(name=name, type=type)

        self._instrument_type: str = InstrumentFactory.default_tag(
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
            sample_form=self.type.sample_form.value,
        )
        self._instrument = InstrumentFactory.create(self._instrument_type)
        self._background_type: str = BackgroundFactory.default_tag()
        self._background = BackgroundFactory.create(self._background_type)

    def _load_ascii_data_to_experiment(
        self,
        data_path: str,
    ) -> int:
        """
        Load (x, y, sy) data from an ASCII file into the data category.

        The file format is space/column separated with 2 or 3 columns:
        ``x y [sy]``. If ``sy`` is missing, it is approximated as
        ``sqrt(y)``.

        If ``sy`` has values smaller than ``0.0001``, they are replaced
        with ``1.0``.

        Parameters
        ----------
        data_path : str
            Path to the ASCII data file.

        Returns
        -------
        int
            Number of loaded data points.
        """
        data = load_numeric_block(data_path)

        if data.shape[1] < _MIN_COLUMNS_XY:
            log.error(
                'Data file must have at least two columns: x and y.',
                exc_type=ValueError,
            )
            return 0

        if data.shape[1] < _MIN_COLUMNS_XY_SY:
            log.warning('No uncertainty (sy) column provided. Defaulting to sqrt(y).')

        # Extract x, y data
        x = data[:, 0]
        y = data[:, 1]

        # Round x to 4 decimal places
        x = np.round(x, 4)

        # Determine sy from column 3 if available, otherwise use sqrt(y)
        sy = data[:, 2] if data.shape[1] > _MIN_COLUMNS_XY else np.sqrt(y)

        # Replace values smaller than _MIN_UNCERTAINTY with 1.0
        # TODO: Not used if loading from cif file?
        sy = np.where(sy < _MIN_UNCERTAINTY, 1.0, sy)

        # Set the experiment data
        self.data._create_items_set_xcoord_and_id(x)
        self.data._set_intensity_meas(y)
        self.data._set_intensity_meas_su(sy)

        return len(x)

    # ------------------------------------------------------------------
    #  Instrument (fixed at creation)
    # ------------------------------------------------------------------

    @property
    def instrument(self) -> object:
        """Active instrument model for this experiment."""
        return self._instrument

    # ------------------------------------------------------------------
    #  Background (switchable-category pattern)
    # ------------------------------------------------------------------

    @property
    def background_type(self) -> object:
        """Current background type enum value."""
        return self._background_type

    @background_type.setter
    def background_type(self, new_type: str) -> None:
        """Set a new background type and recreate background object."""
        if self._background_type == new_type:
            console.paragraph(f"Background type for experiment '{self.name}' already set to")
            console.print(new_type)
            return

        supported = BackgroundFactory.supported_for(
            calculator=self.calculator_type,
        )
        supported_tags = [k.type_info.tag for k in supported]
        if new_type not in supported_tags:
            log.warning(
                f"Unsupported background type '{new_type}'. "
                f'Supported: {supported_tags}. '
                f"For more information, use 'show_supported_background_types()'",
            )
            return

        if len(self._background) > 0:
            log.warning(
                f'Switching background type discards {len(self._background)} '
                f'existing background point(s).',
            )

        self._background = BackgroundFactory.create(new_type)
        self._background_type = new_type
        console.paragraph(f"Background type for experiment '{self.name}' changed to")
        console.print(new_type)

    @property
    def background(self) -> object:
        """Active background model for this experiment."""
        return self._background

    def show_supported_background_types(self) -> None:
        """Print a table of supported background types."""
        BackgroundFactory.show_supported(
            calculator=self.calculator_type,
        )

    def show_current_background_type(self) -> None:
        """Print the currently used background type."""
        console.paragraph('Current background type')
        console.print(self.background_type)
background property

Active background model for this experiment.

background_type property writable

Current background type enum value.

instrument property

Active instrument model for this experiment.

show_current_background_type()

Print the currently used background type.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_pd.py
182
183
184
185
def show_current_background_type(self) -> None:
    """Print the currently used background type."""
    console.paragraph('Current background type')
    console.print(self.background_type)
show_supported_background_types()

Print a table of supported background types.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_pd.py
176
177
178
179
180
def show_supported_background_types(self) -> None:
    """Print a table of supported background types."""
    BackgroundFactory.show_supported(
        calculator=self.calculator_type,
    )

bragg_sc

CwlScExperiment

Bases: ScExperimentBase

Bragg constant-wavelength single-crystal experiment.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_sc.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@ExperimentFactory.register
class CwlScExperiment(ScExperimentBase):
    """Bragg constant-wavelength single-crystal experiment."""

    type_info = TypeInfo(
        tag='bragg-sc-cwl',
        description='Bragg CWL single-crystal experiment',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}),
    )

    def __init__(
        self,
        *,
        name: str,
        type: ExperimentType,
    ) -> None:
        super().__init__(name=name, type=type)

    def _load_ascii_data_to_experiment(self, data_path: str) -> int:
        """
        Load measured data from an ASCII file into the data category.

        The file format is space/column separated with 5 columns: ``h k
        l Iobs sIobs``.

        Parameters
        ----------
        data_path : str
            Path to the ASCII data file.

        Returns
        -------
        int
            Number of loaded data points.
        """
        data = load_numeric_block(data_path)

        if data.shape[1] < _MIN_COLUMNS_CWL_SC:
            log.error(
                'Data file must have at least 5 columns: h, k, l, Iobs, sIobs.',
                exc_type=ValueError,
            )
            return 0

        # Extract Miller indices h, k, l
        indices_h = data[:, 0].astype(int)
        indices_k = data[:, 1].astype(int)
        indices_l = data[:, 2].astype(int)

        # Extract intensities and their standard uncertainties
        integrated_intensities = data[:, 3]
        integrated_intensities_su = data[:, 4]

        # Set the experiment data
        self.data._create_items_set_hkl_and_id(indices_h, indices_k, indices_l)
        self.data._set_intensity_meas(integrated_intensities)
        self.data._set_intensity_meas_su(integrated_intensities_su)

        return len(indices_h)

TofScExperiment

Bases: ScExperimentBase

Bragg time-of-flight single-crystal experiment.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_sc.py
 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
@ExperimentFactory.register
class TofScExperiment(ScExperimentBase):
    """Bragg time-of-flight single-crystal experiment."""

    type_info = TypeInfo(
        tag='bragg-sc-tof',
        description='Bragg TOF single-crystal experiment',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}),
        beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}),
    )

    def __init__(
        self,
        *,
        name: str,
        type: ExperimentType,
    ) -> None:
        super().__init__(name=name, type=type)

    def _load_ascii_data_to_experiment(self, data_path: str) -> int:
        """
        Load measured data from an ASCII file into the data category.

        The file format is space/column separated with 6 columns: ``h k
        l Iobs sIobs wavelength``.

        Parameters
        ----------
        data_path : str
            Path to the ASCII data file.

        Returns
        -------
        int
            Number of loaded data points.
        """
        try:
            data = load_numeric_block(data_path)
        except OSError as e:
            log.error(
                f'Failed to read data from {data_path}: {e}',
                exc_type=IOError,
            )
            return 0

        if data.shape[1] < _MIN_COLUMNS_TOF_SC:
            log.error(
                'Data file must have at least 6 columns: h, k, l, Iobs, sIobs, wavelength.',
                exc_type=ValueError,
            )
            return 0

        # Extract Miller indices h, k, l
        indices_h = data[:, 0].astype(int)
        indices_k = data[:, 1].astype(int)
        indices_l = data[:, 2].astype(int)

        # Extract intensities and their standard uncertainties
        integrated_intensities = data[:, 3]
        integrated_intensities_su = data[:, 4]

        # Extract wavelength values
        wavelength = data[:, 5]

        # Set the experiment data
        self.data._create_items_set_hkl_and_id(indices_h, indices_k, indices_l)
        self.data._set_intensity_meas(integrated_intensities)
        self.data._set_intensity_meas_su(integrated_intensities_su)
        self.data._set_wavelength(wavelength)

        return len(indices_h)

enums

Enumerations for experiment configuration (forms, modes, types).

BeamModeEnum

Bases: StrEnum

Beam delivery mode for the instrument.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
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
class BeamModeEnum(StrEnum):
    """Beam delivery mode for the instrument."""

    # TODO: Rename to CWL and TOF
    CONSTANT_WAVELENGTH = 'constant wavelength'
    TIME_OF_FLIGHT = 'time-of-flight'

    @classmethod
    def default(cls) -> 'BeamModeEnum':
        """
        Return the default beam mode (CONSTANT_WAVELENGTH).

        Returns
        -------
        'BeamModeEnum'
            The default enum member.
        """
        return cls.CONSTANT_WAVELENGTH

    def description(self) -> str:
        """
        Return a human-readable description of this beam mode.

        Returns
        -------
        str
            Description string for the current enum member.
        """
        if self is BeamModeEnum.CONSTANT_WAVELENGTH:
            return 'Constant wavelength (CW) diffraction.'
        if self is BeamModeEnum.TIME_OF_FLIGHT:
            return 'Time-of-flight (TOF) diffraction.'
        return None
default() classmethod

Return the default beam mode (CONSTANT_WAVELENGTH).

Returns:

Type Description
BeamModeEnum

The default enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
117
118
119
120
121
122
123
124
125
126
127
@classmethod
def default(cls) -> 'BeamModeEnum':
    """
    Return the default beam mode (CONSTANT_WAVELENGTH).

    Returns
    -------
    'BeamModeEnum'
        The default enum member.
    """
    return cls.CONSTANT_WAVELENGTH
description()

Return a human-readable description of this beam mode.

Returns:

Type Description
str

Description string for the current enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def description(self) -> str:
    """
    Return a human-readable description of this beam mode.

    Returns
    -------
    str
        Description string for the current enum member.
    """
    if self is BeamModeEnum.CONSTANT_WAVELENGTH:
        return 'Constant wavelength (CW) diffraction.'
    if self is BeamModeEnum.TIME_OF_FLIGHT:
        return 'Time-of-flight (TOF) diffraction.'
    return None

CalculatorEnum

Bases: StrEnum

Known calculation engine identifiers.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
145
146
147
148
149
150
class CalculatorEnum(StrEnum):
    """Known calculation engine identifiers."""

    CRYSPY = 'cryspy'
    CRYSFML = 'crysfml'
    PDFFIT = 'pdffit'

ExtinctionModelEnum

Bases: StrEnum

Mosaicity distribution model for Becker-Coppens extinction.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
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
class ExtinctionModelEnum(StrEnum):
    """Mosaicity distribution model for Becker-Coppens extinction."""

    GAUSS = 'gauss'
    LORENTZ = 'lorentz'

    @classmethod
    def default(cls) -> 'ExtinctionModelEnum':
        """
        Return the default extinction model (GAUSS).

        Returns
        -------
        'ExtinctionModelEnum'
            The default enum member.
        """
        return cls.GAUSS

    def description(self) -> str:
        """
        Return a human-readable description of this extinction model.

        Returns
        -------
        str
            Description string for the current enum member.
        """
        if self is ExtinctionModelEnum.GAUSS:
            return 'Gaussian mosaicity distribution for extinction correction.'
        if self is ExtinctionModelEnum.LORENTZ:
            return 'Lorentzian mosaicity distribution for extinction correction.'
        return None
default() classmethod

Return the default extinction model (GAUSS).

Returns:

Type Description
ExtinctionModelEnum

The default enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
241
242
243
244
245
246
247
248
249
250
251
@classmethod
def default(cls) -> 'ExtinctionModelEnum':
    """
    Return the default extinction model (GAUSS).

    Returns
    -------
    'ExtinctionModelEnum'
        The default enum member.
    """
    return cls.GAUSS
description()

Return a human-readable description of this extinction model.

Returns:

Type Description
str

Description string for the current enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
253
254
255
256
257
258
259
260
261
262
263
264
265
266
def description(self) -> str:
    """
    Return a human-readable description of this extinction model.

    Returns
    -------
    str
        Description string for the current enum member.
    """
    if self is ExtinctionModelEnum.GAUSS:
        return 'Gaussian mosaicity distribution for extinction correction.'
    if self is ExtinctionModelEnum.LORENTZ:
        return 'Lorentzian mosaicity distribution for extinction correction.'
    return None

PeakProfileTypeEnum

Bases: StrEnum

Available peak profile types per scattering and beam mode.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
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
class PeakProfileTypeEnum(StrEnum):
    """Available peak profile types per scattering and beam mode."""

    PSEUDO_VOIGT = 'pseudo-voigt'
    PSEUDO_VOIGT_EMPIRICAL_ASYMMETRY = 'pseudo-voigt + empirical asymmetry'
    THOMPSON_COX_HASTINGS = 'thompson-cox-hastings'
    JORGENSEN = 'jorgensen'
    JORGENSEN_VON_DREELE = 'jorgensen-von-dreele'
    DOUBLE_JORGENSEN_VON_DREELE = 'double-jorgensen-von-dreele'
    GAUSSIAN_DAMPED_SINC = 'gaussian-damped-sinc'

    @classmethod
    def default(
        cls,
        scattering_type: ScatteringTypeEnum | None = None,
        beam_mode: BeamModeEnum | None = None,
    ) -> 'PeakProfileTypeEnum':
        """
        Return the default peak profile type for a given mode.

        Parameters
        ----------
        scattering_type : ScatteringTypeEnum | None, default=None
            Scattering type; defaults to
            ``ScatteringTypeEnum.default()`` when ``None``.
        beam_mode : BeamModeEnum | None, default=None
            Beam mode; defaults to ``BeamModeEnum.default()`` when
            ``None``.

        Returns
        -------
        'PeakProfileTypeEnum'
            The default profile type for the given combination.
        """
        if scattering_type is None:
            scattering_type = ScatteringTypeEnum.default()
        if beam_mode is None:
            beam_mode = BeamModeEnum.default()
        return {
            (ScatteringTypeEnum.BRAGG, BeamModeEnum.CONSTANT_WAVELENGTH): cls.PSEUDO_VOIGT,
            (
                ScatteringTypeEnum.BRAGG,
                BeamModeEnum.TIME_OF_FLIGHT,
            ): cls.JORGENSEN,
            (ScatteringTypeEnum.TOTAL, BeamModeEnum.CONSTANT_WAVELENGTH): cls.GAUSSIAN_DAMPED_SINC,
            (ScatteringTypeEnum.TOTAL, BeamModeEnum.TIME_OF_FLIGHT): cls.GAUSSIAN_DAMPED_SINC,
        }[scattering_type, beam_mode]

    def description(self) -> str:  # noqa: PLR0911
        """
        Return a human-readable description of this peak profile type.

        Returns
        -------
        str
            Description string for the current enum member.
        """
        if self is PeakProfileTypeEnum.PSEUDO_VOIGT:
            return 'Pseudo-Voigt profile'
        if self is PeakProfileTypeEnum.PSEUDO_VOIGT_EMPIRICAL_ASYMMETRY:
            return 'Pseudo-Voigt profile with empirical asymmetry correction.'
        if self is PeakProfileTypeEnum.THOMPSON_COX_HASTINGS:
            return 'Thompson-Cox-Hastings profile with FCJ asymmetry correction.'
        if self is PeakProfileTypeEnum.JORGENSEN:
            return 'Jorgensen back-to-back exponentials convolved with Gaussian.'
        if self is PeakProfileTypeEnum.JORGENSEN_VON_DREELE:
            return 'Jorgensen-Von Dreele back-to-back exponentials convolved with pseudo-Voigt.'
        if self is PeakProfileTypeEnum.DOUBLE_JORGENSEN_VON_DREELE:
            return (
                'Double back-to-back exponentials convolved with pseudo-Voigt (Z-Rietveld type0m).'
            )
        if self is PeakProfileTypeEnum.GAUSSIAN_DAMPED_SINC:
            return 'Gaussian-damped sinc profile for pair distribution function (PDF) analysis.'
        return None
default(scattering_type=None, beam_mode=None) classmethod

Return the default peak profile type for a given mode.

Parameters:

Name Type Description Default
scattering_type ScatteringTypeEnum | None

Scattering type; defaults to ScatteringTypeEnum.default() when None.

None
beam_mode BeamModeEnum | None

Beam mode; defaults to BeamModeEnum.default() when None.

None

Returns:

Type Description
PeakProfileTypeEnum

The default profile type for the given combination.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
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
@classmethod
def default(
    cls,
    scattering_type: ScatteringTypeEnum | None = None,
    beam_mode: BeamModeEnum | None = None,
) -> 'PeakProfileTypeEnum':
    """
    Return the default peak profile type for a given mode.

    Parameters
    ----------
    scattering_type : ScatteringTypeEnum | None, default=None
        Scattering type; defaults to
        ``ScatteringTypeEnum.default()`` when ``None``.
    beam_mode : BeamModeEnum | None, default=None
        Beam mode; defaults to ``BeamModeEnum.default()`` when
        ``None``.

    Returns
    -------
    'PeakProfileTypeEnum'
        The default profile type for the given combination.
    """
    if scattering_type is None:
        scattering_type = ScatteringTypeEnum.default()
    if beam_mode is None:
        beam_mode = BeamModeEnum.default()
    return {
        (ScatteringTypeEnum.BRAGG, BeamModeEnum.CONSTANT_WAVELENGTH): cls.PSEUDO_VOIGT,
        (
            ScatteringTypeEnum.BRAGG,
            BeamModeEnum.TIME_OF_FLIGHT,
        ): cls.JORGENSEN,
        (ScatteringTypeEnum.TOTAL, BeamModeEnum.CONSTANT_WAVELENGTH): cls.GAUSSIAN_DAMPED_SINC,
        (ScatteringTypeEnum.TOTAL, BeamModeEnum.TIME_OF_FLIGHT): cls.GAUSSIAN_DAMPED_SINC,
    }[scattering_type, beam_mode]
description()

Return a human-readable description of this peak profile type.

Returns:

Type Description
str

Description string for the current enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
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
def description(self) -> str:  # noqa: PLR0911
    """
    Return a human-readable description of this peak profile type.

    Returns
    -------
    str
        Description string for the current enum member.
    """
    if self is PeakProfileTypeEnum.PSEUDO_VOIGT:
        return 'Pseudo-Voigt profile'
    if self is PeakProfileTypeEnum.PSEUDO_VOIGT_EMPIRICAL_ASYMMETRY:
        return 'Pseudo-Voigt profile with empirical asymmetry correction.'
    if self is PeakProfileTypeEnum.THOMPSON_COX_HASTINGS:
        return 'Thompson-Cox-Hastings profile with FCJ asymmetry correction.'
    if self is PeakProfileTypeEnum.JORGENSEN:
        return 'Jorgensen back-to-back exponentials convolved with Gaussian.'
    if self is PeakProfileTypeEnum.JORGENSEN_VON_DREELE:
        return 'Jorgensen-Von Dreele back-to-back exponentials convolved with pseudo-Voigt.'
    if self is PeakProfileTypeEnum.DOUBLE_JORGENSEN_VON_DREELE:
        return (
            'Double back-to-back exponentials convolved with pseudo-Voigt (Z-Rietveld type0m).'
        )
    if self is PeakProfileTypeEnum.GAUSSIAN_DAMPED_SINC:
        return 'Gaussian-damped sinc profile for pair distribution function (PDF) analysis.'
    return None

RadiationProbeEnum

Bases: StrEnum

Incident radiation probe used in the experiment.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
class RadiationProbeEnum(StrEnum):
    """Incident radiation probe used in the experiment."""

    NEUTRON = 'neutron'
    XRAY = 'xray'

    @classmethod
    def default(cls) -> 'RadiationProbeEnum':
        """
        Return the default radiation probe (NEUTRON).

        Returns
        -------
        'RadiationProbeEnum'
            The default enum member.
        """
        return cls.NEUTRON

    def description(self) -> str:
        """
        Return a human-readable description of this radiation probe.

        Returns
        -------
        str
            Description string for the current enum member.
        """
        if self is RadiationProbeEnum.NEUTRON:
            return 'Neutron diffraction.'
        if self is RadiationProbeEnum.XRAY:
            return 'X-ray diffraction.'
        return None
default() classmethod

Return the default radiation probe (NEUTRON).

Returns:

Type Description
RadiationProbeEnum

The default enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
82
83
84
85
86
87
88
89
90
91
92
@classmethod
def default(cls) -> 'RadiationProbeEnum':
    """
    Return the default radiation probe (NEUTRON).

    Returns
    -------
    'RadiationProbeEnum'
        The default enum member.
    """
    return cls.NEUTRON
description()

Return a human-readable description of this radiation probe.

Returns:

Type Description
str

Description string for the current enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def description(self) -> str:
    """
    Return a human-readable description of this radiation probe.

    Returns
    -------
    str
        Description string for the current enum member.
    """
    if self is RadiationProbeEnum.NEUTRON:
        return 'Neutron diffraction.'
    if self is RadiationProbeEnum.XRAY:
        return 'X-ray diffraction.'
    return None

SampleFormEnum

Bases: StrEnum

Physical sample form supported by experiments.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class SampleFormEnum(StrEnum):
    """Physical sample form supported by experiments."""

    POWDER = 'powder'
    SINGLE_CRYSTAL = 'single crystal'

    @classmethod
    def default(cls) -> 'SampleFormEnum':
        """
        Return the default sample form (POWDER).

        Returns
        -------
        'SampleFormEnum'
            The default enum member.
        """
        return cls.POWDER

    def description(self) -> str:
        """
        Return a human-readable description of this sample form.

        Returns
        -------
        str
            Description string for the current enum member.
        """
        if self is SampleFormEnum.POWDER:
            return 'Powdered or polycrystalline sample.'
        if self is SampleFormEnum.SINGLE_CRYSTAL:
            return 'Single crystal sample.'
        return None
default() classmethod

Return the default sample form (POWDER).

Returns:

Type Description
SampleFormEnum

The default enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
14
15
16
17
18
19
20
21
22
23
24
@classmethod
def default(cls) -> 'SampleFormEnum':
    """
    Return the default sample form (POWDER).

    Returns
    -------
    'SampleFormEnum'
        The default enum member.
    """
    return cls.POWDER
description()

Return a human-readable description of this sample form.

Returns:

Type Description
str

Description string for the current enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def description(self) -> str:
    """
    Return a human-readable description of this sample form.

    Returns
    -------
    str
        Description string for the current enum member.
    """
    if self is SampleFormEnum.POWDER:
        return 'Powdered or polycrystalline sample.'
    if self is SampleFormEnum.SINGLE_CRYSTAL:
        return 'Single crystal sample.'
    return None

ScatteringTypeEnum

Bases: StrEnum

Type of scattering modeled in an experiment.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class ScatteringTypeEnum(StrEnum):
    """Type of scattering modeled in an experiment."""

    BRAGG = 'bragg'
    TOTAL = 'total'

    @classmethod
    def default(cls) -> 'ScatteringTypeEnum':
        """
        Return the default scattering type (BRAGG).

        Returns
        -------
        'ScatteringTypeEnum'
            The default enum member.
        """
        return cls.BRAGG

    def description(self) -> str:
        """
        Return a human-readable description of this scattering type.

        Returns
        -------
        str
            Description string for the current enum member.
        """
        if self is ScatteringTypeEnum.BRAGG:
            return 'Bragg diffraction for conventional structure refinement.'
        if self is ScatteringTypeEnum.TOTAL:
            return 'Total scattering for pair distribution function analysis (PDF).'
        return None
default() classmethod

Return the default scattering type (BRAGG).

Returns:

Type Description
ScatteringTypeEnum

The default enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
48
49
50
51
52
53
54
55
56
57
58
@classmethod
def default(cls) -> 'ScatteringTypeEnum':
    """
    Return the default scattering type (BRAGG).

    Returns
    -------
    'ScatteringTypeEnum'
        The default enum member.
    """
    return cls.BRAGG
description()

Return a human-readable description of this scattering type.

Returns:

Type Description
str

Description string for the current enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def description(self) -> str:
    """
    Return a human-readable description of this scattering type.

    Returns
    -------
    str
        Description string for the current enum member.
    """
    if self is ScatteringTypeEnum.BRAGG:
        return 'Bragg diffraction for conventional structure refinement.'
    if self is ScatteringTypeEnum.TOTAL:
        return 'Total scattering for pair distribution function analysis (PDF).'
    return None

factory

Factory for creating experiment instances from various inputs.

Provides individual class methods for each creation pathway: from_cif_path, from_cif_str, from_data_path, and from_scratch.

ExperimentFactory

Bases: FactoryBase

Creates Experiment instances with only relevant attributes.

Source code in src/easydiffraction/datablocks/experiment/item/factory.py
 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
class ExperimentFactory(FactoryBase):
    """Creates Experiment instances with only relevant attributes."""

    _default_rules: ClassVar[dict] = {
        frozenset({
            ('scattering_type', ScatteringTypeEnum.BRAGG),
            ('sample_form', SampleFormEnum.POWDER),
        }): 'bragg-pd',
        frozenset({
            ('scattering_type', ScatteringTypeEnum.TOTAL),
            ('sample_form', SampleFormEnum.POWDER),
        }): 'total-pd',
        frozenset({
            ('scattering_type', ScatteringTypeEnum.BRAGG),
            ('sample_form', SampleFormEnum.SINGLE_CRYSTAL),
            ('beam_mode', BeamModeEnum.CONSTANT_WAVELENGTH),
        }): 'bragg-sc-cwl',
        frozenset({
            ('scattering_type', ScatteringTypeEnum.BRAGG),
            ('sample_form', SampleFormEnum.SINGLE_CRYSTAL),
            ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT),
        }): 'bragg-sc-tof',
    }

    # TODO: Add to core/factory.py?
    def __init__(self) -> None:
        log.error(
            'Experiment objects must be created using class methods such as '
            '`ExperimentFactory.from_cif_str(...)`, etc.'
        )

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

    @classmethod
    @typechecked
    def _create_experiment_type(
        cls,
        sample_form: str | None = None,
        beam_mode: str | None = None,
        radiation_probe: str | None = None,
        scattering_type: str | None = None,
    ) -> ExperimentType:
        """Construct ExperimentType with defaults for omitted values."""
        # Note: validation of input values is done via Descriptor setter
        # methods

        et = ExperimentType()

        if sample_form is not None:
            et._set_sample_form(sample_form)
        if beam_mode is not None:
            et._set_beam_mode(beam_mode)
        if radiation_probe is not None:
            et._set_radiation_probe(radiation_probe)
        if scattering_type is not None:
            et._set_scattering_type(scattering_type)

        return et

    @classmethod
    @typechecked
    def _resolve_class(cls, expt_type: ExperimentType) -> type:
        """Look up the experiment class from the type enums."""
        tag = cls.default_tag(
            scattering_type=expt_type.scattering_type.value,
            sample_form=expt_type.sample_form.value,
            beam_mode=expt_type.beam_mode.value,
        )
        return cls._supported_map()[tag]

    @classmethod
    # TODO: @typechecked fails to find gemmi?
    def _from_gemmi_block(
        cls,
        block: gemmi.cif.Block,
    ) -> ExperimentBase:
        """Build a model instance from a single CIF block."""
        name = name_from_block(block)

        expt_type = ExperimentType()
        for param in expt_type.parameters:
            param.from_cif(block)

        expt_class = cls._resolve_class(expt_type)
        expt_obj = expt_class(name=name, type=expt_type)

        # Restore switchable category types before loading parameters
        # so implementation-specific descriptors exist for from_cif.
        expt_obj._restore_switchable_types(block)

        for category in expt_obj.categories:
            category.from_cif(block)

        return expt_obj

    # ------------------------------------------------------------------
    # Public methods
    # ------------------------------------------------------------------

    @classmethod
    @typechecked
    def from_scratch(
        cls,
        *,
        name: str,
        sample_form: str | None = None,
        beam_mode: str | None = None,
        radiation_probe: str | None = None,
        scattering_type: str | None = None,
    ) -> ExperimentBase:
        """
        Create an experiment without measured data.

        Parameters
        ----------
        name : str
            Experiment identifier.
        sample_form : str | None, default=None
            Sample form (e.g. ``'powder'``).
        beam_mode : str | None, default=None
            Beam mode (e.g. ``'constant wavelength'``).
        radiation_probe : str | None, default=None
            Radiation probe (e.g. ``'neutron'``).
        scattering_type : str | None, default=None
            Scattering type (e.g. ``'bragg'``).

        Returns
        -------
        ExperimentBase
            An experiment instance with only metadata.
        """
        expt_type = cls._create_experiment_type(
            sample_form=sample_form,
            beam_mode=beam_mode,
            radiation_probe=radiation_probe,
            scattering_type=scattering_type,
        )
        expt_class = cls._resolve_class(expt_type)
        return expt_class(name=name, type=expt_type)

    # TODO: add minimal default configuration for missing parameters
    @classmethod
    @typechecked
    def from_cif_str(
        cls,
        cif_str: str,
    ) -> ExperimentBase:
        """
        Create an experiment from a CIF string.

        Parameters
        ----------
        cif_str : str
            Full CIF document as a string.

        Returns
        -------
        ExperimentBase
            A populated experiment instance.
        """
        doc = document_from_string(cif_str)
        block = pick_sole_block(doc)
        return cls._from_gemmi_block(block)

    # TODO: Read content and call self.from_cif_str
    @classmethod
    @typechecked
    def from_cif_path(
        cls,
        cif_path: str,
    ) -> ExperimentBase:
        """
        Create an experiment from a CIF file path.

        Parameters
        ----------
        cif_path : str
            Path to a CIF file.

        Returns
        -------
        ExperimentBase
            A populated experiment instance.
        """
        doc = document_from_path(cif_path)
        block = pick_sole_block(doc)
        return cls._from_gemmi_block(block)

    @classmethod
    @typechecked
    def from_data_path(
        cls,
        *,
        name: str,
        data_path: str,
        sample_form: str | None = None,
        beam_mode: str | None = None,
        radiation_probe: str | None = None,
        scattering_type: str | None = None,
    ) -> ExperimentBase:
        """
        Create an experiment from a raw data ASCII file.

        Parameters
        ----------
        name : str
            Experiment identifier.
        data_path : str
            Path to the measured data file.
        sample_form : str | None, default=None
            Sample form (e.g. ``'powder'``).
        beam_mode : str | None, default=None
            Beam mode (e.g. ``'constant wavelength'``).
        radiation_probe : str | None, default=None
            Radiation probe (e.g. ``'neutron'``).
        scattering_type : str | None, default=None
            Scattering type (e.g. ``'bragg'``).

        Returns
        -------
        ExperimentBase
            An experiment instance with measured data attached.
        """
        expt_obj = cls.from_scratch(
            name=name,
            sample_form=sample_form,
            beam_mode=beam_mode,
            radiation_probe=radiation_probe,
            scattering_type=scattering_type,
        )

        expt_obj._load_ascii_data_to_experiment(data_path)

        return expt_obj
from_cif_path(cif_path) classmethod

Create an experiment from a CIF file path.

Parameters:

Name Type Description Default
cif_path str

Path to a CIF file.

required

Returns:

Type Description
ExperimentBase

A populated experiment instance.

Source code in src/easydiffraction/datablocks/experiment/item/factory.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
@classmethod
@typechecked
def from_cif_path(
    cls,
    cif_path: str,
) -> ExperimentBase:
    """
    Create an experiment from a CIF file path.

    Parameters
    ----------
    cif_path : str
        Path to a CIF file.

    Returns
    -------
    ExperimentBase
        A populated experiment instance.
    """
    doc = document_from_path(cif_path)
    block = pick_sole_block(doc)
    return cls._from_gemmi_block(block)
from_cif_str(cif_str) classmethod

Create an experiment from a CIF string.

Parameters:

Name Type Description Default
cif_str str

Full CIF document as a string.

required

Returns:

Type Description
ExperimentBase

A populated experiment instance.

Source code in src/easydiffraction/datablocks/experiment/item/factory.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
@classmethod
@typechecked
def from_cif_str(
    cls,
    cif_str: str,
) -> ExperimentBase:
    """
    Create an experiment from a CIF string.

    Parameters
    ----------
    cif_str : str
        Full CIF document as a string.

    Returns
    -------
    ExperimentBase
        A populated experiment instance.
    """
    doc = document_from_string(cif_str)
    block = pick_sole_block(doc)
    return cls._from_gemmi_block(block)
from_data_path(*, name, data_path, sample_form=None, beam_mode=None, radiation_probe=None, scattering_type=None) classmethod

Create an experiment from a raw data ASCII file.

Parameters:

Name Type Description Default
name str

Experiment identifier.

required
data_path str

Path to the measured data file.

required
sample_form str | None

Sample form (e.g. 'powder').

None
beam_mode str | None

Beam mode (e.g. 'constant wavelength').

None
radiation_probe str | None

Radiation probe (e.g. 'neutron').

None
scattering_type str | None

Scattering type (e.g. 'bragg').

None

Returns:

Type Description
ExperimentBase

An experiment instance with measured data attached.

Source code in src/easydiffraction/datablocks/experiment/item/factory.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
@classmethod
@typechecked
def from_data_path(
    cls,
    *,
    name: str,
    data_path: str,
    sample_form: str | None = None,
    beam_mode: str | None = None,
    radiation_probe: str | None = None,
    scattering_type: str | None = None,
) -> ExperimentBase:
    """
    Create an experiment from a raw data ASCII file.

    Parameters
    ----------
    name : str
        Experiment identifier.
    data_path : str
        Path to the measured data file.
    sample_form : str | None, default=None
        Sample form (e.g. ``'powder'``).
    beam_mode : str | None, default=None
        Beam mode (e.g. ``'constant wavelength'``).
    radiation_probe : str | None, default=None
        Radiation probe (e.g. ``'neutron'``).
    scattering_type : str | None, default=None
        Scattering type (e.g. ``'bragg'``).

    Returns
    -------
    ExperimentBase
        An experiment instance with measured data attached.
    """
    expt_obj = cls.from_scratch(
        name=name,
        sample_form=sample_form,
        beam_mode=beam_mode,
        radiation_probe=radiation_probe,
        scattering_type=scattering_type,
    )

    expt_obj._load_ascii_data_to_experiment(data_path)

    return expt_obj
from_scratch(*, name, sample_form=None, beam_mode=None, radiation_probe=None, scattering_type=None) classmethod

Create an experiment without measured data.

Parameters:

Name Type Description Default
name str

Experiment identifier.

required
sample_form str | None

Sample form (e.g. 'powder').

None
beam_mode str | None

Beam mode (e.g. 'constant wavelength').

None
radiation_probe str | None

Radiation probe (e.g. 'neutron').

None
scattering_type str | None

Scattering type (e.g. 'bragg').

None

Returns:

Type Description
ExperimentBase

An experiment instance with only metadata.

Source code in src/easydiffraction/datablocks/experiment/item/factory.py
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
@classmethod
@typechecked
def from_scratch(
    cls,
    *,
    name: str,
    sample_form: str | None = None,
    beam_mode: str | None = None,
    radiation_probe: str | None = None,
    scattering_type: str | None = None,
) -> ExperimentBase:
    """
    Create an experiment without measured data.

    Parameters
    ----------
    name : str
        Experiment identifier.
    sample_form : str | None, default=None
        Sample form (e.g. ``'powder'``).
    beam_mode : str | None, default=None
        Beam mode (e.g. ``'constant wavelength'``).
    radiation_probe : str | None, default=None
        Radiation probe (e.g. ``'neutron'``).
    scattering_type : str | None, default=None
        Scattering type (e.g. ``'bragg'``).

    Returns
    -------
    ExperimentBase
        An experiment instance with only metadata.
    """
    expt_type = cls._create_experiment_type(
        sample_form=sample_form,
        beam_mode=beam_mode,
        radiation_probe=radiation_probe,
        scattering_type=scattering_type,
    )
    expt_class = cls._resolve_class(expt_type)
    return expt_class(name=name, type=expt_type)

total_pd

TotalPdExperiment

Bases: PdExperimentBase

PDF experiment class with specific attributes.

Source code in src/easydiffraction/datablocks/experiment/item/total_pd.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
@ExperimentFactory.register
class TotalPdExperiment(PdExperimentBase):
    """PDF experiment class with specific attributes."""

    type_info = TypeInfo(
        tag='total-pd',
        description='Total scattering (PDF) powder experiment',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.TOTAL}),
        sample_form=frozenset({SampleFormEnum.POWDER}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}),
    )

    def __init__(
        self,
        name: str,
        type: ExperimentType,
    ) -> None:
        super().__init__(name=name, type=type)

    def _load_ascii_data_to_experiment(self, data_path: str) -> int:
        """
        Load x, y, sy values from an ASCII file into the experiment.

        The file must be structured as:     x  y  sy

        Parameters
        ----------
        data_path : str
            Path to the ASCII data file.

        Returns
        -------
        int
            Number of loaded data points.

        Raises
        ------
        ImportError
            If the ``diffpy`` package is not installed.
        OSError
            If the data file cannot be read.
        ValueError
            If the data file has fewer than two columns.
        """
        try:
            from diffpy.utils.parsers import load_data  # noqa: PLC0415
        except ImportError:
            msg = 'diffpy module not found.'
            raise ImportError(msg) from None
        try:
            data = load_data(data_path)
        except Exception as e:
            msg = f'Failed to read data from {data_path}: {e}'
            raise OSError(msg) from e

        if data.shape[1] < _MIN_COLUMNS_XY:
            msg = 'Data file must have at least two columns: x and y.'
            raise ValueError(msg)

        default_sy = 0.03
        if data.shape[1] < _MIN_COLUMNS_XY_SY:
            log.warning(f'No uncertainty (sy) column provided. Defaulting to {default_sy}.')

        x = data[:, 0]
        y = data[:, 1]
        sy = (
            data[:, 2]
            if data.shape[1] > _MIN_COLUMNS_XY
            else np.full_like(y, fill_value=default_sy)
        )

        self.data._create_items_set_xcoord_and_id(x)
        self.data._set_g_r_meas(y)
        self.data._set_g_r_meas_su(sy)

        return len(x)