Skip to content

experiment

categories

background

base

BackgroundBase

Bases: CategoryCollection

Abstract base for background subcategories in experiments.

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

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

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

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

Print a human-readable view of background components.

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

chebyshev

Chebyshev polynomial background model.

Provides a collection of polynomial terms and evaluation helpers.

ChebyshevPolynomialBackground

Bases: BackgroundBase

Chebyshev polynomial background model.

Source code in src/easydiffraction/datablocks/experiment/categories/background/chebyshev.py
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
@BackgroundFactory.register
class ChebyshevPolynomialBackground(BackgroundBase):
    """Chebyshev polynomial background model."""

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

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

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

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

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

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

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

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

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

Print a table of polynomial orders and coefficients.

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

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

Bases: CategoryItem

Chebyshev polynomial term.

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

Source code in src/easydiffraction/datablocks/experiment/categories/background/chebyshev.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
class PolynomialTerm(CategoryItem):
    """
    Chebyshev polynomial term.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Coefficient used in a Chebyshev polynomial background term.

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

id property writable

Identifier for this background polynomial term.

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

order property writable

Order used in a Chebyshev polynomial background term.

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

enums

Enumerations for background model types.

BackgroundTypeEnum

Bases: StrEnum

Supported background model types.

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

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

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

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

Return a default background type.

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

Human-friendly description for the enum value.

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

factory

Background factory — delegates entirely to FactoryBase.

BackgroundFactory

Bases: FactoryBase

Create background collections by tag.

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

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

line_segment

Line-segment background model.

Interpolate user-specified points to form a background curve.

LineSegment

Bases: CategoryItem

Single background control point for interpolation.

Source code in src/easydiffraction/datablocks/experiment/categories/background/line_segment.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
class LineSegment(CategoryItem):
    """Single background control point for interpolation."""

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

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

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

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

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

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

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

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

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

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

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

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

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

Identifier for this background line segment.

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

x property writable

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

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

y property writable

Intensity used to create many straight-line segments.

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

LineSegmentBackground

Bases: BackgroundBase

Linear-interpolation background between user-defined points.

Source code in src/easydiffraction/datablocks/experiment/categories/background/line_segment.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
@BackgroundFactory.register
class LineSegmentBackground(BackgroundBase):
    """Linear-interpolation background between user-defined points."""

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

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

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

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

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

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

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

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

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

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

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

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

calculation

Experiment calculation category exports.

default

Experiment calculation category.

Calculation

Bases: CategoryItem

Calculator selection and access for an experiment.

Source code in src/easydiffraction/datablocks/experiment/categories/calculation/default.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
@CalculationFactory.register
class Calculation(CategoryItem):
    """Calculator selection and access for an experiment."""

    type_info = TypeInfo(
        tag='default',
        description='Experiment calculation category',
    )

    def __init__(
        self,
        *,
        calculator_type: str,
    ) -> None:
        super().__init__()

        self._calculator_type = StringDescriptor(
            name='calculator_type',
            description='Calculation backend type',
            value_spec=AttributeSpec(
                default=calculator_type,
                validator=MembershipValidator(
                    allowed=[member.value for member in CalculatorEnum],
                ),
            ),
            cif_handler=CifHandler(names=['_calculation.calculator_type']),
        )

        self._identity.category_code = 'calculation'

    @property
    def calculator_type(self) -> StringDescriptor:
        """Calculation backend type."""
        return self._calculator_type

    @calculator_type.setter
    def calculator_type(self, value: str) -> None:
        parent = getattr(self, '_parent', None)
        if parent is None:
            self._calculator_type.value = value
            return
        parent._set_calculator_type(value)

    @property
    def calculator(self) -> object | None:
        """Live calculator backend instance."""
        parent = getattr(self, '_parent', None)
        if parent is None:
            return None
        if getattr(parent, '_calculator', None) is None:
            parent._resolve_calculation()
        return parent._calculator

    def show_calculator_types(self) -> None:
        """Print supported calculator backends and mark current type."""
        from easydiffraction.analysis.calculators.factory import CalculatorFactory  # noqa: PLC0415

        parent = getattr(self, '_parent', None)
        current = self.calculator_type.value
        if parent is None:
            supported_tags = CalculatorFactory.supported_tags()
        else:
            supported_tags = parent._supported_calculator_tags()
        all_classes = CalculatorFactory._supported_map()
        columns_data = [
            ['*' if tag == current else '', tag, cls.type_info.description]
            for tag, cls in all_classes.items()
            if tag in supported_tags
        ]
        console.paragraph('Calculator types')
        render_table(
            columns_headers=['', 'Type', 'Description'],
            columns_alignment=['left', 'left', 'left'],
            columns_data=columns_data,
        )

    def from_cif(self, block: object, idx: int = 0) -> None:
        """Populate this calculation category from a CIF block."""
        del idx
        tag = read_cif_str(block, '_calculation.calculator_type')
        if tag is None:
            return
        parent = getattr(self, '_parent', None)
        if parent is None:
            self._calculator_type.value = tag
            return
        parent._set_calculator_type(tag, announce=False)
calculator property

Live calculator backend instance.

calculator_type property writable

Calculation backend type.

from_cif(block, idx=0)

Populate this calculation category from a CIF block.

Source code in src/easydiffraction/datablocks/experiment/categories/calculation/default.py
 96
 97
 98
 99
100
101
102
103
104
105
106
def from_cif(self, block: object, idx: int = 0) -> None:
    """Populate this calculation category from a CIF block."""
    del idx
    tag = read_cif_str(block, '_calculation.calculator_type')
    if tag is None:
        return
    parent = getattr(self, '_parent', None)
    if parent is None:
        self._calculator_type.value = tag
        return
    parent._set_calculator_type(tag, announce=False)
show_calculator_types()

Print supported calculator backends and mark current type.

Source code in src/easydiffraction/datablocks/experiment/categories/calculation/default.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
def show_calculator_types(self) -> None:
    """Print supported calculator backends and mark current type."""
    from easydiffraction.analysis.calculators.factory import CalculatorFactory  # noqa: PLC0415

    parent = getattr(self, '_parent', None)
    current = self.calculator_type.value
    if parent is None:
        supported_tags = CalculatorFactory.supported_tags()
    else:
        supported_tags = parent._supported_calculator_tags()
    all_classes = CalculatorFactory._supported_map()
    columns_data = [
        ['*' if tag == current else '', tag, cls.type_info.description]
        for tag, cls in all_classes.items()
        if tag in supported_tags
    ]
    console.paragraph('Calculator types')
    render_table(
        columns_headers=['', 'Type', 'Description'],
        columns_alignment=['left', 'left', 'left'],
        columns_data=columns_data,
    )

factory

Factory for experiment calculation categories.

CalculationFactory

Bases: FactoryBase

Create experiment calculation category instances.

Source code in src/easydiffraction/datablocks/experiment/categories/calculation/factory.py
12
13
14
15
16
17
class CalculationFactory(FactoryBase):
    """Create experiment calculation category instances."""

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

data

bragg_pd

PdCwlData

Bases: PdDataBase

Bragg powder CWL data collection.

Source code in src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py
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
@DataFactory.register
class PdCwlData(PdDataBase):
    """Bragg powder CWL data collection."""

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

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

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

    # Should be set only once

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

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

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

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

    # Misc

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

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

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

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

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

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

Get 2θ values for data points included in calculations.

unfiltered_x property

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

x property

Alias for two_theta.

PdCwlDataPoint

Bases: PdDataPointBaseMixin, PdCwlDataPointMixin, CategoryItem

Powder diffraction data point for CWL experiments.

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

    # TODO: ???

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

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

    # Should be set only once

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

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

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

    # Can be set multiple times

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

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

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

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

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

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

    # Misc

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

        calc, refln_records, missing_refln_records = self._phase_calculation_results(
            experiment=experiment,
            structures=structures,
            calculator=calculator,
            called_by_minimizer=called_by_minimizer,
            collect_refln_records=refln is not None,
        )
        self._set_intensity_calc(calc + self.intensity_bkg)
        if refln is None:
            return

        if missing_refln_records:
            refln._replace_from_records([])
            log.warning(
                'Calculated powder reflection metadata is unavailable for '
                f"experiment '{experiment.name}' with calculator "
                f"'{calculator.name}'. Clearing experiment.refln.",
            )
            return

        refln._replace_from_records(refln_records)

    def _phase_calculation_results(
        self,
        *,
        experiment: object,
        structures: object,
        calculator: object,
        called_by_minimizer: bool,
        collect_refln_records: bool,
    ) -> tuple[np.ndarray, list[PowderReflnRecord], bool]:
        calc = np.zeros_like(self.x)
        refln_records: list[PowderReflnRecord] = []
        missing_refln_records = False

        for linked_phase in experiment._get_valid_linked_phases(structures):
            structure_id = linked_phase._identity.category_entry_name
            structure = structures[structure_id]
            structure_scaled_calc, structure_refln_records = self._phase_result(
                structure=structure,
                experiment=experiment,
                calculator=calculator,
                linked_phase=linked_phase,
                called_by_minimizer=called_by_minimizer,
                collect_refln_records=collect_refln_records,
            )
            calc += structure_scaled_calc
            if not collect_refln_records:
                continue
            if structure_refln_records is None:
                missing_refln_records = True
                continue
            refln_records.extend(structure_refln_records)

        return calc, refln_records, missing_refln_records

    @staticmethod
    def _phase_result(
        *,
        structure: object,
        experiment: object,
        calculator: object,
        linked_phase: object,
        called_by_minimizer: bool,
        collect_refln_records: bool,
    ) -> tuple[np.ndarray, list[PowderReflnRecord] | None]:
        structure_calc = calculator.calculate_pattern(
            structure,
            experiment,
            called_by_minimizer=called_by_minimizer,
        )
        structure_scaled_calc = linked_phase.scale.value * structure_calc
        if not collect_refln_records:
            return structure_scaled_calc, []

        structure_refln_records = calculator.last_powder_refln_records(
            structure,
            experiment,
            phase_id=linked_phase.id.value,
        )
        return structure_scaled_calc, structure_refln_records

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

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

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

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

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

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

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

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

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

d_spacing property

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

intensity_bkg property

Background intensities for active data points.

intensity_calc property

Calculated intensities for active data points.

intensity_meas property

Measured intensities for active data points.

intensity_meas_su property

Standard uncertainties of the measured intensities.

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

PdDataPointBaseMixin

Single base data point mixin for powder diffraction data.

Source code in src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
class 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
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
@DataFactory.register
class PdTofData(PdDataBase):
    """Bragg powder TOF data collection."""

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

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

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

    # Should be set only once

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

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

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

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

    # Misc

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

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

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

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

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

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

Get TOF values for data points included in calculations.

unfiltered_x property

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

x property

Alias for time_of_flight.

PdTofDataPoint

Bases: PdDataPointBaseMixin, PdTofDataPointMixin, CategoryItem

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

Source code in src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py
294
295
296
297
298
299
300
301
302
303
304
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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
class 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.

factory

Data collection factory — delegates to FactoryBase.

DataFactory

Bases: FactoryBase

Factory for creating diffraction data collections.

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

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

total_pd

Data categories for total scattering (PDF) experiments.

TotalData

Bases: TotalDataBase

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

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

Source code in src/easydiffraction/datablocks/experiment/categories/data/total_pd.py
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
@DataFactory.register
class TotalData(TotalDataBase):
    """
    Total scattering (PDF) data collection in r-space.

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

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

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

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

    # Should be set only once

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

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

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

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

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

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

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

Get the r values for all data points.

x property

