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
23
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."""
        pass
show() abstractmethod

Print a human-readable view of background components.

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

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
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
@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,
            CalculatorEnum.CRYSFML,
        }),
    )

    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[Union[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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
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[Union[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
 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 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: str, Enum

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
class BackgroundTypeEnum(str, Enum):
    """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'
        elif self is BackgroundTypeEnum.CHEBYSHEV:
            return 'Chebyshev polynomial background'
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
def description(self) -> str:
    """Human-friendly description for the enum value."""
    if self is BackgroundTypeEnum.LINE_SEGMENT:
        return 'Linear interpolation between points'
    elif self is BackgroundTypeEnum.CHEBYSHEV:
        return 'Chebyshev polynomial background'

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
 9
10
11
12
13
14
class BackgroundFactory(FactoryBase):
    """Create background collections by tag."""

    _default_rules = {
        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
 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
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
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
@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
178
179
180
181
182
183
184
185
186
187
188
189
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
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
@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}),
    )

    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)

        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
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
class 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
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
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:
                raise ValueError(
                    f'Invalid refinement status value: {v}. Expected boolean True/False.'
                )

    @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 0.0001 with 1.0
        modified = np.where(original < 0.0001, 1.0, original)
        return modified

    @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
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
class 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
551
552
553
554
555
556
557
558
559
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
@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)

        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