Get the r values for data points included in calculations.

TotalDataBase

Bases: CategoryCollection

Base class for total scattering data collections.

Source code in src/easydiffraction/datablocks/experiment/categories/data/total_pd.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
class TotalDataBase(CategoryCollection):
    """Base class for total scattering data collections."""

    _update_priority = 100

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

    # Should be set only once

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

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

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

    # Can be set multiple times

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

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

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

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

    # Misc

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

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

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

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

            structure_scaled_calc = structure_scale * structure_calc
            calc += structure_scaled_calc

        self._set_g_r_calc(calc)

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

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

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

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

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

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

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

intensity_bkg property

Background is always zero for PDF data.

intensity_calc property

Calculated G(r) values for active data points.

intensity_meas property

Measured G(r) values for active data points.

intensity_meas_su property

Standard uncertainties of the measured G(r) values.

TotalDataPoint

Bases: CategoryItem

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Status code of the data point in calculation.

Reading this property returns the underlying StringDescriptor object.

g_r_calc property

Calculated pair distribution function G(r).

Reading this property returns the underlying NumericDescriptor object.

g_r_meas property

Measured pair distribution function G(r).

Reading this property returns the underlying NumericDescriptor object.

g_r_meas_su property

Standard uncertainty of measured G(r).

Reading this property returns the underlying NumericDescriptor object.

point_id property

Identifier for this data point in the dataset.

Reading this property returns the underlying StringDescriptor object.

r property

Interatomic distance in real space (Å).

Reading this property returns the underlying NumericDescriptor object.

diffrn

default

Default diffraction ambient-conditions category.

DefaultDiffrn

Bases: CategoryItem

Ambient conditions recorded during diffraction measurement.

Source code in src/easydiffraction/datablocks/experiment/categories/diffrn/default.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
@DiffrnFactory.register
class DefaultDiffrn(CategoryItem):
    """Ambient conditions recorded during diffraction measurement."""

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

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

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

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

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

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

        self._identity.category_code = 'diffrn'

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

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

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

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

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

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

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

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

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

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

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

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

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

Mean electric field during measurement (V/m).

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

ambient_magnetic_field property writable

Mean magnetic field during measurement (T).

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

ambient_pressure property writable

Mean hydrostatic pressure during measurement (kPa).

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

ambient_temperature property writable

Mean temperature during measurement (K).

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

factory

Factory for diffraction ambient-conditions categories.

DiffrnFactory

Bases: FactoryBase

Create diffraction ambient-conditions category instances.

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

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

excluded_regions

default

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

ExcludedRegion

Bases: CategoryItem

Closed interval [start, end] to be excluded.

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

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

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

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

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

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

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

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

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

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

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

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

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

End of the excluded region.

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

id property writable

Identifier for this excluded region.

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

start property writable

Start of the excluded region.

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

ExcludedRegions

Bases: CategoryCollection

Collection of ExcludedRegion instances.

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

Source code in src/easydiffraction/datablocks/experiment/categories/excluded_regions/default.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
@ExcludedRegionsFactory.register
class ExcludedRegions(CategoryCollection):
    """
    Collection of ExcludedRegion instances.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

factory

Excluded-regions factory — delegates entirely to FactoryBase.

ExcludedRegionsFactory

Bases: FactoryBase

Create excluded-regions collections by tag.

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

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

experiment_type

default

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

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

ExperimentType

Bases: CategoryItem

Container of attributes defining the experiment type.

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

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

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

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

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

        self._identity.category_code = 'expt_type'

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Reading this property returns the underlying StringDescriptor object.

radiation_probe property

Neutron or X-ray diffraction measurement.

Reading this property returns the underlying StringDescriptor object.

sample_form property

Powder diffraction or single crystal diffraction.

Reading this property returns the underlying StringDescriptor object.

scattering_type property

Conventional Bragg diffraction or total scattering (PDF).

Reading this property returns the underlying StringDescriptor object.

factory

Experiment-type factory — delegates entirely to FactoryBase.

ExperimentTypeFactory

Bases: FactoryBase

Create experiment-type descriptors by tag.

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

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

extinction

becker_coppens

Becker-Coppens isotropic extinction correction for single crystals.

BeckerCoppensExtinction

Bases: CategoryItem

Becker-Coppens spherical extinction correction for single crystals.

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

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

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

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

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

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

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

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

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

        self._identity.category_code = 'extinction'

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

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

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

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

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

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

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

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

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

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

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

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

mosaicity property writable

Mosaicity of the crystal (arcmin).

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

radius property writable

Mean radius of the crystal (μm).

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

factory

Extinction factory — delegates entirely to FactoryBase.

ExtinctionFactory

Bases: FactoryBase

Create extinction correction models by tag.

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

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

instrument

base

Instrument category base definitions for CWL/TOF instruments.

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

InstrumentBase

Bases: CategoryItem

Base class for instrument category items.

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

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

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

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

Initialize instrument base and set category code.

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

cwl

CwlInstrumentBase

Bases: InstrumentBase

Base class for constant-wavelength instruments.

Source code in src/easydiffraction/datablocks/experiment/categories/instrument/cwl.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class CwlInstrumentBase(InstrumentBase):
    """Base class for constant-wavelength instruments."""

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

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

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

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

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

Incident neutron or X-ray wavelength (Å).

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

CwlPdInstrument

Bases: CwlInstrumentBase

CW powder diffractometer.

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

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

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

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

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

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

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

Instrument misalignment offset (deg).

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

CwlScInstrument

Bases: CwlInstrumentBase

CW single-crystal diffractometer.

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

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

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

factory

Instrument factory — delegates to FactoryBase.

InstrumentFactory

Bases: FactoryBase

Create instrument instances for supported modes.

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

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

tof

TofPdInstrument

Bases: InstrumentBase

TOF powder diffractometer.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

TOF linear conversion (μs/Å).

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

calib_d_to_tof_offset property writable

TOF offset (μs).

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

calib_d_to_tof_quad property writable

TOF quadratic correction (μs/Ų).

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

calib_d_to_tof_recip property writable

TOF reciprocal velocity correction (μs·Å).

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

setup_twotheta_bank property writable

Detector bank position (deg).

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

TofScInstrument

Bases: InstrumentBase

TOF single-crystal diffractometer.

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

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

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

linked_crystal

default

Default linked-crystal reference (id + scale).

LinkedCrystal

Bases: CategoryItem

Linked crystal reference for single-crystal diffraction.

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

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

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

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

        self._identity.category_code = 'linked_crystal'

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

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

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

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

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

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

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

Identifier of the linked crystal.

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

scale property writable

Scale factor of the linked crystal.

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

factory

Linked-crystal factory — delegates entirely to FactoryBase.

LinkedCrystalFactory

Bases: FactoryBase

Create linked-crystal references by tag.

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

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

linked_phases

default

Linked phases allow combining phases with scale factors.

LinkedPhase

Bases: CategoryItem

Link to a phase by id with a scale factor.

Source code in src/easydiffraction/datablocks/experiment/categories/linked_phases/default.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class LinkedPhase(CategoryItem):
    """Link to a phase by id with a scale factor."""

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

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

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

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

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

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

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

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

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

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

Identifier of the linked phase.

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

scale property writable

Scale factor of the linked phase.

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

LinkedPhases

Bases: CategoryCollection

Collection of LinkedPhase instances.

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

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

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

Create an empty collection of linked phases.

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

factory

Linked-phases factory — delegates entirely to FactoryBase.

LinkedPhasesFactory

Bases: FactoryBase

Create linked-phases collections by tag.

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

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

peak

base

Base class for peak profile categories.

PeakBase

Bases: CategoryItem

Base class for peak profile categories.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/base.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class PeakBase(CategoryItem):
    """Base class for peak profile categories."""

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

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

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

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

CIF identifier for the active peak profile type.

Returns:

Type Description
StringDescriptor

The descriptor holding the profile type tag string.

cwl

Constant-wavelength peak profile classes.

CwlPseudoVoigt

Bases: PeakBase, CwlBroadeningMixin

Constant-wavelength pseudo-Voigt peak shape.

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

    type_info = TypeInfo(
        tag=PeakProfileTypeEnum.CWL_PSEUDO_VOIGT.value,
        description=PeakProfileTypeEnum.CWL_PSEUDO_VOIGT.description(),
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}),
    )

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

Bases: PeakBase, CwlBroadeningMixin, EmpiricalAsymmetryMixin

Pseudo-Voigt with empirical asymmetry correction for CWL mode.

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

    type_info = TypeInfo(
        tag=PeakProfileTypeEnum.CWL_PSEUDO_VOIGT_EMPIRICAL_ASYMMETRY.value,
        description=(PeakProfileTypeEnum.CWL_PSEUDO_VOIGT_EMPIRICAL_ASYMMETRY.description()),
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY}),
    )

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

Bases: PeakBase, CwlBroadeningMixin, FcjAsymmetryMixin

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

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

    type_info = TypeInfo(
        tag=PeakProfileTypeEnum.CWL_THOMPSON_COX_HASTINGS.value,
        description=PeakProfileTypeEnum.CWL_THOMPSON_COX_HASTINGS.description(),
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSFML}),
    )

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

cwl_mixins

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

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

CwlBroadeningMixin

CWL Gaussian and Lorentz broadening parameters.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

broad_gauss_v property writable

Gaussian broadening instrumental contribution (deg²).

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

broad_gauss_w property writable

Gaussian broadening instrumental contribution (deg²).

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

broad_lorentz_x property writable

Lorentzian broadening (sample strain effects) (deg).

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

broad_lorentz_y property writable

Lorentzian broadening from microstructural defects (deg).

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

EmpiricalAsymmetryMixin

Empirical CWL peak asymmetry parameters.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/cwl_mixins.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
class EmpiricalAsymmetryMixin:
    """Empirical CWL peak asymmetry parameters."""

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Empirical asymmetry coefficient p1.

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

asym_empir_2 property writable

Empirical asymmetry coefficient p2.

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

asym_empir_3 property writable

Empirical asymmetry coefficient p3.

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

asym_empir_4 property writable

Empirical asymmetry coefficient p4.

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

FcjAsymmetryMixin

Finger-Cox-Jephcoat (FCJ) asymmetry parameters.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/cwl_mixins.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
class FcjAsymmetryMixin:
    """Finger-Cox-Jephcoat (FCJ) asymmetry parameters."""

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

        self._asym_fcj_1: Parameter = Parameter(
            name='asym_fcj_1',
            description='Finger-Cox-Jephcoat asymmetry parameter 1',
            units='',
            value_spec=AttributeSpec(
                default=0.0,
                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.0,
                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
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
class PeakFactory(FactoryBase):
    """Factory for creating peak profile objects."""

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

    _local_alias_rules: ClassVar[dict] = {
        frozenset({
            ('scattering_type', ScatteringTypeEnum.BRAGG),
            ('beam_mode', BeamModeEnum.CONSTANT_WAVELENGTH),
        }): {
            'pseudo-voigt': PeakProfileTypeEnum.CWL_PSEUDO_VOIGT,
            'pseudo-voigt + empirical asymmetry': (
                PeakProfileTypeEnum.CWL_PSEUDO_VOIGT_EMPIRICAL_ASYMMETRY
            ),
            'thompson-cox-hastings': PeakProfileTypeEnum.CWL_THOMPSON_COX_HASTINGS,
        },
        frozenset({
            ('scattering_type', ScatteringTypeEnum.BRAGG),
            ('beam_mode', BeamModeEnum.TIME_OF_FLIGHT),
        }): {
            'pseudo-voigt': PeakProfileTypeEnum.TOF_PSEUDO_VOIGT,
            'jorgensen': PeakProfileTypeEnum.TOF_JORGENSEN,
            'jorgensen-von-dreele': PeakProfileTypeEnum.TOF_JORGENSEN_VON_DREELE,
            'double-jorgensen-von-dreele': (PeakProfileTypeEnum.TOF_DOUBLE_JORGENSEN_VON_DREELE),
        },
        frozenset({
            ('scattering_type', ScatteringTypeEnum.TOTAL),
        }): {
            'gaussian-damped-sinc': PeakProfileTypeEnum.TOTAL_GAUSSIAN_DAMPED_SINC,
        },
    }

    @classmethod
    def _local_aliases_for(cls, **conditions: object) -> dict[str, str]:
        """Return context-local aliases for peak profile tags."""
        condition_set = frozenset(conditions.items())
        best_match_aliases: dict[str, str] = {}
        best_match_size = -1

        for rule_key, aliases in cls._local_alias_rules.items():
            if rule_key <= condition_set and len(rule_key) > best_match_size:
                best_match_aliases = aliases
                best_match_size = len(rule_key)

        return best_match_aliases

    @classmethod
    def _canonical_tag_for(cls, tag: str, **conditions: object) -> str:
        """Resolve a canonical tag or context-local alias to a tag."""
        if tag in cls._supported_map():
            return str(tag)
        aliases = cls._local_aliases_for(**conditions)
        canonical_tag = aliases.get(tag)
        if canonical_tag is None:
            return tag
        return str(canonical_tag)

    @classmethod
    def _local_alias_for(cls, tag: str, **conditions: object) -> str:
        """Return the context-local alias for a canonical tag."""
        aliases = cls._local_aliases_for(**conditions)
        for alias, canonical_tag in aliases.items():
            if canonical_tag == tag:
                return alias
        return str(tag)

    @classmethod
    def show_supported(
        cls,
        *,
        calculator: object = None,
        sample_form: object = None,
        scattering_type: object = None,
        beam_mode: object = None,
        radiation_probe: object = None,
    ) -> None:
        """
        Pretty-print supported peak profiles with context-local aliases.

        Parameters
        ----------
        calculator : object, default=None
            Optional ``CalculatorEnum`` value.
        sample_form : object, default=None
            Optional ``SampleFormEnum`` value.
        scattering_type : object, default=None
            Optional ``ScatteringTypeEnum`` value.
        beam_mode : object, default=None
            Optional ``BeamModeEnum`` value.
        radiation_probe : object, default=None
            Optional ``RadiationProbeEnum`` value.
        """
        matching = cls.supported_for(
            calculator=calculator,
            sample_form=sample_form,
            scattering_type=scattering_type,
            beam_mode=beam_mode,
            radiation_probe=radiation_probe,
        )
        conditions = {
            'sample_form': sample_form,
            'scattering_type': scattering_type,
            'beam_mode': beam_mode,
            'radiation_probe': radiation_probe,
        }
        columns_headers = ['Type', 'Description']
        columns_alignment = ['left', 'left']
        columns_data = [
            [
                cls._local_alias_for(klass.type_info.tag, **conditions),
                klass.type_info.description,
            ]
            for klass in matching
        ]
        console.paragraph('Supported types')
        render_table(
            columns_headers=columns_headers,
            columns_alignment=columns_alignment,
            columns_data=columns_data,
        )
show_supported(*, calculator=None, sample_form=None, scattering_type=None, beam_mode=None, radiation_probe=None) classmethod

Pretty-print supported peak profiles with context-local aliases.

Parameters:

Name Type Description Default
calculator object

Optional CalculatorEnum value.

None
sample_form object

Optional SampleFormEnum value.

None
scattering_type object

Optional ScatteringTypeEnum value.

None
beam_mode object

Optional BeamModeEnum value.

None
radiation_probe object

Optional RadiationProbeEnum value.

None
Source code in src/easydiffraction/datablocks/experiment/categories/peak/factory.py
 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
@classmethod
def show_supported(
    cls,
    *,
    calculator: object = None,
    sample_form: object = None,
    scattering_type: object = None,
    beam_mode: object = None,
    radiation_probe: object = None,
) -> None:
    """
    Pretty-print supported peak profiles with context-local aliases.

    Parameters
    ----------
    calculator : object, default=None
        Optional ``CalculatorEnum`` value.
    sample_form : object, default=None
        Optional ``SampleFormEnum`` value.
    scattering_type : object, default=None
        Optional ``ScatteringTypeEnum`` value.
    beam_mode : object, default=None
        Optional ``BeamModeEnum`` value.
    radiation_probe : object, default=None
        Optional ``RadiationProbeEnum`` value.
    """
    matching = cls.supported_for(
        calculator=calculator,
        sample_form=sample_form,
        scattering_type=scattering_type,
        beam_mode=beam_mode,
        radiation_probe=radiation_probe,
    )
    conditions = {
        'sample_form': sample_form,
        'scattering_type': scattering_type,
        'beam_mode': beam_mode,
        'radiation_probe': radiation_probe,
    }
    columns_headers = ['Type', 'Description']
    columns_alignment = ['left', 'left']
    columns_data = [
        [
            cls._local_alias_for(klass.type_info.tag, **conditions),
            klass.type_info.description,
        ]
        for klass in matching
    ]
    console.paragraph('Supported types')
    render_table(
        columns_headers=columns_headers,
        columns_alignment=columns_alignment,
        columns_data=columns_data,
    )

tof

Time-of-flight peak profile classes.

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

TofDoubleJorgensenVonDreele

Bases: PeakBase, TofGaussianBroadeningMixin, TofLorentzianBroadeningMixin, TofDoubleExponentialMixin

Double back-to-back exponentials ⊗ pseudo-Voigt TOF profile.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
@PeakFactory.register
class TofDoubleJorgensenVonDreele(
    PeakBase,
    TofGaussianBroadeningMixin,
    TofLorentzianBroadeningMixin,
    TofDoubleExponentialMixin,
):
    """Double back-to-back exponentials ⊗ pseudo-Voigt TOF profile."""

    type_info = TypeInfo(
        tag=PeakProfileTypeEnum.TOF_DOUBLE_JORGENSEN_VON_DREELE.value,
        description=PeakProfileTypeEnum.TOF_DOUBLE_JORGENSEN_VON_DREELE.description(),
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY}),
    )

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

Bases: PeakBase, TofGaussianBroadeningMixin, TofBackToBackExponentialMixin

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

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@PeakFactory.register
class TofJorgensen(
    PeakBase,
    TofGaussianBroadeningMixin,
    TofBackToBackExponentialMixin,
):
    """Jorgensen TOF profile: back-to-back exponentials ⊗ Gaussian."""

    type_info = TypeInfo(
        tag=PeakProfileTypeEnum.TOF_JORGENSEN.value,
        description=PeakProfileTypeEnum.TOF_JORGENSEN.description(),
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY, CalculatorEnum.CRYSFML}),
    )

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

Bases: PeakBase, TofGaussianBroadeningMixin, TofLorentzianBroadeningMixin, TofBackToBackExponentialMixin

Back-to-back exponentials ⊗ pseudo-Voigt TOF profile.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
@PeakFactory.register
class TofJorgensenVonDreele(
    PeakBase,
    TofGaussianBroadeningMixin,
    TofLorentzianBroadeningMixin,
    TofBackToBackExponentialMixin,
):
    """Back-to-back exponentials ⊗ pseudo-Voigt TOF profile."""

    type_info = TypeInfo(
        tag=PeakProfileTypeEnum.TOF_JORGENSEN_VON_DREELE.value,
        description=PeakProfileTypeEnum.TOF_JORGENSEN_VON_DREELE.description(),
    )
    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__()
TofPseudoVoigt

Bases: PeakBase, TofGaussianBroadeningMixin, TofLorentzianBroadeningMixin

Simple non-convoluted pseudo-Voigt TOF profile.

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@PeakFactory.register
class TofPseudoVoigt(
    PeakBase,
    TofGaussianBroadeningMixin,
    TofLorentzianBroadeningMixin,
):
    """Simple non-convoluted pseudo-Voigt TOF profile."""

    type_info = TypeInfo(
        tag=PeakProfileTypeEnum.TOF_PSEUDO_VOIGT.value,
        description=PeakProfileTypeEnum.TOF_PSEUDO_VOIGT.description(),
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}),
    )
    calculator_support = CalculatorSupport(
        calculators=frozenset({CalculatorEnum.CRYSPY}),
    )

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

tof_mixins

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

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

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

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

TofBackToBackExponentialMixin

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

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

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
class TofBackToBackExponentialMixin:
    """
    Back-to-back exponential (BBE) rise and decay parameters.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

exp_decay_beta_1 property writable

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

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

exp_rise_alpha_0 property writable

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

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

exp_rise_alpha_1 property writable

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

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

TofDoubleExponentialMixin

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

dexp_decay_beta_01 property writable

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

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

dexp_decay_beta_10 property writable

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

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

dexp_rise_alpha_1 property writable

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

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

dexp_rise_alpha_2 property writable

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

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

dexp_switch_r_01 property writable

Double-exp switching function r₀₁.

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

dexp_switch_r_02 property writable

Double-exp switching function r₀₂.

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

dexp_switch_r_03 property writable

Double-exp switching function r₀₃.

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

TofGaussianBroadeningMixin

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

broad_gauss_sigma_1 property writable

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

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

broad_gauss_sigma_2 property writable

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

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

TofLorentzianBroadeningMixin

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

Source code in src/easydiffraction/datablocks/experiment/categories/peak/tof_mixins.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
class TofLorentzianBroadeningMixin:
    """TOF Lorentzian broadening parameters γ₀, γ₁, γ₂."""

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

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

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

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

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

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

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

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

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

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

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

Lorentzian broadening (microstrain effects) (μs).

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

broad_lorentz_gamma_1 property writable

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

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

broad_lorentz_gamma_2 property writable

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

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

total

Total-scattering (PDF) peak profile classes.

TotalGaussianDampedSinc

Bases: PeakBase, TotalBroadeningMixin

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

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

    type_info = TypeInfo(
        tag=PeakProfileTypeEnum.TOTAL_GAUSSIAN_DAMPED_SINC.value,
        description=PeakProfileTypeEnum.TOTAL_GAUSSIAN_DAMPED_SINC.description(),
    )
    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.

refln

bragg_pd

PowderCwlRefln

Bases: PowderReflnBase

Single calculated powder reflection row for CWL experiments.

Source code in src/easydiffraction/datablocks/experiment/categories/refln/bragg_pd.py
 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
class PowderCwlRefln(PowderReflnBase):
    """Single calculated powder reflection row for CWL experiments."""

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

        self._two_theta = NumericDescriptor(
            name='two_theta',
            description='Calculated 2theta position for this reflection',
            units='deg',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0, le=180),
            ),
            cif_handler=CifHandler(names=['_refln.two_theta']),
        )

    @property
    def two_theta(self) -> NumericDescriptor:
        """Calculated 2theta position for this reflection."""
        return self._two_theta

    @property
    def parameters(self) -> list:
        """Powder CWL reflection descriptors serialized in CIF loops."""
        return [*super().parameters, self._two_theta]
parameters property

Powder CWL reflection descriptors serialized in CIF loops.

two_theta property

Calculated 2theta position for this reflection.

PowderCwlReflnData

Bases: PowderReflnDataBase

Calculated powder reflection collection for CWL experiments.