285
286
287
288
289
290
291
292
293
294
295
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
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
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
@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
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class DataFactory(FactoryBase):
    """Factory for creating diffraction data collections."""

    _default_rules = {
        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
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
@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
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:
                raise ValueError(
                    f'Invalid calculation status value: {v}. Expected boolean True/False.'
                )

    @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
10
11
12
13
14
15
class DiffrnFactory(FactoryBase):
    """Create diffraction ambient-conditions category instances."""

    _default_rules = {
        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
 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
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
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
@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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
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
12
13
14
15
16
17
class ExcludedRegionsFactory(FactoryBase):
    """Create excluded-regions collections by tag."""

    _default_rules = {
        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
10
11
12
13
14
15
class ExperimentTypeFactory(FactoryBase):
    """Create experiment-type descriptors by tag."""

    _default_rules = {
        frozenset(): 'default',
    }

extinction

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
10
11
12
13
14
15
class ExtinctionFactory(FactoryBase):
    """Create extinction correction models by tag."""

    _default_rules = {
        frozenset(): 'shelx',
    }

shelx

Shelx-style isotropic extinction correction.

ShelxExtinction

Bases: CategoryItem

Shelx-style extinction correction for single crystals.

Source code in src/easydiffraction/datablocks/experiment/categories/extinction/shelx.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@ExtinctionFactory.register
class ShelxExtinction(CategoryItem):
    """Shelx-style extinction correction for single crystals."""

    type_info = TypeInfo(
        tag='shelx',
        description='Shelx-style isotropic extinction correction',
    )
    compatibility = Compatibility(
        sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}),
    )

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

        self._mosaicity = Parameter(
            name='mosaicity',
            description='Mosaicity value for extinction correction',
            units='deg',
            value_spec=AttributeSpec(
                default=1.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(
                names=[
                    '_extinction.mosaicity',
                ]
            ),
        )
        self._radius = Parameter(
            name='radius',
            description='Crystal radius for extinction correction',
            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 mosaicity(self) -> Parameter:
        """
        Mosaicity value for extinction correction (deg).

        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:
        """
        Crystal radius for extinction correction (µ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
mosaicity property writable

Mosaicity value for extinction correction (deg).

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

radius property writable

Crystal radius for extinction correction (µm).

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

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(),
            ),
            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
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class InstrumentFactory(FactoryBase):
    """Create instrument instances for supported modes."""

    _default_rules = {
        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
10
11
12
13
14
15
class LinkedCrystalFactory(FactoryBase):
    """Create linked-crystal references by tag."""

    _default_rules = {
        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(),
            ),
            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
10
11
12
13
14
15
class LinkedPhasesFactory(FactoryBase):
    """Create linked-phases collections by tag."""

    _default_rules = {
        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
 8
 9
10
11
12
13
class PeakBase(CategoryItem):
    """Base class for peak profile categories."""

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

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__()
CwlSplitPseudoVoigt

Bases: PeakBase, CwlBroadeningMixin, EmpiricalAsymmetryMixin

Split pseudo-Voigt (empirical asymmetry) 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 CwlSplitPseudoVoigt(
    PeakBase,
    CwlBroadeningMixin,
    EmpiricalAsymmetryMixin,
):
    """Split pseudo-Voigt (empirical asymmetry) for CWL mode."""

    type_info = TypeInfo(
        tag='split pseudo-voigt',
        description='Split 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, CalculatorEnum.CRYSFML}),
    )

    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.CRYSPY, 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.1,
                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.2,
                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.3,
                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.4,
                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
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class PeakFactory(FactoryBase):
    """Factory for creating peak profile objects."""

    _default_rules = {
        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.PSEUDO_VOIGT_IKEDA_CARPENTER,
        frozenset({
            ('scattering_type', ScatteringTypeEnum.TOTAL),
        }): PeakProfileTypeEnum.GAUSSIAN_DAMPED_SINC,
    }

tof

Time-of-flight peak profile classes.

TofPseudoVoigt

Bases: PeakBase, TofBroadeningMixin

Time-of-flight pseudo-Voigt peak shape.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@PeakFactory.register
class TofPseudoVoigt(
    PeakBase,
    TofBroadeningMixin,
):
    """Time-of-flight pseudo-Voigt peak shape."""

    type_info = TypeInfo(
        tag='tof-pseudo-voigt',
        description='TOF 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__()
TofPseudoVoigtBackToBack

Bases: PeakBase, TofBroadeningMixin, IkedaCarpenterAsymmetryMixin

TOF back-to-back pseudo-Voigt with asymmetry.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
@PeakFactory.register
class TofPseudoVoigtBackToBack(
    PeakBase,
    TofBroadeningMixin,
    IkedaCarpenterAsymmetryMixin,
):
    """TOF back-to-back pseudo-Voigt with asymmetry."""

    type_info = TypeInfo(
        tag='pseudo-voigt * back-to-back',
        description='TOF back-to-back pseudo-Voigt with asymmetry',
    )
    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__()
TofPseudoVoigtIkedaCarpenter

Bases: PeakBase, TofBroadeningMixin, IkedaCarpenterAsymmetryMixin

TOF pseudo-Voigt with Ikeda–Carpenter asymmetry.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@PeakFactory.register
class TofPseudoVoigtIkedaCarpenter(
    PeakBase,
    TofBroadeningMixin,
    IkedaCarpenterAsymmetryMixin,
):
    """TOF pseudo-Voigt with Ikeda–Carpenter asymmetry."""

    type_info = TypeInfo(
        tag='pseudo-voigt * ikeda-carpenter',
        description='Pseudo-Voigt with Ikeda-Carpenter asymmetry correction',
    )
    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 component classes.

Defines classes that add Gaussian/Lorentz broadening, mixing, and Ikeda–Carpenter asymmetry parameters used by TOF peak shapes. This module provides classes that add broadening and asymmetry parameters. They are composed into concrete peak classes elsewhere via multiple inheritance.

IkedaCarpenterAsymmetryMixin

Ikeda–Carpenter asymmetry parameters.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.py
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
class IkedaCarpenterAsymmetryMixin:
    """Ikeda–Carpenter asymmetry parameters."""

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

        self._asym_alpha_0 = Parameter(
            name='asym_alpha_0',
            description='Ikeda-Carpenter asymmetry parameter α₀',
            units='',  # TODO
            value_spec=AttributeSpec(
                default=0.01,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.asym_alpha_0']),
        )
        self._asym_alpha_1 = Parameter(
            name='asym_alpha_1',
            description='Ikeda-Carpenter asymmetry parameter α₁',
            units='',  # TODO
            value_spec=AttributeSpec(
                default=0.02,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.asym_alpha_1']),
        )

    @property
    def asym_alpha_0(self) -> Parameter:
        """
        Ikeda-Carpenter asymmetry parameter α₀.

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

    @asym_alpha_0.setter
    def asym_alpha_0(self, value: float) -> None:
        self._asym_alpha_0.value = value

    @property
    def asym_alpha_1(self) -> Parameter:
        """
        Ikeda-Carpenter asymmetry parameter α₁.

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

    @asym_alpha_1.setter
    def asym_alpha_1(self, value: float) -> None:
        self._asym_alpha_1.value = value
asym_alpha_0 property writable

Ikeda-Carpenter asymmetry parameter α₀.

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

asym_alpha_1 property writable

Ikeda-Carpenter asymmetry parameter α₁.

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

TofBroadeningMixin

TOF Gaussian/Lorentz broadening and mixing parameters.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
class TofBroadeningMixin:
    """TOF Gaussian/Lorentz broadening and mixing 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']),
        )
        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']),
        )
        self._broad_mix_beta_0 = Parameter(
            name='mix_beta_0',
            description='Ratio of Gaussian to Lorentzian contributions',
            units='deg',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.mix_beta_0']),
        )
        self._broad_mix_beta_1 = Parameter(
            name='mix_beta_1',
            description='Ratio of Gaussian to Lorentzian contributions',
            units='deg',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_peak.mix_beta_1']),
        )

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

    @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

    @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

    @property
    def broad_mix_beta_0(self) -> Parameter:
        """
        Ratio of Gaussian to Lorentzian contributions (deg).

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

    @broad_mix_beta_0.setter
    def broad_mix_beta_0(self, value: float) -> None:
        self._broad_mix_beta_0.value = value

    @property
    def broad_mix_beta_1(self) -> Parameter:
        """
        Ratio of Gaussian to Lorentzian contributions (deg).

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

    @broad_mix_beta_1.setter
    def broad_mix_beta_1(self, value: float) -> None:
        self._broad_mix_beta_1.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.

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.

broad_mix_beta_0 property writable

Ratio of Gaussian to Lorentzian contributions (deg).

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

broad_mix_beta_1 property writable

Ratio of Gaussian to Lorentzian contributions (deg).

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
156
157
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,
        verbosity: 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 : str | None, default=None
            Console output verbosity: ``'full'`` for multi-line output,
            ``'short'`` for a one-line status message, or ``'silent'``
            for no output. When ``None``, uses ``project.verbosity``.
        """
        if verbosity is None and self._parent is not None:
            verbosity = self._parent.verbosity
        verb = VerbosityEnum(verbosity) if verbosity is not None else VerbosityEnum.FULL
        experiment = ExperimentFactory.from_data_path(
            name=name,
            data_path=data_path,
            sample_form=sample_form,
            beam_mode=beam_mode,
            radiation_probe=radiation_probe,
            scattering_type=scattering_type,
            verbosity=verb,
        )
        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, verbosity=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
verbosity str | None

Console output verbosity: 'full' for multi-line output, 'short' for a one-line status message, or 'silent' for no output. When None, uses project.verbosity.

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
144
145
@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,
    verbosity: 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 : str | None, default=None
        Console output verbosity: ``'full'`` for multi-line output,
        ``'short'`` for a one-line status message, or ``'silent'``
        for no output. When ``None``, uses ``project.verbosity``.
    """
    if verbosity is None and self._parent is not None:
        verbosity = self._parent.verbosity
    verb = VerbosityEnum(verbosity) if verbosity is not None else VerbosityEnum.FULL
    experiment = ExperimentFactory.from_data_path(
        name=name,
        data_path=data_path,
        sample_form=sample_form,
        beam_mode=beam_mode,
        radiation_probe=radiation_probe,
        scattering_type=scattering_type,
        verbosity=verb,
    )
    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
148
149
150
151
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
154
155
156
157
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
 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
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 (switchable-category pattern)
    # ------------------------------------------------------------------

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

    @property
    def diffrn_type(self) -> str:
        """Tag of the active diffraction conditions type."""
        return self._diffrn_type

    @diffrn_type.setter
    def diffrn_type(self, new_type: str) -> None:
        """
        Switch to a different diffraction conditions type.

        Parameters
        ----------
        new_type : str
            Diffrn conditions tag (e.g. ``'default'``).
        """
        supported_tags = DiffrnFactory.supported_tags()
        if new_type not in supported_tags:
            log.warning(
                f"Unsupported diffrn type '{new_type}'. "
                f'Supported: {supported_tags}. '
                f"For more information, use 'show_supported_diffrn_types()'",
            )
            return

        self._diffrn = DiffrnFactory.create(new_type)
        self._diffrn_type = new_type
        console.paragraph(f"Diffrn type for experiment '{self.name}' changed to")
        console.print(new_type)

    def show_supported_diffrn_types(self) -> None:
        """Print a table of supported diffraction conditions types."""
        DiffrnFactory.show_supported()

    def show_current_diffrn_type(self) -> None:
        """Print the currently used diffraction conditions type."""
        console.paragraph('Current diffrn type')
        console.print(self.diffrn_type)

    @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."""
        experiment_cif = super().as_cif
        paragraph_title: str = f"Experiment 🔬 '{self.name}' as cif"
        console.paragraph(paragraph_title)
        render_cif(experiment_cif)

    @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

        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

        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
        ]
        from easydiffraction.utils.utils import render_table

        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

        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

        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.

diffrn_type property writable

Tag of the active diffraction conditions type.

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
130
131
132
133
134
135
def show_as_cif(self) -> None:
    """Pretty-print the experiment as CIF text."""
    experiment_cif = super().as_cif
    paragraph_title: str = f"Experiment 🔬 '{self.name}' as cif"
    console.paragraph(paragraph_title)
    render_cif(experiment_cif)
show_current_calculator_type()

Print the name of the currently active calculator.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
226
227
228
229
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_current_diffrn_type()

Print the currently used diffraction conditions type.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
120
121
122
123
def show_current_diffrn_type(self) -> None:
    """Print the currently used diffraction conditions type."""
    console.paragraph('Current diffrn type')
    console.print(self.diffrn_type)
show_supported_calculator_types()

Print a table of supported calculator backends.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
def show_supported_calculator_types(self) -> None:
    """Print a table of supported calculator backends."""
    from easydiffraction.analysis.calculators.factory import CalculatorFactory

    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
    ]
    from easydiffraction.utils.utils import render_table

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

Print a table of supported diffraction conditions types.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
116
117
118
def show_supported_diffrn_types(self) -> None:
    """Print a table of supported diffraction conditions types."""
    DiffrnFactory.show_supported()
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
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
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
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
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. 2θ/I/σ for CWL, TOF/I/σ for TOF).

        Returns
        -------
        int
            Number of loaded data points.
        """
        pass

    @property
    def linked_phases(self) -> object:
        """Collection of phases linked to this experiment."""
        return self._linked_phases

    @property
    def linked_phases_type(self) -> str:
        """Tag of the active linked-phases collection type."""
        return self._linked_phases_type

    @linked_phases_type.setter
    def linked_phases_type(self, new_type: str) -> None:
        """
        Switch to a different linked-phases collection type.

        Parameters
        ----------
        new_type : str
            Linked-phases tag (e.g. ``'default'``).
        """
        supported_tags = LinkedPhasesFactory.supported_tags()
        if new_type not in supported_tags:
            log.warning(
                f"Unsupported linked phases type '{new_type}'. "
                f'Supported: {supported_tags}. '
                f"For more information, use 'show_supported_linked_phases_types()'",
            )
            return

        self._linked_phases = LinkedPhasesFactory.create(new_type)
        self._linked_phases_type = new_type
        console.paragraph(f"Linked phases type for experiment '{self.name}' changed to")
        console.print(new_type)

    def show_supported_linked_phases_types(self) -> None:
        """Print a table of supported linked-phases collection types."""
        LinkedPhasesFactory.show_supported()

    def show_current_linked_phases_type(self) -> None:
        """Print the currently used linked-phases collection type."""
        console.paragraph('Current linked phases type')
        console.print(self.linked_phases_type)

    @property
    def excluded_regions(self) -> object:
        """Collection of excluded regions for the x-grid."""
        return self._excluded_regions

    @property
    def excluded_regions_type(self) -> str:
        """Tag of the active excluded-regions collection type."""
        return self._excluded_regions_type

    @excluded_regions_type.setter
    def excluded_regions_type(self, new_type: str) -> None:
        """
        Switch to a different excluded-regions collection type.

        Parameters
        ----------
        new_type : str
            Excluded-regions tag (e.g. ``'default'``).
        """
        supported_tags = ExcludedRegionsFactory.supported_tags()
        if new_type not in supported_tags:
            log.warning(
                f"Unsupported excluded regions type '{new_type}'. "
                f'Supported: {supported_tags}. '
                f"For more information, use 'show_supported_excluded_regions_types()'",
            )
            return

        self._excluded_regions = ExcludedRegionsFactory.create(new_type)
        self._excluded_regions_type = new_type
        console.paragraph(f"Excluded regions type for experiment '{self.name}' changed to")
        console.print(new_type)

    def show_supported_excluded_regions_types(self) -> None:
        """Print a table of supported excluded-regions types."""
        ExcludedRegionsFactory.show_supported()

    def show_current_excluded_regions_type(self) -> None:
        """Print the currently used excluded-regions collection type."""
        console.paragraph('Current excluded regions type')
        console.print(self.excluded_regions_type)

    # ------------------------------------------------------------------
    #  Data (switchable-category pattern)
    # ------------------------------------------------------------------

    @property
    def data(self) -> object:
        """Data collection for this experiment."""
        return self._data

    @property
    def data_type(self) -> str:
        """Tag of the active data collection type."""
        return self._data_type

    @data_type.setter
    def data_type(self, new_type: str) -> None:
        """
        Switch to a different data collection type.

        Parameters
        ----------
        new_type : str
            Data tag (e.g. ``'bragg-pd-cwl'``).
        """
        supported_tags = DataFactory.supported_tags()
        if new_type not in supported_tags:
            log.warning(
                f"Unsupported data type '{new_type}'. "
                f'Supported: {supported_tags}. '
                f"For more information, use 'show_supported_data_types()'",
            )
            return
        self._data = DataFactory.create(new_type)
        self._data_type = new_type
        console.paragraph(f"Data type for experiment '{self.name}' changed to")
        console.print(new_type)

    def show_supported_data_types(self) -> None:
        """Print a table of supported data collection types."""
        DataFactory.show_supported()

    def show_current_data_type(self) -> None:
        """Print the currently used data collection type."""
        console.paragraph('Current data type')
        console.print(self.data_type)

    @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(
            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(
            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)
data property

Data collection for this experiment.

data_type property writable

Tag of the active data collection type.

excluded_regions property

Collection of excluded regions for the x-grid.

excluded_regions_type property writable

Tag of the active excluded-regions collection type.

linked_phases property

Collection of phases linked to this experiment.

linked_phases_type property writable

Tag of the active linked-phases collection type.

peak property

Peak category object with profile parameters and mixins.

peak_profile_type property writable

Currently selected peak profile type enum.

show_current_data_type()

Print the currently used data collection type.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
709
710
711
712
def show_current_data_type(self) -> None:
    """Print the currently used data collection type."""
    console.paragraph('Current data type')
    console.print(self.data_type)
show_current_excluded_regions_type()

Print the currently used excluded-regions collection type.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
663
664
665
666
def show_current_excluded_regions_type(self) -> None:
    """Print the currently used excluded-regions collection type."""
    console.paragraph('Current excluded regions type')
    console.print(self.excluded_regions_type)
show_current_linked_phases_type()

Print the currently used linked-phases collection type.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
620
621
622
623
def show_current_linked_phases_type(self) -> None:
    """Print the currently used linked-phases collection type."""
    console.paragraph('Current linked phases type')
    console.print(self.linked_phases_type)
show_current_peak_profile_type()

Print the currently selected peak profile type.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
765
766
767
768
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_data_types()

Print a table of supported data collection types.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
705
706
707
def show_supported_data_types(self) -> None:
    """Print a table of supported data collection types."""
    DataFactory.show_supported()
show_supported_excluded_regions_types()

Print a table of supported excluded-regions types.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
659
660
661
def show_supported_excluded_regions_types(self) -> None:
    """Print a table of supported excluded-regions types."""
    ExcludedRegionsFactory.show_supported()
show_supported_linked_phases_types()

Print a table of supported linked-phases collection types.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
616
617
618
def show_supported_linked_phases_types(self) -> None:
    """Print a table of supported linked-phases collection types."""
    LinkedPhasesFactory.show_supported()
show_supported_peak_profile_types()

Print available peak profile types for this experiment.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
758
759
760
761
762
763
def show_supported_peak_profile_types(self) -> None:
    """Print available peak profile types for this experiment."""
    PeakFactory.show_supported(
        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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
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.
        """
        pass

    # ------------------------------------------------------------------
    #  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. ``'shelx'``).
        """
        supported_tags = ExtinctionFactory.supported_tags()
        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(f"Extinction type for experiment '{self.name}' changed to")
        console.print(new_type)

    def show_supported_extinction_types(self) -> None:
        """Print a table of supported extinction correction models."""
        ExtinctionFactory.show_supported()

    def show_current_extinction_type(self) -> None:
        """Print the currently used extinction correction model."""
        console.paragraph('Current extinction type')
        console.print(self.extinction_type)

    # ------------------------------------------------------------------
    #  Linked crystal (switchable-category pattern)
    # ------------------------------------------------------------------

    @property
    def linked_crystal(self) -> object:
        """Linked crystal model for this experiment."""
        return self._linked_crystal

    @property
    def linked_crystal_type(self) -> str:
        """Tag of the active linked-crystal reference type."""
        return self._linked_crystal_type

    @linked_crystal_type.setter
    def linked_crystal_type(self, new_type: str) -> None:
        """
        Switch to a different linked-crystal reference type.

        Parameters
        ----------
        new_type : str
            Linked-crystal tag (e.g. ``'default'``).
        """
        supported_tags = LinkedCrystalFactory.supported_tags()
        if new_type not in supported_tags:
            log.warning(
                f"Unsupported linked crystal type '{new_type}'. "
                f'Supported: {supported_tags}. '
                f"For more information, use 'show_supported_linked_crystal_types()'",
            )
            return

        self._linked_crystal = LinkedCrystalFactory.create(new_type)
        self._linked_crystal_type = new_type
        console.paragraph(f"Linked crystal type for experiment '{self.name}' changed to")
        console.print(new_type)

    def show_supported_linked_crystal_types(self) -> None:
        """Print a table of supported linked-crystal reference types."""
        LinkedCrystalFactory.show_supported()

    def show_current_linked_crystal_type(self) -> None:
        """Print the currently used linked-crystal reference type."""
        console.paragraph('Current linked crystal type')
        console.print(self.linked_crystal_type)

    # ------------------------------------------------------------------
    #  Instrument (switchable-category pattern)
    # ------------------------------------------------------------------

    @property
    def instrument(self) -> object:
        """Active instrument model for this experiment."""
        return self._instrument

    @property
    def instrument_type(self) -> str:
        """Tag of the active instrument type."""
        return self._instrument_type

    @instrument_type.setter
    def instrument_type(self, new_type: str) -> None:
        """
        Switch to a different instrument type.

        Parameters
        ----------
        new_type : str
            Instrument tag (e.g. ``'cwl-sc'``).
        """
        supported = InstrumentFactory.supported_for(
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
            sample_form=self.type.sample_form.value,
        )
        supported_tags = [k.type_info.tag for k in supported]
        if new_type not in supported_tags:
            log.warning(
                f"Unsupported instrument type '{new_type}'. "
                f'Supported: {supported_tags}. '
                f"For more information, use 'show_supported_instrument_types()'",
            )
            return
        self._instrument = InstrumentFactory.create(new_type)
        self._instrument_type = new_type
        console.paragraph(f"Instrument type for experiment '{self.name}' changed to")
        console.print(new_type)

    def show_supported_instrument_types(self) -> None:
        """Print a table of supported instrument types."""
        InstrumentFactory.show_supported(
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
            sample_form=self.type.sample_form.value,
        )

    def show_current_instrument_type(self) -> None:
        """Print the currently used instrument type."""
        console.paragraph('Current instrument type')
        console.print(self.instrument_type)

    # ------------------------------------------------------------------
    #  Data (switchable-category pattern)
    # ------------------------------------------------------------------

    @property
    def data(self) -> object:
        """Data collection for this experiment."""
        return self._data

    @property
    def data_type(self) -> str:
        """Tag of the active data collection type."""
        return self._data_type

    @data_type.setter
    def data_type(self, new_type: str) -> None:
        """
        Switch to a different data collection type.

        Parameters
        ----------
        new_type : str
            Data tag (e.g. ``'bragg-sc'``).
        """
        supported_tags = DataFactory.supported_tags()
        if new_type not in supported_tags:
            log.warning(
                f"Unsupported data type '{new_type}'. "
                f'Supported: {supported_tags}. '
                f"For more information, use 'show_supported_data_types()'",
            )
            return
        self._data = DataFactory.create(new_type)
        self._data_type = new_type
        console.paragraph(f"Data type for experiment '{self.name}' changed to")
        console.print(new_type)

    def show_supported_data_types(self) -> None:
        """Print a table of supported data collection types."""
        DataFactory.show_supported()

    def show_current_data_type(self) -> None:
        """Print the currently used data collection type."""
        console.paragraph('Current data type')
        console.print(self.data_type)
data property

Data collection for this experiment.

data_type property writable

Tag of the active data collection type.

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.

instrument_type property writable

Tag of the active instrument type.

linked_crystal property

Linked crystal model for this experiment.

linked_crystal_type property writable

Tag of the active linked-crystal reference type.

show_current_data_type()

Print the currently used data collection type.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
493
494
495
496
def show_current_data_type(self) -> None:
    """Print the currently used data collection type."""
    console.paragraph('Current data type')
    console.print(self.data_type)
show_current_extinction_type()

Print the currently used extinction correction model.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
345
346
347
348
def show_current_extinction_type(self) -> None:
    """Print the currently used extinction correction model."""
    console.paragraph('Current extinction type')
    console.print(self.extinction_type)
show_current_instrument_type()

Print the currently used instrument type.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
447
448
449
450
def show_current_instrument_type(self) -> None:
    """Print the currently used instrument type."""
    console.paragraph('Current instrument type')
    console.print(self.instrument_type)
show_current_linked_crystal_type()

Print the currently used linked-crystal reference type.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
392
393
394
395
def show_current_linked_crystal_type(self) -> None:
    """Print the currently used linked-crystal reference type."""
    console.paragraph('Current linked crystal type')
    console.print(self.linked_crystal_type)
show_supported_data_types()

Print a table of supported data collection types.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
489
490
491
def show_supported_data_types(self) -> None:
    """Print a table of supported data collection types."""
    DataFactory.show_supported()
show_supported_extinction_types()

Print a table of supported extinction correction models.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
341
342
343
def show_supported_extinction_types(self) -> None:
    """Print a table of supported extinction correction models."""
    ExtinctionFactory.show_supported()
show_supported_instrument_types()

Print a table of supported instrument types.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
439
440
441
442
443
444
445
def show_supported_instrument_types(self) -> None:
    """Print a table of supported instrument types."""
    InstrumentFactory.show_supported(
        scattering_type=self.type.scattering_type.value,
        beam_mode=self.type.beam_mode.value,
        sample_form=self.type.sample_form.value,
    )
show_supported_linked_crystal_types()

Print a table of supported linked-crystal reference types.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
388
389
390
def show_supported_linked_crystal_types(self) -> None:
    """Print a table of supported linked-crystal reference types."""
    LinkedCrystalFactory.show_supported()

bragg_pd

BraggPdExperiment

Bases: PdExperimentBase

Standard Bragg powder diffraction experiment.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_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
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
@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] < 2:
            log.error(
                'Data file must have at least two columns: x and y.',
                exc_type=ValueError,
            )
            return 0

        if data.shape[1] < 3:
            log.warning('No uncertainty (sy) column provided. Defaulting to sqrt(y).')

        # Extract x, y data
        x: np.ndarray = data[:, 0]
        y: np.ndarray = 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: np.ndarray = data[:, 2] if data.shape[1] > 2 else np.sqrt(y)

        # Replace values smaller than 0.0001 with 1.0
        # TODO: Not used if loading from cif file?
        sy = np.where(sy < 0.0001, 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 (switchable-category pattern)
    # ------------------------------------------------------------------

    @property
    def instrument(self) -> object:
        """Active instrument model for this experiment."""
        return self._instrument

    @property
    def instrument_type(self) -> str:
        """Tag of the active instrument type."""
        return self._instrument_type

    @instrument_type.setter
    def instrument_type(self, new_type: str) -> None:
        """
        Switch to a different instrument type.

        Parameters
        ----------
        new_type : str
            Instrument tag (e.g. ``'cwl-pd'``).
        """
        supported = InstrumentFactory.supported_for(
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
            sample_form=self.type.sample_form.value,
        )
        supported_tags = [k.type_info.tag for k in supported]
        if new_type not in supported_tags:
            log.warning(
                f"Unsupported instrument type '{new_type}'. "
                f'Supported: {supported_tags}. '
                f"For more information, use 'show_supported_instrument_types()'",
            )
            return
        self._instrument = InstrumentFactory.create(new_type)
        self._instrument_type = new_type
        console.paragraph(f"Instrument type for experiment '{self.name}' changed to")
        console.print(new_type)

    def show_supported_instrument_types(self) -> None:
        """Print a table of supported instrument types."""
        InstrumentFactory.show_supported(
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
            sample_form=self.type.sample_form.value,
        )

    def show_current_instrument_type(self) -> None:
        """Print the currently used instrument type."""
        console.paragraph('Current instrument type')
        console.print(self.instrument_type)

    # ------------------------------------------------------------------
    #  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_tags = BackgroundFactory.supported_tags()
        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()

    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.

instrument_type property writable

Tag of the active instrument type.

show_current_background_type()

Print the currently used background type.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_pd.py
216
217
218
219
def show_current_background_type(self) -> None:
    """Print the currently used background type."""
    console.paragraph('Current background type')
    console.print(self.background_type)
show_current_instrument_type()

Print the currently used instrument type.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_pd.py
165
166
167
168
def show_current_instrument_type(self) -> None:
    """Print the currently used instrument type."""
    console.paragraph('Current instrument type')
    console.print(self.instrument_type)
show_supported_background_types()

Print a table of supported background types.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_pd.py
212
213
214
def show_supported_background_types(self) -> None:
    """Print a table of supported background types."""
    BackgroundFactory.show_supported()
show_supported_instrument_types()

Print a table of supported instrument types.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_pd.py
157
158
159
160
161
162
163
def show_supported_instrument_types(self) -> None:
    """Print a table of supported instrument types."""
    InstrumentFactory.show_supported(
        scattering_type=self.type.scattering_type.value,
        beam_mode=self.type.beam_mode.value,
        sample_form=self.type.sample_form.value,
    )

bragg_sc

CwlScExperiment

Bases: ScExperimentBase

Bragg constant-wavelength single-crystal experiment.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_sc.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
@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] < 5:
            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: np.ndarray = data[:, 0].astype(int)
        indices_k: np.ndarray = data[:, 1].astype(int)
        indices_l: np.ndarray = data[:, 2].astype(int)

        # Extract intensities and their standard uncertainties
        integrated_intensities: np.ndarray = data[:, 3]
        integrated_intensities_su: np.ndarray = 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
 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
@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 IOError as e:
            log.error(
                f'Failed to read data from {data_path}: {e}',
                exc_type=IOError,
            )
            return 0

        if data.shape[1] < 6:
            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: np.ndarray = data[:, 0].astype(int)
        indices_k: np.ndarray = data[:, 1].astype(int)
        indices_l: np.ndarray = data[:, 2].astype(int)

        # Extract intensities and their standard uncertainties
        integrated_intensities: np.ndarray = data[:, 3]
        integrated_intensities_su: np.ndarray = data[:, 4]

        # Extract wavelength values
        wavelength: np.ndarray = 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: str, Enum

Beam delivery mode for the instrument.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
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
class BeamModeEnum(str, Enum):
    """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.'
        elif self is BeamModeEnum.TIME_OF_FLIGHT:
            return 'Time-of-flight (TOF) diffraction.'
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
114
115
116
117
118
119
120
121
122
123
124
@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
126
127
128
129
130
131
132
133
134
135
136
137
138
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.'
    elif self is BeamModeEnum.TIME_OF_FLIGHT:
        return 'Time-of-flight (TOF) diffraction.'

CalculatorEnum

Bases: str, Enum

Known calculation engine identifiers.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
141
142
143
144
145
146
class CalculatorEnum(str, Enum):
    """Known calculation engine identifiers."""

    CRYSPY = 'cryspy'
    CRYSFML = 'crysfml'
    PDFFIT = 'pdffit'

PeakProfileTypeEnum

Bases: str, Enum

Available peak profile types per scattering and beam mode.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
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
class PeakProfileTypeEnum(str, Enum):
    """Available peak profile types per scattering and beam mode."""

    PSEUDO_VOIGT = 'pseudo-voigt'
    SPLIT_PSEUDO_VOIGT = 'split pseudo-voigt'
    THOMPSON_COX_HASTINGS = 'thompson-cox-hastings'
    PSEUDO_VOIGT_IKEDA_CARPENTER = 'pseudo-voigt * ikeda-carpenter'
    PSEUDO_VOIGT_BACK_TO_BACK = 'pseudo-voigt * back-to-back'
    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.PSEUDO_VOIGT_IKEDA_CARPENTER,
            (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:
        """
        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'
        elif self is PeakProfileTypeEnum.SPLIT_PSEUDO_VOIGT:
            return 'Split pseudo-Voigt profile with empirical asymmetry correction.'
        elif self is PeakProfileTypeEnum.THOMPSON_COX_HASTINGS:
            return 'Thompson-Cox-Hastings profile with FCJ asymmetry correction.'
        elif self is PeakProfileTypeEnum.PSEUDO_VOIGT_IKEDA_CARPENTER:
            return 'Pseudo-Voigt profile with Ikeda-Carpenter asymmetry correction.'
        elif self is PeakProfileTypeEnum.PSEUDO_VOIGT_BACK_TO_BACK:
            return 'Pseudo-Voigt profile with Back-to-Back Exponential asymmetry correction.'
        elif self is PeakProfileTypeEnum.GAUSSIAN_DAMPED_SINC:
            return 'Gaussian-damped sinc profile for pair distribution function (PDF) analysis.'
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
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
@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.PSEUDO_VOIGT_IKEDA_CARPENTER,
        (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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def description(self) -> str:
    """
    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'
    elif self is PeakProfileTypeEnum.SPLIT_PSEUDO_VOIGT:
        return 'Split pseudo-Voigt profile with empirical asymmetry correction.'
    elif self is PeakProfileTypeEnum.THOMPSON_COX_HASTINGS:
        return 'Thompson-Cox-Hastings profile with FCJ asymmetry correction.'
    elif self is PeakProfileTypeEnum.PSEUDO_VOIGT_IKEDA_CARPENTER:
        return 'Pseudo-Voigt profile with Ikeda-Carpenter asymmetry correction.'
    elif self is PeakProfileTypeEnum.PSEUDO_VOIGT_BACK_TO_BACK:
        return 'Pseudo-Voigt profile with Back-to-Back Exponential asymmetry correction.'
    elif self is PeakProfileTypeEnum.GAUSSIAN_DAMPED_SINC:
        return 'Gaussian-damped sinc profile for pair distribution function (PDF) analysis.'

RadiationProbeEnum

Bases: str, Enum

Incident radiation probe used in the experiment.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
 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
class RadiationProbeEnum(str, Enum):
    """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.'
        elif self is RadiationProbeEnum.XRAY:
            return 'X-ray diffraction.'
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
80
81
82
83
84
85
86
87
88
89
90
@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
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
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.'
    elif self is RadiationProbeEnum.XRAY:
        return 'X-ray diffraction.'

SampleFormEnum

Bases: str, Enum

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
class SampleFormEnum(str, Enum):
    """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.'
        elif self is SampleFormEnum.SINGLE_CRYSTAL:
            return 'Single crystal sample.'
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
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.'
    elif self is SampleFormEnum.SINGLE_CRYSTAL:
        return 'Single crystal sample.'

ScatteringTypeEnum

Bases: str, Enum

Type of scattering modeled in an experiment.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class ScatteringTypeEnum(str, Enum):
    """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.'
        elif self is ScatteringTypeEnum.TOTAL:
            return 'Total scattering for pair distribution function analysis (PDF).'
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
47
48
49
50
51
52
53
54
55
56
57
@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
59
60
61
62
63
64
65
66
67
68
69
70
71
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.'
    elif self is ScatteringTypeEnum.TOTAL:
        return 'Total scattering for pair distribution function analysis (PDF).'

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
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
class ExperimentFactory(FactoryBase):
    """Creates Experiment instances with only relevant attributes."""

    _default_rules = {
        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)

        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)
        expt_obj = expt_class(name=name, type=expt_type)
        return expt_obj

    # 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,
        verbosity: VerbosityEnum = VerbosityEnum.FULL,
    ) -> 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'``).
        verbosity : VerbosityEnum, default=VerbosityEnum.FULL
            Console output verbosity.

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

        num_points = expt_obj._load_ascii_data_to_experiment(data_path)

        if verbosity is VerbosityEnum.FULL:
            console.paragraph('Data loaded successfully')
            console.print(f"Experiment 🔬 '{name}'. Number of data points: {num_points}.")
        elif verbosity is VerbosityEnum.SHORT:
            console.print(f"✅ Data loaded: Experiment 🔬 '{name}'. {num_points} points.")

        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
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
@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
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
@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, verbosity=VerbosityEnum.FULL) 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
verbosity VerbosityEnum

Console output verbosity.

VerbosityEnum.FULL

Returns:

Type Description
ExperimentBase

An experiment instance with measured data attached.

Source code in src/easydiffraction/datablocks/experiment/item/factory.py
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
@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,
    verbosity: VerbosityEnum = VerbosityEnum.FULL,
) -> 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'``).
    verbosity : VerbosityEnum, default=VerbosityEnum.FULL
        Console output verbosity.

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

    num_points = expt_obj._load_ascii_data_to_experiment(data_path)

    if verbosity is VerbosityEnum.FULL:
        console.paragraph('Data loaded successfully')
        console.print(f"Experiment 🔬 '{name}'. Number of data points: {num_points}.")
    elif verbosity is VerbosityEnum.SHORT:
        console.print(f"✅ Data loaded: Experiment 🔬 '{name}'. {num_points} points.")

    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
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
@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)
    expt_obj = expt_class(name=name, type=expt_type)
    return expt_obj

total_pd

TotalPdExperiment

Bases: PdExperimentBase

PDF experiment class with specific attributes.

Source code in src/easydiffraction/datablocks/experiment/item/total_pd.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
@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.
        IOError
            If the data file cannot be read.
        ValueError
            If the data file has fewer than two columns.
        """
        try:
            from diffpy.utils.parsers.loaddata import loadData
        except ImportError:
            raise ImportError('diffpy module not found.') from None
        try:
            data = loadData(data_path)
        except Exception as e:
            raise IOError(f'Failed to read data from {data_path}: {e}') from e

        if data.shape[1] < 2:
            raise ValueError('Data file must have at least two columns: x and y.')

        default_sy = 0.03
        if data.shape[1] < 3:
            print(f'Warning: No uncertainty (sy) column provided. Defaulting to {default_sy}.')

        x = data[:, 0]
        y = data[:, 1]
        sy = data[:, 2] if data.shape[1] > 2 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)