Source code in src/easydiffraction/datablocks/experiment/categories/refln/bragg_pd.py
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
@ReflnFactory.register
class PowderCwlReflnData(PowderReflnDataBase):
    """Calculated powder reflection collection for CWL experiments."""

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

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

    def _set_x_value(
        self,
        *,
        item: PowderReflnBase,
        record: PowderReflnRecord,
    ) -> None:
        del self
        item.two_theta._value = float(record.two_theta)

    @property
    def two_theta(self) -> np.ndarray:
        """Calculated 2theta positions for all rows."""
        return np.fromiter((item.two_theta.value for item in self._items), dtype=float)
two_theta property

Calculated 2theta positions for all rows.

PowderReflnBase

Bases: Refln

Single calculated powder reflection row.

Source code in src/easydiffraction/datablocks/experiment/categories/refln/bragg_pd.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class PowderReflnBase(SingleCrystalRefln):
    """Single calculated powder reflection row."""

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

        self._phase_id = StringDescriptor(
            name='phase_id',
            description='Identifier of the linked phase for this reflection',
            value_spec=AttributeSpec(default=''),
            cif_handler=CifHandler(names=['_refln.phase_id']),
        )
        self._f_calc = NumericDescriptor(
            name='f_calc',
            description='Calculated structure-factor amplitude for this reflection',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(names=['_refln.f_calc']),
        )
        self._f_squared_calc = NumericDescriptor(
            name='f_squared_calc',
            description='Calculated structure-factor amplitude squared for this reflection',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(names=['_refln.f_squared_calc']),
        )

    @property
    def phase_id(self) -> StringDescriptor:
        """Linked-phase identifier for this reflection."""
        return self._phase_id

    @property
    def f_calc(self) -> NumericDescriptor:
        """Calculated structure-factor amplitude for this reflection."""
        return self._f_calc

    @property
    def f_squared_calc(self) -> NumericDescriptor:
        """Calculated structure-factor amplitude squared."""
        return self._f_squared_calc

    @property
    def parameters(self) -> list:
        """Powder reflection descriptors serialized in CIF loops."""
        return [
            self._id,
            self._phase_id,
            self._d_spacing,
            self._sin_theta_over_lambda,
            self._index_h,
            self._index_k,
            self._index_l,
            self._f_calc,
            self._f_squared_calc,
        ]
f_calc property

Calculated structure-factor amplitude for this reflection.

f_squared_calc property

Calculated structure-factor amplitude squared.

parameters property

Powder reflection descriptors serialized in CIF loops.

phase_id property

Linked-phase identifier for this reflection.

PowderReflnDataBase

Bases: CategoryCollection

Base collection for calculated powder reflection rows.

Source code in src/easydiffraction/datablocks/experiment/categories/refln/bragg_pd.py
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
class PowderReflnDataBase(CategoryCollection):
    """Base collection for calculated powder reflection rows."""

    _update_priority = 110

    def _replace_from_records(self, records: Sequence[PowderReflnRecord]) -> None:
        """Replace all rows from calculator reflection records."""
        for item in self._items:
            item._parent = None

        new_items = []
        for index, record in enumerate(records, start=1):
            item = self._item_type()
            item._parent = self
            item.id._value = str(index)
            item.phase_id._value = str(record.phase_id)
            item.d_spacing._value = float(record.d_spacing)
            item.sin_theta_over_lambda._value = float(record.sin_theta_over_lambda)
            item.index_h._value = record.index_h
            item.index_k._value = record.index_k
            item.index_l._value = record.index_l
            item.f_calc._value = float(record.f_calc)
            item.f_squared_calc._value = float(record.f_squared_calc)
            self._set_x_value(item=item, record=record)
            new_items.append(item)

        self._items = new_items
        self._rebuild_index()

    def _set_x_value(
        self,
        *,
        item: PowderReflnBase,
        record: PowderReflnRecord,
    ) -> None:
        """Set the beam-mode-specific x coordinate."""
        del self, item, record

    @property
    def id(self) -> np.ndarray:
        """Reflection identifiers for all rows."""
        return np.fromiter((item.id.value for item in self._items), dtype=object)

    @property
    def phase_id(self) -> np.ndarray:
        """Linked-phase identifiers for all rows."""
        return np.fromiter((item.phase_id.value for item in self._items), dtype=object)

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

    @property
    def sin_theta_over_lambda(self) -> np.ndarray:
        """sin(theta)/lambda values for all rows."""
        return np.fromiter(
            (item.sin_theta_over_lambda.value for item in self._items),
            dtype=float,
        )

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

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

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

    @property
    def f_calc(self) -> np.ndarray:
        """Calculated structure-factor amplitudes for all rows."""
        return np.fromiter((item.f_calc.value for item in self._items), dtype=float)

    @property
    def f_squared_calc(self) -> np.ndarray:
        """
        Calculated structure-factor amplitudes squared for all rows.
        """
        return np.fromiter((item.f_squared_calc.value for item in self._items), dtype=float)
d_spacing property

D-spacing values for all rows.

f_calc property

Calculated structure-factor amplitudes for all rows.

f_squared_calc property

Calculated structure-factor amplitudes squared for all rows.

id property

Reflection identifiers for all rows.

index_h property

Miller h indices for all rows.

index_k property

Miller k indices for all rows.

index_l property

Miller l indices for all rows.

phase_id property

Linked-phase identifiers for all rows.

sin_theta_over_lambda property

sin(theta)/lambda values for all rows.

PowderTofRefln

Bases: PowderReflnBase

Single calculated powder reflection row for TOF experiments.

Source code in src/easydiffraction/datablocks/experiment/categories/refln/bragg_pd.py
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
class PowderTofRefln(PowderReflnBase):
    """Single calculated powder reflection row for TOF experiments."""

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

        self._time_of_flight = NumericDescriptor(
            name='time_of_flight',
            description='Calculated time-of-flight position for this reflection',
            units='μs',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0),
            ),
            cif_handler=CifHandler(names=['_refln.time_of_flight']),
        )

    @property
    def time_of_flight(self) -> NumericDescriptor:
        """Calculated time-of-flight position for this reflection."""
        return self._time_of_flight

    @property
    def parameters(self) -> list:
        """Powder TOF reflection descriptors serialized in CIF loops."""
        return [*super().parameters, self._time_of_flight]
parameters property

Powder TOF reflection descriptors serialized in CIF loops.

time_of_flight property

Calculated time-of-flight position for this reflection.

PowderTofReflnData

Bases: PowderReflnDataBase

Calculated powder reflection collection for TOF experiments.

Source code in src/easydiffraction/datablocks/experiment/categories/refln/bragg_pd.py
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
@ReflnFactory.register
class PowderTofReflnData(PowderReflnDataBase):
    """Calculated powder reflection collection for TOF experiments."""

    type_info = TypeInfo(tag='bragg-pd-tof-refln', description='Bragg powder TOF reflection 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}),
    )

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

    def _set_x_value(
        self,
        *,
        item: PowderReflnBase,
        record: PowderReflnRecord,
    ) -> None:
        del self
        item.time_of_flight._value = float(record.time_of_flight)

    @property
    def time_of_flight(self) -> np.ndarray:
        """Calculated time-of-flight positions for all rows."""
        return np.fromiter((item.time_of_flight.value for item in self._items), dtype=float)
time_of_flight property

Calculated time-of-flight positions for all rows.

bragg_sc

Refln

Bases: CategoryItem

Single reflection for single-crystal diffraction data.

Source code in src/easydiffraction/datablocks/experiment/categories/refln/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/refln/bragg_sc.py
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
@ReflnFactory.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.calculation.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

Reflection collection factory — delegates to FactoryBase.

ReflnFactory

Bases: FactoryBase

Factory for creating reflection collections.

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

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

collection

Collection of experiment data blocks.

Experiments

Bases: DatablockCollection

Collection of Experiment data blocks.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

add_from_cif_path(cif_path)

Add an experiment from a CIF file path.

Parameters:

Name Type Description Default
cif_path str

Path to a CIF document.

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

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

add_from_cif_str(cif_str)

Add an experiment from a CIF string.

Parameters:

Name Type Description Default
cif_str str

Full CIF document as a string.

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

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

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

Add an experiment from a data file path.

Parameters:

Name Type Description Default
name str

Experiment identifier.

required
data_path str

Path to the measured data file.

required
sample_form str | None

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

None
beam_mode str | None

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

None
radiation_probe str | None

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

None
scattering_type str | None

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

None
Source code in src/easydiffraction/datablocks/experiment/collection.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
@typechecked
def add_from_data_path(
    self,
    *,
    name: str,
    data_path: str,
    sample_form: str | None = None,
    beam_mode: str | None = None,
    radiation_probe: str | None = None,
    scattering_type: str | None = None,
) -> None:
    """
    Add an experiment from a data file path.

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

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

Add an experiment without associating a data file.

Parameters:

Name Type Description Default
name str

Experiment identifier.

required
sample_form str | None

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

None
beam_mode str | None

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

None
radiation_probe str | None

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

None
scattering_type str | None

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

None
Source code in src/easydiffraction/datablocks/experiment/collection.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@typechecked
def create(
    self,
    *,
    name: str,
    sample_form: str | None = None,
    beam_mode: str | None = None,
    radiation_probe: str | None = None,
    scattering_type: str | None = None,
) -> None:
    """
    Add an experiment without associating a data file.

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

show_names()

List all experiment names in the collection.

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

show_params()

Show parameters of all experiments in the collection.

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

item

base

Base classes for experiment datablock items.

ExperimentBase

Bases: DatablockItem

Base class for all experiment datablock items.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
 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)
        self._calculation = CalculationFactory.create(
            'default',
            calculator_type=self._default_calculator_tag(),
        )
        self._calculation._parent = self

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

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

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

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

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

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

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

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

        Parameters
        ----------
        block : object
            Parsed ``gemmi.cif.Block`` to read type tags from.
        """
        calculator_type = read_cif_str(block, '_calculation.calculator_type')
        if calculator_type is not None:
            self._set_calculator_type(calculator_type, announce=False)

    def _normalize_switchable_type_descriptors(self) -> None:
        """
        Normalize switchable category descriptors after CIF loading.
        """
        if self._calculator_type is not None:
            self.calculation.calculator_type.value = self._calculator_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."""
        paragraph_title: str = f"Experiment 🔬 '{self.name}' as cif"
        console.paragraph(paragraph_title)
        render_cif(self._cif_for_display())

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

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

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

    # ------------------------------------------------------------------
    #  Calculation (switchable-category pattern)
    # ------------------------------------------------------------------

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

        Holds the selected calculator type and provides access to the
        live calculator backend instance.
        """
        if self._calculator is None:
            self._resolve_calculation()
        return self._calculation

    def _default_calculator_tag(self) -> str:
        """Return the default calculator tag for this experiment."""
        from easydiffraction.analysis.calculators.factory import CalculatorFactory  # noqa: PLC0415

        return CalculatorFactory.default_tag(
            scattering_type=self.type.scattering_type.value,
        )

    def _set_calculator_type(
        self,
        tag: str,
        *,
        announce: bool = True,
    ) -> None:
        """Switch to a different calculator backend."""
        from easydiffraction.analysis.calculators.factory import CalculatorFactory  # noqa: PLC0415

        supported = self._supported_calculator_tags()
        if tag not in supported:
            log.warning(
                f"Unsupported calculator '{tag}' for experiment "
                f"'{self.name}'. Supported: {supported}. "
                f"For more information, use 'calculation.show_calculator_types()'",
            )
            return
        if self._calculator_type == tag and self._calculator is not None:
            if announce:
                console.paragraph(f"Calculator for experiment '{self.name}' already set to")
                console.print(tag)
            return
        self._calculator = CalculatorFactory.create(tag)
        self._calculator_type = tag
        self.calculation.calculator_type.value = tag
        if announce:
            console.paragraph(f"Calculator for experiment '{self.name}' changed to")
            console.print(tag)

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

        tag = self._default_calculator_tag()
        supported = self._supported_calculator_tags()
        if supported and tag not in supported:
            tag = supported[0]
        self._calculator = CalculatorFactory.create(tag)
        self._calculator_type = tag
        self.calculation.calculator_type.value = tag

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

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

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

    def _calculator_support_category(self) -> object | None:
        """
        Return the category that constrains calculator availability.
        """
        return getattr(self, '_data', None) or getattr(self, '_refln', None)

    def _intensity_category(self) -> object:
        """Return the experiment category exposing intensity arrays."""
        msg = f"Experiment '{self.name}' has no intensity category."
        raise AttributeError(msg)
as_cif property

Serialize this experiment to a CIF fragment.

calculation property

The active calculation category for this experiment.

Holds the selected calculator type and provides access to the live calculator backend instance.

diffrn property

Ambient conditions recorded during measurement.

name property writable

Human-readable name of the experiment.

show_as_cif()

Pretty-print the experiment as CIF text.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
145
146
147
148
149
def show_as_cif(self) -> None:
    """Pretty-print the experiment as CIF text."""
    paragraph_title: str = f"Experiment 🔬 '{self.name}' as cif"
    console.paragraph(paragraph_title)
    render_cif(self._cif_for_display())
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
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
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
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)
        self._resolve_calculation()

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

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

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

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

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

        return valid_linked_phases

    @abstractmethod
    def _load_ascii_data_to_experiment(self, data_path: str) -> int:
        """
        Load powder diffraction data from an ASCII file.

        Parameters
        ----------
        data_path : str
            Path to data file with columns compatible with the beam mode
            (e.g. 2theta/I/sigma for CWL, TOF/I/sigma for TOF).

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

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

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

    # ------------------------------------------------------------------
    #  Data (fixed at creation)
    # ------------------------------------------------------------------

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

    def _calculator_support_category(self) -> object | None:
        """
        Return the powder data collection that constrains calculators.
        """
        return self._data

    def _intensity_category(self) -> object:
        """Return the powder intensity data collection."""
        return self._data

    @property
    def peak(self) -> object:
        """Peak category object with profile parameters and mixins."""
        return self._peak

    @property
    def peak_profile_type(self) -> object:
        """Currently selected peak profile type alias."""
        return PeakFactory._local_alias_for(
            self._peak_profile_type,
            **self._peak_profile_context(),
        )

    @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 context-local alias or canonical tag.
        """
        context = self._peak_profile_context()
        supported = PeakFactory.supported_for(
            calculator=self.calculation.calculator_type.value,
            **context,
        )
        supported_tags = [klass.type_info.tag for klass in supported]
        supported_aliases = [
            PeakFactory._local_alias_for(tag, **context) for tag in supported_tags
        ]
        canonical_type = PeakFactory._canonical_tag_for(new_type, **context)

        if canonical_type not in supported_tags:
            log.warning(
                f"Unsupported peak profile '{new_type}'. "
                f'Supported peak profiles: {supported_aliases}. '
                f"For more information, use 'show_peak_profile_types()'",
            )
            return

        if self._peak is not None:
            log.warning(
                'Switching peak profile type discards existing peak parameters.',
            )

        self._peak = PeakFactory.create(canonical_type)
        self._peak_profile_type = canonical_type
        console.paragraph(f"Peak profile type for experiment '{self.name}' changed to")
        console.print(self.peak_profile_type)

    def show_peak_profile_types(self) -> None:
        """Print supported peak profile types and mark current type."""
        supported = PeakFactory.supported_for(
            calculator=self.calculation.calculator_type.value,
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
        )
        context = self._peak_profile_context()
        current = PeakFactory._local_alias_for(self._peak_profile_type, **context)
        columns_data = [
            [
                '*'
                if PeakFactory._local_alias_for(klass.type_info.tag, **context) == current
                else '',
                PeakFactory._local_alias_for(klass.type_info.tag, **context),
                klass.type_info.description,
            ]
            for klass in supported
        ]
        console.paragraph('Peak profile types')
        render_table(
            columns_headers=['', 'Type', 'Description'],
            columns_alignment=['left', 'left', 'left'],
            columns_data=columns_data,
        )

    def _set_peak_profile_type(self, new_type: str) -> None:
        """
        Switch the peak profile type without console output.

        Used internally by the factory when restoring state from CIF so
        that no user-facing warnings or progress messages are emitted.
        Invalid type tags are logged as warnings and ignored.

        Parameters
        ----------
        new_type : str
            Peak profile type alias or canonical tag.
        """
        context = self._peak_profile_context()
        supported = PeakFactory.supported_for(
            **context,
        )
        supported_tags = [klass.type_info.tag for klass in supported]
        canonical_type = PeakFactory._canonical_tag_for(new_type, **context)
        if canonical_type not in supported_tags:
            supported_aliases = [
                PeakFactory._local_alias_for(tag, **context) for tag in supported_tags
            ]
            log.warning(
                f"Unsupported peak profile '{new_type}' in CIF. "
                f'Supported: {supported_aliases}. Keeping default.',
            )
            return
        self._peak = PeakFactory.create(canonical_type)
        self._peak_profile_type = canonical_type

    def _peak_profile_context(self) -> dict[str, object]:
        """
        Return the context that resolves local peak profile aliases.
        """
        return {
            'scattering_type': self.type.scattering_type.value,
            'beam_mode': self.type.beam_mode.value,
        }

    def _normalize_switchable_type_descriptors(self) -> None:
        """
        Normalize switchable category descriptors after CIF loading.
        """
        super()._normalize_switchable_type_descriptors()
        self.peak.profile_type.value = self._peak_profile_type

    def _restore_switchable_types(self, block: object) -> None:
        """
        Restore switchable category types for powder experiments.

        Reads ``_peak.profile_type`` from the CIF block and switches to
        the matching peak implementation before category parameters are
        loaded, ensuring profile-specific descriptors are present.

        Parameters
        ----------
        block : object
            Parsed ``gemmi.cif.Block`` to read type tags from.
        """
        super()._restore_switchable_types(block)
        peak_type = read_cif_str(block, '_peak.profile_type')
        if peak_type is not None:
            self._set_peak_profile_type(peak_type)
data property

Data collection for this experiment.

excluded_regions property

Collection of excluded regions for the x-grid.

linked_phases property

Collection of phases linked to this experiment.

peak property

Peak category object with profile parameters and mixins.

peak_profile_type property writable

Currently selected peak profile type alias.

show_peak_profile_types()

Print supported peak profile types and mark current type.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
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
def show_peak_profile_types(self) -> None:
    """Print supported peak profile types and mark current type."""
    supported = PeakFactory.supported_for(
        calculator=self.calculation.calculator_type.value,
        scattering_type=self.type.scattering_type.value,
        beam_mode=self.type.beam_mode.value,
    )
    context = self._peak_profile_context()
    current = PeakFactory._local_alias_for(self._peak_profile_type, **context)
    columns_data = [
        [
            '*'
            if PeakFactory._local_alias_for(klass.type_info.tag, **context) == current
            else '',
            PeakFactory._local_alias_for(klass.type_info.tag, **context),
            klass.type_info.description,
        ]
        for klass in supported
    ]
    console.paragraph('Peak profile types')
    render_table(
        columns_headers=['', 'Type', 'Description'],
        columns_alignment=['left', 'left', 'left'],
        columns_data=columns_data,
    )

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
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._refln_type: str = ReflnFactory.default_tag(
            sample_form=self.type.sample_form.value,
            beam_mode=self.type.beam_mode.value,
            scattering_type=self.type.scattering_type.value,
        )
        self._refln = ReflnFactory.create(self._refln_type)
        self._resolve_calculation()

    @abstractmethod
    def _load_ascii_data_to_experiment(self, data_path: str) -> None:
        """
        Load single crystal data from an ASCII file.

        Parameters
        ----------
        data_path : str
            Path to data file with columns compatible with the beam
            mode.
        """

    # ------------------------------------------------------------------
    #  Extinction (switchable-category pattern)
    # ------------------------------------------------------------------

    @property
    def extinction(self) -> object:
        """Active extinction correction model."""
        return self._extinction

    @property
    def extinction_type(self) -> str:
        """Tag of the active extinction correction model."""
        return self._extinction_type

    @extinction_type.setter
    def extinction_type(self, new_type: str) -> None:
        """
        Switch to a different extinction correction model.

        Parameters
        ----------
        new_type : str
            Extinction tag (e.g. ``'becker-coppens'``).
        """
        supported = ExtinctionFactory.supported_for(
            calculator=self.calculation.calculator_type.value,
        )
        supported_tags = [k.type_info.tag for k in supported]
        if new_type not in supported_tags:
            log.warning(
                f"Unsupported extinction type '{new_type}'. "
                f'Supported: {supported_tags}. '
                f"For more information, use 'show_extinction_types()'",
            )
            return
        self._extinction = ExtinctionFactory.create(new_type)
        self._extinction_type = new_type
        console.paragraph('Extinction type changed to')
        console.print(new_type)

    def show_extinction_types(self) -> None:
        """Print supported extinction types and mark current type."""
        supported = ExtinctionFactory.supported_for(
            calculator=self.calculation.calculator_type.value,
        )
        columns_data = [
            [
                '*' if klass.type_info.tag == self._extinction_type else '',
                klass.type_info.tag,
                klass.type_info.description,
            ]
            for klass in supported
        ]
        console.paragraph('Extinction types')
        render_table(
            columns_headers=['', 'Type', 'Description'],
            columns_alignment=['left', 'left', 'left'],
            columns_data=columns_data,
        )

    # ------------------------------------------------------------------
    #  Linked crystal (read-only, single type)
    # ------------------------------------------------------------------

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

    # ------------------------------------------------------------------
    #  Instrument (fixed at creation)
    # ------------------------------------------------------------------

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

    @property
    def refln(self) -> object:
        """Reflection collection for this experiment."""
        return self._refln

    def _calculator_support_category(self) -> object | None:
        """
        Return the reflection collection that constrains calculators.
        """
        return self._refln

    def _intensity_category(self) -> object:
        """Return the single-crystal reflection collection."""
        return self._refln
extinction property

Active extinction correction model.

extinction_type property writable

Tag of the active extinction correction model.

instrument property

Active instrument model for this experiment.

linked_crystal property

Linked crystal model for this experiment.

refln property

Reflection collection for this experiment.

show_extinction_types()

Print supported extinction types and mark current type.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
def show_extinction_types(self) -> None:
    """Print supported extinction types and mark current type."""
    supported = ExtinctionFactory.supported_for(
        calculator=self.calculation.calculator_type.value,
    )
    columns_data = [
        [
            '*' if klass.type_info.tag == self._extinction_type else '',
            klass.type_info.tag,
            klass.type_info.description,
        ]
        for klass in supported
    ]
    console.paragraph('Extinction types')
    render_table(
        columns_headers=['', 'Type', 'Description'],
        columns_alignment=['left', 'left', 'left'],
        columns_data=columns_data,
    )

intensity_category_for(experiment)

Return the category exposing measured and calculated values.

Source code in src/easydiffraction/datablocks/experiment/item/base.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def intensity_category_for(experiment: object) -> object:
    """Return the category exposing measured and calculated values."""
    resolver = getattr(experiment, '_intensity_category', None)
    if callable(resolver):
        return resolver()

    data = getattr(experiment, 'data', None)
    if data is not None:
        return data

    refln = getattr(experiment, 'refln', None)
    if refln is not None:
        return refln

    name = getattr(experiment, 'name', type(experiment).__name__)
    msg = f"Experiment '{name}' has no intensity category."
    raise AttributeError(msg)

bragg_pd

BraggPdExperiment

Bases: PdExperimentBase

Standard Bragg powder diffraction experiment.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_pd.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
@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)
        self._refln = None
        self._sync_refln_category()

    def _refln_collection_tag(self) -> str:
        """
        Return the reflection-collection tag for this beam mode.
        """
        return ReflnFactory.default_tag(
            sample_form=self.type.sample_form.value,
            beam_mode=self.type.beam_mode.value,
            scattering_type=self.type.scattering_type.value,
        )

    def _refln_collection_type(self) -> type[object]:
        """
        Return the reflection-collection type for this beam mode.
        """
        refln_tag = self._refln_collection_tag()
        return ReflnFactory._supported_map()[refln_tag]

    def _sync_refln_category(self) -> None:
        """Create or remove ``refln`` for the active calculator."""
        calculator_type = self._calculator_type or self._default_calculator_tag()
        refln_collection_type = self._refln_collection_type()
        calculator = CalculatorEnum(calculator_type)
        if refln_collection_type.calculator_support.supports(calculator):
            if not isinstance(self._refln, refln_collection_type):
                self._refln = ReflnFactory.create(self._refln_collection_tag())
            return

        self._refln = None

    def _set_calculator_type(
        self,
        tag: str,
        *,
        announce: bool = True,
    ) -> None:
        """Switch calculator backend and sync ``refln`` availability."""
        super()._set_calculator_type(tag, announce=announce)
        self._sync_refln_category()

    def _load_ascii_data_to_experiment(
        self,
        data_path: str,
    ) -> int:
        """
        Load (x, y, sy) data from an ASCII file into the data category.

        The file format is space/column separated with 2 or 3 columns:
        ``x y [sy]``. If ``sy`` is missing, it is approximated as
        ``sqrt(y)``.

        If ``sy`` has values smaller than ``0.0001``, they are replaced
        with ``1.0``.

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

        Returns
        -------
        int
            Number of loaded data points.
        """
        data = load_numeric_block(data_path)

        if data.shape[1] < _MIN_COLUMNS_XY:
            log.error(
                'Data file must have at least two columns: x and y.',
                exc_type=ValueError,
            )
            return 0

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

        # Extract x, y data
        x = data[:, 0]
        y = data[:, 1]

        # Round x to 4 decimal places
        x = np.round(x, 4)

        # Determine sy from column 3 if available, otherwise use sqrt(y)
        sy = data[:, 2] if data.shape[1] > _MIN_COLUMNS_XY else np.sqrt(y)

        # Replace values smaller than _MIN_UNCERTAINTY with 1.0
        # TODO: Not used if loading from cif file?
        sy = np.where(sy < _MIN_UNCERTAINTY, 1.0, sy)

        # Set the experiment data
        self.data._create_items_set_xcoord_and_id(x)
        self.data._set_intensity_meas(y)
        self.data._set_intensity_meas_su(sy)

        return len(x)

    # ------------------------------------------------------------------
    #  Instrument (fixed at creation)
    # ------------------------------------------------------------------

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

    @property
    def refln(self) -> object | None:
        """Calculated reflection metadata when supported."""
        return self._refln

    # ------------------------------------------------------------------
    #  Background (switchable-category pattern)
    # ------------------------------------------------------------------

    @property
    def background_type(self) -> object:
        """Current background type enum value."""
        return self._background_type

    @background_type.setter
    def background_type(self, new_type: str) -> None:
        """Set a new background type and recreate background object."""
        if self._background_type == new_type:
            console.paragraph(f"Background type for experiment '{self.name}' already set to")
            console.print(new_type)
            return

        supported = BackgroundFactory.supported_for(
            calculator=self.calculation.calculator_type.value,
        )
        supported_tags = [k.type_info.tag for k in supported]
        if new_type not in supported_tags:
            log.warning(
                f"Unsupported background type '{new_type}'. "
                f'Supported: {supported_tags}. '
                f"For more information, use 'show_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_background_types(self) -> None:
        """Print supported background types and mark current type."""
        supported = BackgroundFactory.supported_for(
            calculator=self.calculation.calculator_type.value,
        )
        columns_data = [
            [
                '*' if klass.type_info.tag == self._background_type else '',
                klass.type_info.tag,
                klass.type_info.description,
            ]
            for klass in supported
        ]
        console.paragraph('Background types')
        render_table(
            columns_headers=['', 'Type', 'Description'],
            columns_alignment=['left', 'left', 'left'],
            columns_data=columns_data,
        )
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.

refln property

Calculated reflection metadata when supported.

show_background_types()

Print supported background types and mark current type.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_pd.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def show_background_types(self) -> None:
    """Print supported background types and mark current type."""
    supported = BackgroundFactory.supported_for(
        calculator=self.calculation.calculator_type.value,
    )
    columns_data = [
        [
            '*' if klass.type_info.tag == self._background_type else '',
            klass.type_info.tag,
            klass.type_info.description,
        ]
        for klass in supported
    ]
    console.paragraph('Background types')
    render_table(
        columns_headers=['', 'Type', 'Description'],
        columns_alignment=['left', 'left', 'left'],
        columns_data=columns_data,
    )

bragg_sc

CwlScExperiment

Bases: ScExperimentBase

Bragg constant-wavelength single-crystal experiment.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_sc.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
@ExperimentFactory.register
class CwlScExperiment(ScExperimentBase):
    """Bragg constant-wavelength single-crystal experiment."""

    type_info = TypeInfo(
        tag='bragg-sc-cwl',
        description='Bragg CWL single-crystal experiment',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH}),
    )

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

    def _load_ascii_data_to_experiment(self, data_path: str) -> int:
        """
        Load measured data from an ASCII file into the refln category.

        The file format is space/column separated with 5 columns: ``h k
        l Iobs sIobs``.

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

        Returns
        -------
        int
            Number of loaded data points.
        """
        data = load_numeric_block(data_path)

        if data.shape[1] < _MIN_COLUMNS_CWL_SC:
            log.error(
                'Data file must have at least 5 columns: h, k, l, Iobs, sIobs.',
                exc_type=ValueError,
            )
            return 0

        # Extract Miller indices h, k, l
        indices_h = data[:, 0].astype(int)
        indices_k = data[:, 1].astype(int)
        indices_l = data[:, 2].astype(int)

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

        # Set the experiment reflections
        self.refln._create_items_set_hkl_and_id(indices_h, indices_k, indices_l)
        self.refln._set_intensity_meas(integrated_intensities)
        self.refln._set_intensity_meas_su(integrated_intensities_su)

        return len(indices_h)

TofScExperiment

Bases: ScExperimentBase

Bragg time-of-flight single-crystal experiment.

Source code in src/easydiffraction/datablocks/experiment/item/bragg_sc.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
@ExperimentFactory.register
class TofScExperiment(ScExperimentBase):
    """Bragg time-of-flight single-crystal experiment."""

    type_info = TypeInfo(
        tag='bragg-sc-tof',
        description='Bragg TOF single-crystal experiment',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.BRAGG}),
        sample_form=frozenset({SampleFormEnum.SINGLE_CRYSTAL}),
        beam_mode=frozenset({BeamModeEnum.TIME_OF_FLIGHT}),
    )

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

    def _load_ascii_data_to_experiment(self, data_path: str) -> int:
        """
        Load measured data from an ASCII file into the refln category.

        The file format is space/column separated with 6 columns: ``h k
        l Iobs sIobs wavelength``.

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

        Returns
        -------
        int
            Number of loaded data points.
        """
        try:
            data = load_numeric_block(data_path)
        except OSError as e:
            log.error(
                f'Failed to read data from {data_path}: {e}',
                exc_type=IOError,
            )
            return 0

        if data.shape[1] < _MIN_COLUMNS_TOF_SC:
            log.error(
                'Data file must have at least 6 columns: h, k, l, Iobs, sIobs, wavelength.',
                exc_type=ValueError,
            )
            return 0

        # Extract Miller indices h, k, l
        indices_h = data[:, 0].astype(int)
        indices_k = data[:, 1].astype(int)
        indices_l = data[:, 2].astype(int)

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

        # Extract wavelength values
        wavelength = data[:, 5]

        # Set the experiment reflections
        self.refln._create_items_set_hkl_and_id(indices_h, indices_k, indices_l)
        self.refln._set_intensity_meas(integrated_intensities)
        self.refln._set_intensity_meas_su(integrated_intensities_su)
        self.refln._set_wavelength(wavelength)

        return len(indices_h)

enums

Enumerations for experiment configuration (forms, modes, types).

BeamModeEnum

Bases: StrEnum

Beam delivery mode for the instrument.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
class BeamModeEnum(StrEnum):
    """Beam delivery mode for the instrument."""

    # TODO: Rename to CWL and TOF
    CONSTANT_WAVELENGTH = 'constant wavelength'
    TIME_OF_FLIGHT = 'time-of-flight'

    @classmethod
    def default(cls) -> 'BeamModeEnum':
        """
        Return the default beam mode (CONSTANT_WAVELENGTH).

        Returns
        -------
        'BeamModeEnum'
            The default enum member.
        """
        return cls.CONSTANT_WAVELENGTH

    def description(self) -> str:
        """
        Return a human-readable description of this beam mode.

        Returns
        -------
        str
            Description string for the current enum member.
        """
        if self is BeamModeEnum.CONSTANT_WAVELENGTH:
            return 'Constant wavelength (CW) diffraction.'
        if self is BeamModeEnum.TIME_OF_FLIGHT:
            return 'Time-of-flight (TOF) diffraction.'
        return None
default() classmethod

Return the default beam mode (CONSTANT_WAVELENGTH).

Returns:

Type Description
BeamModeEnum

The default enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
117
118
119
120
121
122
123
124
125
126
127
@classmethod
def default(cls) -> 'BeamModeEnum':
    """
    Return the default beam mode (CONSTANT_WAVELENGTH).

    Returns
    -------
    'BeamModeEnum'
        The default enum member.
    """
    return cls.CONSTANT_WAVELENGTH
description()

Return a human-readable description of this beam mode.

Returns:

Type Description
str

Description string for the current enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def description(self) -> str:
    """
    Return a human-readable description of this beam mode.

    Returns
    -------
    str
        Description string for the current enum member.
    """
    if self is BeamModeEnum.CONSTANT_WAVELENGTH:
        return 'Constant wavelength (CW) diffraction.'
    if self is BeamModeEnum.TIME_OF_FLIGHT:
        return 'Time-of-flight (TOF) diffraction.'
    return None

CalculatorEnum

Bases: StrEnum

Known calculation engine identifiers.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
145
146
147
148
149
150
class CalculatorEnum(StrEnum):
    """Known calculation engine identifiers."""

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

ExtinctionModelEnum

Bases: StrEnum

Mosaicity distribution model for Becker-Coppens extinction.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
class ExtinctionModelEnum(StrEnum):
    """Mosaicity distribution model for Becker-Coppens extinction."""

    GAUSS = 'gauss'
    LORENTZ = 'lorentz'

    @classmethod
    def default(cls) -> 'ExtinctionModelEnum':
        """
        Return the default extinction model (GAUSS).

        Returns
        -------
        'ExtinctionModelEnum'
            The default enum member.
        """
        return cls.GAUSS

    def description(self) -> str:
        """
        Return a human-readable description of this extinction model.

        Returns
        -------
        str
            Description string for the current enum member.
        """
        if self is ExtinctionModelEnum.GAUSS:
            return 'Gaussian mosaicity distribution for extinction correction.'
        if self is ExtinctionModelEnum.LORENTZ:
            return 'Lorentzian mosaicity distribution for extinction correction.'
        return None
default() classmethod

Return the default extinction model (GAUSS).

Returns:

Type Description
ExtinctionModelEnum

The default enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
249
250
251
252
253
254
255
256
257
258
259
@classmethod
def default(cls) -> 'ExtinctionModelEnum':
    """
    Return the default extinction model (GAUSS).

    Returns
    -------
    'ExtinctionModelEnum'
        The default enum member.
    """
    return cls.GAUSS
description()

Return a human-readable description of this extinction model.

Returns:

Type Description
str

Description string for the current enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
def description(self) -> str:
    """
    Return a human-readable description of this extinction model.

    Returns
    -------
    str
        Description string for the current enum member.
    """
    if self is ExtinctionModelEnum.GAUSS:
        return 'Gaussian mosaicity distribution for extinction correction.'
    if self is ExtinctionModelEnum.LORENTZ:
        return 'Lorentzian mosaicity distribution for extinction correction.'
    return None

PeakProfileTypeEnum

Bases: StrEnum

Available peak profile types per scattering and beam mode.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
class PeakProfileTypeEnum(StrEnum):
    """Available peak profile types per scattering and beam mode."""

    CWL_PSEUDO_VOIGT = 'cwl-pseudo-voigt'
    CWL_PSEUDO_VOIGT_EMPIRICAL_ASYMMETRY = 'cwl-pseudo-voigt-empirical-asymmetry'
    CWL_THOMPSON_COX_HASTINGS = 'cwl-thompson-cox-hastings'
    TOF_PSEUDO_VOIGT = 'tof-pseudo-voigt'
    TOF_JORGENSEN = 'tof-jorgensen'
    TOF_JORGENSEN_VON_DREELE = 'tof-jorgensen-von-dreele'
    TOF_DOUBLE_JORGENSEN_VON_DREELE = 'tof-double-jorgensen-von-dreele'
    TOTAL_GAUSSIAN_DAMPED_SINC = 'total-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.CWL_PSEUDO_VOIGT),
            (
                ScatteringTypeEnum.BRAGG,
                BeamModeEnum.TIME_OF_FLIGHT,
            ): cls.TOF_JORGENSEN,
            (ScatteringTypeEnum.TOTAL, BeamModeEnum.CONSTANT_WAVELENGTH): (
                cls.TOTAL_GAUSSIAN_DAMPED_SINC
            ),
            (ScatteringTypeEnum.TOTAL, BeamModeEnum.TIME_OF_FLIGHT): (
                cls.TOTAL_GAUSSIAN_DAMPED_SINC
            ),
        }[scattering_type, beam_mode]

    def description(self) -> str:  # noqa: PLR0911
        """
        Return a human-readable description of this peak profile type.

        Returns
        -------
        str
            Description string for the current enum member.
        """
        if self is PeakProfileTypeEnum.CWL_PSEUDO_VOIGT:
            return 'CWL pseudo-Voigt profile'
        if self is PeakProfileTypeEnum.CWL_PSEUDO_VOIGT_EMPIRICAL_ASYMMETRY:
            return 'CWL pseudo-Voigt profile with empirical asymmetry correction.'
        if self is PeakProfileTypeEnum.CWL_THOMPSON_COX_HASTINGS:
            return 'CWL Thompson-Cox-Hastings profile with FCJ asymmetry correction.'
        if self is PeakProfileTypeEnum.TOF_PSEUDO_VOIGT:
            return 'TOF non-convoluted pseudo-Voigt profile'
        if self is PeakProfileTypeEnum.TOF_JORGENSEN:
            return 'TOF Jorgensen profile: back-to-back exponentials ⊗ Gaussian'
        if self is PeakProfileTypeEnum.TOF_JORGENSEN_VON_DREELE:
            return 'TOF Jorgensen-Von Dreele profile: back-to-back exponentials ⊗ pseudo-Voigt'
        if self is PeakProfileTypeEnum.TOF_DOUBLE_JORGENSEN_VON_DREELE:
            return (
                'TOF Double-Jorgensen-Von Dreele profile: double back-to-back '
                'exponentials ⊗ pseudo-Voigt (Z-Rietveld type0m)'
            )
        if self is PeakProfileTypeEnum.TOTAL_GAUSSIAN_DAMPED_SINC:
            return 'Total-scattering Gaussian-damped sinc profile for PDF analysis.'
        return None
default(scattering_type=None, beam_mode=None) classmethod

Return the default peak profile type for a given mode.

Parameters:

Name Type Description Default
scattering_type ScatteringTypeEnum | None

Scattering type; defaults to ScatteringTypeEnum.default() when None.

None
beam_mode BeamModeEnum | None

Beam mode; defaults to BeamModeEnum.default() when None.

None

Returns:

Type Description
PeakProfileTypeEnum

The default profile type for the given combination.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
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
@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.CWL_PSEUDO_VOIGT),
        (
            ScatteringTypeEnum.BRAGG,
            BeamModeEnum.TIME_OF_FLIGHT,
        ): cls.TOF_JORGENSEN,
        (ScatteringTypeEnum.TOTAL, BeamModeEnum.CONSTANT_WAVELENGTH): (
            cls.TOTAL_GAUSSIAN_DAMPED_SINC
        ),
        (ScatteringTypeEnum.TOTAL, BeamModeEnum.TIME_OF_FLIGHT): (
            cls.TOTAL_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
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
def description(self) -> str:  # noqa: PLR0911
    """
    Return a human-readable description of this peak profile type.

    Returns
    -------
    str
        Description string for the current enum member.
    """
    if self is PeakProfileTypeEnum.CWL_PSEUDO_VOIGT:
        return 'CWL pseudo-Voigt profile'
    if self is PeakProfileTypeEnum.CWL_PSEUDO_VOIGT_EMPIRICAL_ASYMMETRY:
        return 'CWL pseudo-Voigt profile with empirical asymmetry correction.'
    if self is PeakProfileTypeEnum.CWL_THOMPSON_COX_HASTINGS:
        return 'CWL Thompson-Cox-Hastings profile with FCJ asymmetry correction.'
    if self is PeakProfileTypeEnum.TOF_PSEUDO_VOIGT:
        return 'TOF non-convoluted pseudo-Voigt profile'
    if self is PeakProfileTypeEnum.TOF_JORGENSEN:
        return 'TOF Jorgensen profile: back-to-back exponentials ⊗ Gaussian'
    if self is PeakProfileTypeEnum.TOF_JORGENSEN_VON_DREELE:
        return 'TOF Jorgensen-Von Dreele profile: back-to-back exponentials ⊗ pseudo-Voigt'
    if self is PeakProfileTypeEnum.TOF_DOUBLE_JORGENSEN_VON_DREELE:
        return (
            'TOF Double-Jorgensen-Von Dreele profile: double back-to-back '
            'exponentials ⊗ pseudo-Voigt (Z-Rietveld type0m)'
        )
    if self is PeakProfileTypeEnum.TOTAL_GAUSSIAN_DAMPED_SINC:
        return 'Total-scattering Gaussian-damped sinc profile for PDF analysis.'
    return None

RadiationProbeEnum

Bases: StrEnum

Incident radiation probe used in the experiment.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
class RadiationProbeEnum(StrEnum):
    """Incident radiation probe used in the experiment."""

    NEUTRON = 'neutron'
    XRAY = 'xray'

    @classmethod
    def default(cls) -> 'RadiationProbeEnum':
        """
        Return the default radiation probe (NEUTRON).

        Returns
        -------
        'RadiationProbeEnum'
            The default enum member.
        """
        return cls.NEUTRON

    def description(self) -> str:
        """
        Return a human-readable description of this radiation probe.

        Returns
        -------
        str
            Description string for the current enum member.
        """
        if self is RadiationProbeEnum.NEUTRON:
            return 'Neutron diffraction.'
        if self is RadiationProbeEnum.XRAY:
            return 'X-ray diffraction.'
        return None
default() classmethod

Return the default radiation probe (NEUTRON).

Returns:

Type Description
RadiationProbeEnum

The default enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
82
83
84
85
86
87
88
89
90
91
92
@classmethod
def default(cls) -> 'RadiationProbeEnum':
    """
    Return the default radiation probe (NEUTRON).

    Returns
    -------
    'RadiationProbeEnum'
        The default enum member.
    """
    return cls.NEUTRON
description()

Return a human-readable description of this radiation probe.

Returns:

Type Description
str

Description string for the current enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def description(self) -> str:
    """
    Return a human-readable description of this radiation probe.

    Returns
    -------
    str
        Description string for the current enum member.
    """
    if self is RadiationProbeEnum.NEUTRON:
        return 'Neutron diffraction.'
    if self is RadiationProbeEnum.XRAY:
        return 'X-ray diffraction.'
    return None

SampleFormEnum

Bases: StrEnum

Physical sample form supported by experiments.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class SampleFormEnum(StrEnum):
    """Physical sample form supported by experiments."""

    POWDER = 'powder'
    SINGLE_CRYSTAL = 'single crystal'

    @classmethod
    def default(cls) -> 'SampleFormEnum':
        """
        Return the default sample form (POWDER).

        Returns
        -------
        'SampleFormEnum'
            The default enum member.
        """
        return cls.POWDER

    def description(self) -> str:
        """
        Return a human-readable description of this sample form.

        Returns
        -------
        str
            Description string for the current enum member.
        """
        if self is SampleFormEnum.POWDER:
            return 'Powdered or polycrystalline sample.'
        if self is SampleFormEnum.SINGLE_CRYSTAL:
            return 'Single crystal sample.'
        return ''
default() classmethod

Return the default sample form (POWDER).

Returns:

Type Description
SampleFormEnum

The default enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
14
15
16
17
18
19
20
21
22
23
24
@classmethod
def default(cls) -> 'SampleFormEnum':
    """
    Return the default sample form (POWDER).

    Returns
    -------
    'SampleFormEnum'
        The default enum member.
    """
    return cls.POWDER
description()

Return a human-readable description of this sample form.

Returns:

Type Description
str

Description string for the current enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def description(self) -> str:
    """
    Return a human-readable description of this sample form.

    Returns
    -------
    str
        Description string for the current enum member.
    """
    if self is SampleFormEnum.POWDER:
        return 'Powdered or polycrystalline sample.'
    if self is SampleFormEnum.SINGLE_CRYSTAL:
        return 'Single crystal sample.'
    return ''

ScatteringTypeEnum

Bases: StrEnum

Type of scattering modeled in an experiment.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
class ScatteringTypeEnum(StrEnum):
    """Type of scattering modeled in an experiment."""

    BRAGG = 'bragg'
    TOTAL = 'total'

    @classmethod
    def default(cls) -> 'ScatteringTypeEnum':
        """
        Return the default scattering type (BRAGG).

        Returns
        -------
        'ScatteringTypeEnum'
            The default enum member.
        """
        return cls.BRAGG

    def description(self) -> str:
        """
        Return a human-readable description of this scattering type.

        Returns
        -------
        str
            Description string for the current enum member.
        """
        if self is ScatteringTypeEnum.BRAGG:
            return 'Bragg diffraction for conventional structure refinement.'
        if self is ScatteringTypeEnum.TOTAL:
            return 'Total scattering for pair distribution function analysis (PDF).'
        return ''
default() classmethod

Return the default scattering type (BRAGG).

Returns:

Type Description
ScatteringTypeEnum

The default enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
48
49
50
51
52
53
54
55
56
57
58
@classmethod
def default(cls) -> 'ScatteringTypeEnum':
    """
    Return the default scattering type (BRAGG).

    Returns
    -------
    'ScatteringTypeEnum'
        The default enum member.
    """
    return cls.BRAGG
description()

Return a human-readable description of this scattering type.

Returns:

Type Description
str

Description string for the current enum member.

Source code in src/easydiffraction/datablocks/experiment/item/enums.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def description(self) -> str:
    """
    Return a human-readable description of this scattering type.

    Returns
    -------
    str
        Description string for the current enum member.
    """
    if self is ScatteringTypeEnum.BRAGG:
        return 'Bragg diffraction for conventional structure refinement.'
    if self is ScatteringTypeEnum.TOTAL:
        return 'Total scattering for pair distribution function analysis (PDF).'
    return ''

factory

Factory for creating experiment instances from various inputs.

Provides individual class methods for each creation pathway: from_cif_path, from_cif_str, from_data_path, and from_scratch.

ExperimentFactory

Bases: FactoryBase

Creates Experiment instances with only relevant attributes.

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

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

    # TODO: Add to core/factory.py?
    def __init__(self) -> None:
        log.error(
            'Experiment objects must be created using class methods such as '
            '`ExperimentFactory.from_cif_str(...)`, etc.'
        )

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

    @classmethod
    @typechecked
    def _create_experiment_type(
        cls,
        sample_form: str | None = None,
        beam_mode: str | None = None,
        radiation_probe: str | None = None,
        scattering_type: str | None = None,
    ) -> ExperimentType:
        """Construct ExperimentType with defaults for omitted values."""
        # Note: validation of input values is done via Descriptor setter
        # methods

        et = ExperimentType()

        if sample_form is not None:
            et._set_sample_form(sample_form)
        if beam_mode is not None:
            et._set_beam_mode(beam_mode)
        if radiation_probe is not None:
            et._set_radiation_probe(radiation_probe)
        if scattering_type is not None:
            et._set_scattering_type(scattering_type)

        return et

    @classmethod
    @typechecked
    def _resolve_class(cls, expt_type: ExperimentType) -> type:
        """Look up the experiment class from the type enums."""
        tag = cls.default_tag(
            scattering_type=expt_type.scattering_type.value,
            sample_form=expt_type.sample_form.value,
            beam_mode=expt_type.beam_mode.value,
        )
        return cls._supported_map()[tag]

    @classmethod
    # TODO: @typechecked fails to find gemmi?
    def _from_gemmi_block(
        cls,
        block: gemmi.cif.Block,
    ) -> ExperimentBase:
        """Build a model instance from a single CIF block."""
        name = name_from_block(block)

        expt_type = ExperimentType()
        for param in expt_type.parameters:
            param.from_cif(block)

        expt_class = cls._resolve_class(expt_type)
        expt_obj = expt_class(name=name, type=expt_type)

        # Restore switchable category types before loading parameters
        # so implementation-specific descriptors exist for from_cif.
        expt_obj._restore_switchable_types(block)

        for category in expt_obj.categories:
            category.from_cif(block)

        expt_obj._normalize_switchable_type_descriptors()

        return expt_obj

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

    @classmethod
    @typechecked
    def from_scratch(
        cls,
        *,
        name: str,
        sample_form: str | None = None,
        beam_mode: str | None = None,
        radiation_probe: str | None = None,
        scattering_type: str | None = None,
    ) -> ExperimentBase:
        """
        Create an experiment without measured data.

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

        Returns
        -------
        ExperimentBase
            An experiment instance with only metadata.
        """
        expt_type = cls._create_experiment_type(
            sample_form=sample_form,
            beam_mode=beam_mode,
            radiation_probe=radiation_probe,
            scattering_type=scattering_type,
        )
        expt_class = cls._resolve_class(expt_type)
        return expt_class(name=name, type=expt_type)

    # TODO: add minimal default configuration for missing parameters
    @classmethod
    @typechecked
    def from_cif_str(
        cls,
        cif_str: str,
    ) -> ExperimentBase:
        """
        Create an experiment from a CIF string.

        Parameters
        ----------
        cif_str : str
            Full CIF document as a string.

        Returns
        -------
        ExperimentBase
            A populated experiment instance.
        """
        doc = document_from_string(cif_str)
        block = pick_sole_block(doc)
        return cls._from_gemmi_block(block)

    # TODO: Read content and call self.from_cif_str
    @classmethod
    @typechecked
    def from_cif_path(
        cls,
        cif_path: str,
    ) -> ExperimentBase:
        """
        Create an experiment from a CIF file path.

        Parameters
        ----------
        cif_path : str
            Path to a CIF file.

        Returns
        -------
        ExperimentBase
            A populated experiment instance.
        """
        doc = document_from_path(cif_path)
        block = pick_sole_block(doc)
        return cls._from_gemmi_block(block)

    @classmethod
    @typechecked
    def from_data_path(
        cls,
        *,
        name: str,
        data_path: str,
        sample_form: str | None = None,
        beam_mode: str | None = None,
        radiation_probe: str | None = None,
        scattering_type: str | None = None,
    ) -> ExperimentBase:
        """
        Create an experiment from a raw data ASCII file.

        Parameters
        ----------
        name : str
            Experiment identifier.
        data_path : str
            Path to the measured data file.
        sample_form : str | None, default=None
            Sample form (e.g. ``'powder'``).
        beam_mode : str | None, default=None
            Beam mode (e.g. ``'constant wavelength'``).
        radiation_probe : str | None, default=None
            Radiation probe (e.g. ``'neutron'``).
        scattering_type : str | None, default=None
            Scattering type (e.g. ``'bragg'``).

        Returns
        -------
        ExperimentBase
            An experiment instance with measured data attached.
        """
        expt_obj = cls.from_scratch(
            name=name,
            sample_form=sample_form,
            beam_mode=beam_mode,
            radiation_probe=radiation_probe,
            scattering_type=scattering_type,
        )

        expt_obj._load_ascii_data_to_experiment(data_path)

        return expt_obj
from_cif_path(cif_path) classmethod

Create an experiment from a CIF file path.

Parameters:

Name Type Description Default
cif_path str

Path to a CIF file.

required

Returns:

Type Description
ExperimentBase

A populated experiment instance.

Source code in src/easydiffraction/datablocks/experiment/item/factory.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
@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
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
@classmethod
@typechecked
def from_cif_str(
    cls,
    cif_str: str,
) -> ExperimentBase:
    """
    Create an experiment from a CIF string.

    Parameters
    ----------
    cif_str : str
        Full CIF document as a string.

    Returns
    -------
    ExperimentBase
        A populated experiment instance.
    """
    doc = document_from_string(cif_str)
    block = pick_sole_block(doc)
    return cls._from_gemmi_block(block)
from_data_path(*, name, data_path, sample_form=None, beam_mode=None, radiation_probe=None, scattering_type=None) classmethod

Create an experiment from a raw data ASCII file.

Parameters:

Name Type Description Default
name str

Experiment identifier.

required
data_path str

Path to the measured data file.

required
sample_form str | None

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

None
beam_mode str | None

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

None
radiation_probe str | None

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

None
scattering_type str | None

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

None

Returns:

Type Description
ExperimentBase

An experiment instance with measured data attached.

Source code in src/easydiffraction/datablocks/experiment/item/factory.py
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
@classmethod
@typechecked
def from_data_path(
    cls,
    *,
    name: str,
    data_path: str,
    sample_form: str | None = None,
    beam_mode: str | None = None,
    radiation_probe: str | None = None,
    scattering_type: str | None = None,
) -> ExperimentBase:
    """
    Create an experiment from a raw data ASCII file.

    Parameters
    ----------
    name : str
        Experiment identifier.
    data_path : str
        Path to the measured data file.
    sample_form : str | None, default=None
        Sample form (e.g. ``'powder'``).
    beam_mode : str | None, default=None
        Beam mode (e.g. ``'constant wavelength'``).
    radiation_probe : str | None, default=None
        Radiation probe (e.g. ``'neutron'``).
    scattering_type : str | None, default=None
        Scattering type (e.g. ``'bragg'``).

    Returns
    -------
    ExperimentBase
        An experiment instance with measured data attached.
    """
    expt_obj = cls.from_scratch(
        name=name,
        sample_form=sample_form,
        beam_mode=beam_mode,
        radiation_probe=radiation_probe,
        scattering_type=scattering_type,
    )

    expt_obj._load_ascii_data_to_experiment(data_path)

    return expt_obj
from_scratch(*, name, sample_form=None, beam_mode=None, radiation_probe=None, scattering_type=None) classmethod

Create an experiment without measured data.

Parameters:

Name Type Description Default
name str

Experiment identifier.

required
sample_form str | None

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

None
beam_mode str | None

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

None
radiation_probe str | None

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

None
scattering_type str | None

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

None

Returns:

Type Description
ExperimentBase

An experiment instance with only metadata.

Source code in src/easydiffraction/datablocks/experiment/item/factory.py
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
@classmethod
@typechecked
def from_scratch(
    cls,
    *,
    name: str,
    sample_form: str | None = None,
    beam_mode: str | None = None,
    radiation_probe: str | None = None,
    scattering_type: str | None = None,
) -> ExperimentBase:
    """
    Create an experiment without measured data.

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

    Returns
    -------
    ExperimentBase
        An experiment instance with only metadata.
    """
    expt_type = cls._create_experiment_type(
        sample_form=sample_form,
        beam_mode=beam_mode,
        radiation_probe=radiation_probe,
        scattering_type=scattering_type,
    )
    expt_class = cls._resolve_class(expt_type)
    return expt_class(name=name, type=expt_type)

total_pd

TotalPdExperiment

Bases: PdExperimentBase

PDF experiment class with specific attributes.

Source code in src/easydiffraction/datablocks/experiment/item/total_pd.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
@ExperimentFactory.register
class TotalPdExperiment(PdExperimentBase):
    """PDF experiment class with specific attributes."""

    type_info = TypeInfo(
        tag='total-pd',
        description='Total scattering (PDF) powder experiment',
    )
    compatibility = Compatibility(
        scattering_type=frozenset({ScatteringTypeEnum.TOTAL}),
        sample_form=frozenset({SampleFormEnum.POWDER}),
        beam_mode=frozenset({BeamModeEnum.CONSTANT_WAVELENGTH, BeamModeEnum.TIME_OF_FLIGHT}),
    )

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

    def _load_ascii_data_to_experiment(self, data_path: str) -> int:
        """
        Load x, y, sy values from an ASCII file into the experiment.

        The file must be structured as:     x  y  sy

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

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

        Raises
        ------
        ImportError
            If the ``diffpy`` package is not installed.
        OSError
            If the data file cannot be read.
        ValueError
            If the data file has fewer than two columns.
        """
        try:
            from diffpy.utils.parsers import load_data  # noqa: PLC0415
        except ImportError:
            msg = 'diffpy module not found.'
            raise ImportError(msg) from None
        try:
            data = load_data(data_path)
        except Exception as e:
            msg = f'Failed to read data from {data_path}: {e}'
            raise OSError(msg) from e

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

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

        x = data[:, 0]
        y = data[:, 1]
        sy = (
            data[:, 2]
            if data.shape[1] > _MIN_COLUMNS_XY
            else np.full_like(y, fill_value=default_sy)
        )

        self.data._create_items_set_xcoord_and_id(x)
        self.data._set_g_r_meas(y)
        self.data._set_g_r_meas_su(sy)

        return len(x)