Skip to content

experiments

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

Print a human-readable view of background components.

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

chebyshev

Chebyshev polynomial background model.

Provides a collection of polynomial terms and evaluation helpers.

ChebyshevPolynomialBackground

Bases: BackgroundBase

Source code in src/easydiffraction/experiments/categories/background/chebyshev.py
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
class ChebyshevPolynomialBackground(BackgroundBase):
    _description: str = 'Chebyshev polynomial background'

    def __init__(self):
        super().__init__(item_type=PolynomialTerm)

    def _update(self, called_by_minimizer=False):
        """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_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_bkg(y)

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

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

Print a table of polynomial orders and coefficients.

Source code in src/easydiffraction/experiments/categories/background/chebyshev.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
def show(self) -> None:
    """Print a table of polynomial orders and coefficients."""
    columns_headers: List[str] = ['Order', 'Coefficient']
    columns_alignment = ['left', 'left']
    columns_data: List[List[Union[int, float]]] = [
        [t.order.value, t.coef.value] for t in self._items
    ]

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

Bases: CategoryItem

Chebyshev polynomial term.

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

Source code in src/easydiffraction/experiments/categories/background/chebyshev.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 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
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,
        *,
        id=None,  # TODO: rename as in the case of data points?
        order=None,
        coef=None,
    ) -> None:
        super().__init__()

        self._id = StringDescriptor(
            name='id',
            description='Identifier for this background polynomial term.',
            value_spec=AttributeSpec(
                type_=DataTypes.STRING,
                value=id,
                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?
                content_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(
                value=order,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_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(
                value=coef,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_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)

    @property
    def id(self):
        return self._id

    @id.setter
    def id(self, value):
        self._id.value = value

    @property
    def order(self):
        return self._order

    @order.setter
    def order(self, value):
        self._order.value = value

    @property
    def coef(self):
        return self._coef

    @coef.setter
    def coef(self, value):
        self._coef.value = value

enums

Enumerations for background model types.

BackgroundTypeEnum

Bases: str, Enum

Supported background model types.

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

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

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

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

Return a default background type.

Source code in src/easydiffraction/experiments/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/experiments/categories/background/enums.py
22
23
24
25
26
27
def description(self) -> str:
    """Human-friendly description for the enum value."""
    if self is BackgroundTypeEnum.LINE_SEGMENT:
        return 'Linear interpolation between points'
    elif self is BackgroundTypeEnum.CHEBYSHEV:
        return 'Chebyshev polynomial background'

factory

Background collection entry point (public facade).

End users should import Background classes from this module. Internals live under the package easydiffraction.experiments.category_collections.background_types and are re-exported here for a stable and readable API.

BackgroundFactory

Create background collections by type.

Source code in src/easydiffraction/experiments/categories/background/factory.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
class BackgroundFactory:
    """Create background collections by type."""

    BT = BackgroundTypeEnum

    @classmethod
    def _supported_map(cls) -> dict:
        """Return mapping of enum values to concrete background
        classes.
        """
        # Lazy import to avoid circulars
        from easydiffraction.experiments.categories.background.chebyshev import (
            ChebyshevPolynomialBackground,
        )
        from easydiffraction.experiments.categories.background.line_segment import (
            LineSegmentBackground,
        )

        return {
            cls.BT.LINE_SEGMENT: LineSegmentBackground,
            cls.BT.CHEBYSHEV: ChebyshevPolynomialBackground,
        }

    @classmethod
    def create(
        cls,
        background_type: Optional[BackgroundTypeEnum] = None,
    ) -> BackgroundBase:
        """Instantiate a background collection of requested type.

        If type is None, the default enum value is used.
        """
        if background_type is None:
            background_type = BackgroundTypeEnum.default()

        supported = cls._supported_map()
        if background_type not in supported:
            supported_types = list(supported.keys())
            raise ValueError(
                f"Unsupported background type: '{background_type}'. "
                f'Supported background types: {[bt.value for bt in supported_types]}'
            )

        background_class = supported[background_type]
        return background_class()
create(background_type=None) classmethod

Instantiate a background collection of requested type.

If type is None, the default enum value is used.

Source code in src/easydiffraction/experiments/categories/background/factory.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@classmethod
def create(
    cls,
    background_type: Optional[BackgroundTypeEnum] = None,
) -> BackgroundBase:
    """Instantiate a background collection of requested type.

    If type is None, the default enum value is used.
    """
    if background_type is None:
        background_type = BackgroundTypeEnum.default()

    supported = cls._supported_map()
    if background_type not in supported:
        supported_types = list(supported.keys())
        raise ValueError(
            f"Unsupported background type: '{background_type}'. "
            f'Supported background types: {[bt.value for bt in supported_types]}'
        )

    background_class = supported[background_type]
    return background_class()

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/experiments/categories/background/line_segment.py
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 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
class LineSegment(CategoryItem):
    """Single background control point for interpolation."""

    def __init__(
        self,
        *,
        id=None,  # TODO: rename as in the case of data points?
        x=None,
        y=None,
    ) -> None:
        super().__init__()

        self._id = StringDescriptor(
            name='id',
            description='Identifier for this background line segment.',
            value_spec=AttributeSpec(
                type_=DataTypes.STRING,
                value=id,
                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?
                content_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 '
                'representing the background in a calculated diffractogram.'
            ),
            value_spec=AttributeSpec(
                value=x,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_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 '
                'representing the background in a calculated diffractogram'
            ),
            value_spec=AttributeSpec(
                value=y,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_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)

    @property
    def id(self):
        return self._id

    @id.setter
    def id(self, value):
        self._id.value = value

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x.value = value

    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, value):
        self._y.value = value
LineSegmentBackground

Bases: BackgroundBase

Source code in src/easydiffraction/experiments/categories/background/line_segment.py
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
class LineSegmentBackground(BackgroundBase):
    _description: str = 'Linear interpolation between points'

    def __init__(self):
        super().__init__(item_type=LineSegment)

    def _update(self, called_by_minimizer=False):
        """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_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_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/experiments/categories/background/line_segment.py
158
159
160
161
162
163
164
165
166
167
168
169
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,
    )

excluded_regions

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

ExcludedRegion

Bases: CategoryItem

Closed interval [start, end] to be excluded.

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

    def __init__(
        self,
        *,
        id=None,  # TODO: rename as in the case of data points?
        start=None,
        end=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(
                type_=DataTypes.STRING,
                value=id,
                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?
                content_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(
                value=start,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            cif_handler=CifHandler(
                names=[
                    '_excluded_region.start',
                ]
            ),
        )
        self._end = NumericDescriptor(
            name='end',
            description='End of the excluded region.',
            value_spec=AttributeSpec(
                value=end,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            cif_handler=CifHandler(
                names=[
                    '_excluded_region.end',
                ]
            ),
        )
        # self._category_entry_attr_name = f'{start}-{end}'
        # self._category_entry_attr_name = self.start.name
        # self.name = self.start.value
        self._identity.category_code = 'excluded_regions'
        self._identity.category_entry_name = lambda: str(self._id.value)

    @property
    def id(self):
        return self._id

    @id.setter
    def id(self, value):
        self._id.value = value

    @property
    def start(self) -> NumericDescriptor:
        return self._start

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

    @property
    def end(self) -> NumericDescriptor:
        return self._end

    @end.setter
    def end(self, value: float):
        self._end.value = 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/experiments/categories/excluded_regions.py
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
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.
    """

    def __init__(self):
        super().__init__(item_type=ExcludedRegion)

    def _update(self, called_by_minimizer=False):
        del called_by_minimizer

        data = self._parent.data
        x = data.all_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/experiments/categories/excluded_regions.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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,
    )

experiment_type

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 categorical attributes defining experiment flavor.

Parameters:

Name Type Description Default
sample_form

Powder or Single crystal.

None
beam_mode

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

None
radiation_probe

Neutrons or X-rays.

None
scattering_type

Bragg or Total.

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

    Args:
        sample_form: Powder or Single crystal.
        beam_mode: Constant wavelength (CW) or time-of-flight (TOF).
        radiation_probe: Neutrons or X-rays.
        scattering_type: Bragg or Total.
    """

    def __init__(
        self,
        *,
        sample_form=None,
        beam_mode=None,
        radiation_probe=None,
        scattering_type=None,
    ):
        super().__init__()

        self._sample_form: StringDescriptor = StringDescriptor(
            name='sample_form',
            description='Specifies whether the diffraction data corresponds to '
            'powder diffraction or single crystal diffraction',
            value_spec=AttributeSpec(
                value=sample_form,
                type_=DataTypes.STRING,
                default=SampleFormEnum.default().value,
                content_validator=MembershipValidator(
                    allowed=[member.value for member in SampleFormEnum]
                ),
            ),
            cif_handler=CifHandler(
                names=[
                    '_expt_type.sample_form',
                ]
            ),
        )

        self._beam_mode: StringDescriptor = StringDescriptor(
            name='beam_mode',
            description='Defines whether the measurement is performed with a '
            'constant wavelength (CW) or time-of-flight (TOF) method',
            value_spec=AttributeSpec(
                value=beam_mode,
                type_=DataTypes.STRING,
                default=BeamModeEnum.default().value,
                content_validator=MembershipValidator(
                    allowed=[member.value for member in BeamModeEnum]
                ),
            ),
            cif_handler=CifHandler(
                names=[
                    '_expt_type.beam_mode',
                ]
            ),
        )
        self._radiation_probe: StringDescriptor = StringDescriptor(
            name='radiation_probe',
            description='Specifies whether the measurement uses neutrons or X-rays',
            value_spec=AttributeSpec(
                value=radiation_probe,
                type_=DataTypes.STRING,
                default=RadiationProbeEnum.default().value,
                content_validator=MembershipValidator(
                    allowed=[member.value for member in RadiationProbeEnum]
                ),
            ),
            cif_handler=CifHandler(
                names=[
                    '_expt_type.radiation_probe',
                ]
            ),
        )
        self._scattering_type: StringDescriptor = StringDescriptor(
            name='scattering_type',
            description='Specifies whether the experiment uses Bragg scattering '
            '(for conventional structure refinement) or total scattering '
            '(for pair distribution function analysis - PDF)',
            value_spec=AttributeSpec(
                value=scattering_type,
                type_=DataTypes.STRING,
                default=ScatteringTypeEnum.default().value,
                content_validator=MembershipValidator(
                    allowed=[member.value for member in ScatteringTypeEnum]
                ),
            ),
            cif_handler=CifHandler(
                names=[
                    '_expt_type.scattering_type',
                ]
            ),
        )

        self._identity.category_code = 'expt_type'

    @property
    def sample_form(self):
        """Sample form descriptor (powder/single crystal)."""
        return self._sample_form

    @sample_form.setter
    def sample_form(self, value):
        """Set sample form value."""
        self._sample_form.value = value

    @property
    def beam_mode(self):
        """Beam mode descriptor (CW/TOF)."""
        return self._beam_mode

    @beam_mode.setter
    def beam_mode(self, value):
        """Set beam mode value."""
        self._beam_mode.value = value

    @property
    def radiation_probe(self):
        """Radiation probe descriptor (neutrons/X-rays)."""
        return self._radiation_probe

    @radiation_probe.setter
    def radiation_probe(self, value):
        """Set radiation probe value."""
        self._radiation_probe.value = value

    @property
    def scattering_type(self):
        """Scattering type descriptor (Bragg/Total)."""
        return self._scattering_type

    @scattering_type.setter
    def scattering_type(self, value):
        """Set scattering type value."""
        self._scattering_type.value = value
beam_mode property writable

Beam mode descriptor (CW/TOF).

radiation_probe property writable

Radiation probe descriptor (neutrons/X-rays).

sample_form property writable

Sample form descriptor (powder/single crystal).

scattering_type property writable

Scattering type descriptor (Bragg/Total).

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/experiments/categories/instrument/base.py
14
15
16
17
18
19
20
21
22
23
24
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/experiments/categories/instrument/base.py
21
22
23
24
def __init__(self) -> None:
    """Initialize instrument base and set category code."""
    super().__init__()
    self._identity.category_code = 'instrument'

cwl

CwlInstrument

Bases: InstrumentBase

Source code in src/easydiffraction/experiments/categories/instrument/cwl.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class CwlInstrument(InstrumentBase):
    def __init__(
        self,
        *,
        setup_wavelength=None,
        calib_twotheta_offset=None,
    ) -> None:
        super().__init__()

        self._setup_wavelength: Parameter = Parameter(
            name='wavelength',
            description='Incident neutron or X-ray wavelength',
            value_spec=AttributeSpec(
                value=setup_wavelength,
                type_=DataTypes.NUMERIC,
                default=1.5406,
                content_validator=RangeValidator(),
            ),
            units='Å',
            cif_handler=CifHandler(
                names=[
                    '_instr.wavelength',
                ]
            ),
        )
        self._calib_twotheta_offset: Parameter = Parameter(
            name='twotheta_offset',
            description='Instrument misalignment offset',
            value_spec=AttributeSpec(
                value=calib_twotheta_offset,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='deg',
            cif_handler=CifHandler(
                names=[
                    '_instr.2theta_offset',
                ]
            ),
        )

    @property
    def setup_wavelength(self):
        """Incident wavelength parameter (Å)."""
        return self._setup_wavelength

    @setup_wavelength.setter
    def setup_wavelength(self, value):
        """Set incident wavelength value (Å)."""
        self._setup_wavelength.value = value

    @property
    def calib_twotheta_offset(self):
        """Instrument misalignment two-theta offset (deg)."""
        return self._calib_twotheta_offset

    @calib_twotheta_offset.setter
    def calib_twotheta_offset(self, value):
        """Set two-theta offset value (deg)."""
        self._calib_twotheta_offset.value = value
calib_twotheta_offset property writable

Instrument misalignment two-theta offset (deg).

setup_wavelength property writable

Incident wavelength parameter (Å).

factory

Factory for instrument category items.

Provides a stable entry point for creating instrument objects from the experiment's scattering type and beam mode.

InstrumentFactory

Create instrument instances for supported modes.

The factory hides implementation details and lazy-loads concrete instrument classes to avoid circular imports.

Source code in src/easydiffraction/experiments/categories/instrument/factory.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
class InstrumentFactory:
    """Create instrument instances for supported modes.

    The factory hides implementation details and lazy-loads concrete
    instrument classes to avoid circular imports.
    """

    ST = ScatteringTypeEnum
    BM = BeamModeEnum

    @classmethod
    def _supported_map(cls) -> dict:
        # Lazy import to avoid circulars
        from easydiffraction.experiments.categories.instrument.cwl import CwlInstrument
        from easydiffraction.experiments.categories.instrument.tof import TofInstrument

        return {
            cls.ST.BRAGG: {
                cls.BM.CONSTANT_WAVELENGTH: CwlInstrument,
                cls.BM.TIME_OF_FLIGHT: TofInstrument,
            }
        }

    @classmethod
    def create(
        cls,
        scattering_type: Optional[ScatteringTypeEnum] = None,
        beam_mode: Optional[BeamModeEnum] = None,
    ) -> InstrumentBase:
        if beam_mode is None:
            beam_mode = BeamModeEnum.default()
        if scattering_type is None:
            scattering_type = ScatteringTypeEnum.default()

        supported = cls._supported_map()

        supported_scattering_types = list(supported.keys())
        if scattering_type not in supported_scattering_types:
            raise ValueError(
                f"Unsupported scattering type: '{scattering_type}'.\n "
                f'Supported scattering types: {supported_scattering_types}'
            )

        supported_beam_modes = list(supported[scattering_type].keys())
        if beam_mode not in supported_beam_modes:
            raise ValueError(
                f"Unsupported beam mode: '{beam_mode}' for scattering type: "
                f"'{scattering_type}'.\n "
                f'Supported beam modes: {supported_beam_modes}'
            )

        instrument_class: Type[InstrumentBase] = supported[scattering_type][beam_mode]
        return instrument_class()

tof

TofInstrument

Bases: InstrumentBase

Source code in src/easydiffraction/experiments/categories/instrument/tof.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 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
class TofInstrument(InstrumentBase):
    def __init__(
        self,
        *,
        setup_twotheta_bank=None,
        calib_d_to_tof_offset=None,
        calib_d_to_tof_linear=None,
        calib_d_to_tof_quad=None,
        calib_d_to_tof_recip=None,
    ) -> None:
        super().__init__()

        self._setup_twotheta_bank: Parameter = Parameter(
            name='twotheta_bank',
            description='Detector bank position',
            value_spec=AttributeSpec(
                value=setup_twotheta_bank,
                type_=DataTypes.NUMERIC,
                default=150.0,
                content_validator=RangeValidator(),
            ),
            units='deg',
            cif_handler=CifHandler(
                names=[
                    '_instr.2theta_bank',
                ]
            ),
        )
        self._calib_d_to_tof_offset: Parameter = Parameter(
            name='d_to_tof_offset',
            description='TOF offset',
            value_spec=AttributeSpec(
                value=calib_d_to_tof_offset,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='µs',
            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',
            value_spec=AttributeSpec(
                value=calib_d_to_tof_linear,
                type_=DataTypes.NUMERIC,
                default=10000.0,
                content_validator=RangeValidator(),
            ),
            units='µs/Å',
            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',
            value_spec=AttributeSpec(
                value=calib_d_to_tof_quad,
                type_=DataTypes.NUMERIC,
                default=-0.00001,
                content_validator=RangeValidator(),
            ),
            units='µs/Ų',
            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',
            value_spec=AttributeSpec(
                value=calib_d_to_tof_recip,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='µs·Å',
            cif_handler=CifHandler(
                names=[
                    '_instr.d_to_tof_recip',
                ]
            ),
        )

    @property
    def setup_twotheta_bank(self):
        """Detector bank two-theta position (deg)."""
        return self._setup_twotheta_bank

    @setup_twotheta_bank.setter
    def setup_twotheta_bank(self, value):
        """Set detector bank two-theta position (deg)."""
        self._setup_twotheta_bank.value = value

    @property
    def calib_d_to_tof_offset(self):
        """TOF offset calibration parameter (µs)."""
        return self._calib_d_to_tof_offset

    @calib_d_to_tof_offset.setter
    def calib_d_to_tof_offset(self, value):
        """Set TOF offset (µs)."""
        self._calib_d_to_tof_offset.value = value

    @property
    def calib_d_to_tof_linear(self):
        """Linear d to TOF conversion coefficient (µs/Å)."""
        return self._calib_d_to_tof_linear

    @calib_d_to_tof_linear.setter
    def calib_d_to_tof_linear(self, value):
        """Set linear d to TOF coefficient (µs/Å)."""
        self._calib_d_to_tof_linear.value = value

    @property
    def calib_d_to_tof_quad(self):
        """Quadratic d to TOF correction coefficient (µs/Ų)."""
        return self._calib_d_to_tof_quad

    @calib_d_to_tof_quad.setter
    def calib_d_to_tof_quad(self, value):
        """Set quadratic d to TOF correction (µs/Ų)."""
        self._calib_d_to_tof_quad.value = value

    @property
    def calib_d_to_tof_recip(self):
        """Reciprocal-velocity d to TOF correction (µs·Å)."""
        return self._calib_d_to_tof_recip

    @calib_d_to_tof_recip.setter
    def calib_d_to_tof_recip(self, value):
        """Set reciprocal-velocity d to TOF correction (µs·Å)."""
        self._calib_d_to_tof_recip.value = value
calib_d_to_tof_linear property writable

Linear d to TOF conversion coefficient (µs/Å).

calib_d_to_tof_offset property writable

TOF offset calibration parameter (µs).

calib_d_to_tof_quad property writable

Quadratic d to TOF correction coefficient (µs/Ų).

calib_d_to_tof_recip property writable

Reciprocal-velocity d to TOF correction (µs·Å).

setup_twotheta_bank property writable

Detector bank two-theta position (deg).

linked_phases

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/experiments/categories/linked_phases.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
class LinkedPhase(CategoryItem):
    """Link to a phase by id with a scale factor."""

    def __init__(
        self,
        *,
        id=None,  # TODO: need new name instead of id
        scale=None,
    ):
        super().__init__()

        self._id = StringDescriptor(
            name='id',
            description='Identifier of the linked phase.',
            value_spec=AttributeSpec(
                value=id,
                type_=DataTypes.STRING,
                default='Si',
                content_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(
                value=scale,
                type_=DataTypes.NUMERIC,
                default=1.0,
                content_validator=RangeValidator(),
            ),
            cif_handler=CifHandler(
                names=[
                    '_pd_phase_block.scale',
                ]
            ),
        )
        self._identity.category_code = 'linked_phases'
        self._identity.category_entry_name = lambda: str(self.id.value)

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

    @id.setter
    def id(self, value: str):
        """Set the linked phase identifier."""
        self._id.value = value

    @property
    def scale(self) -> Parameter:
        """Scale factor parameter."""
        return self._scale

    @scale.setter
    def scale(self, value: float):
        """Set scale factor value."""
        self._scale.value = value
id property writable

Identifier of the linked phase.

scale property writable

Scale factor parameter.

LinkedPhases

Bases: CategoryCollection

Collection of LinkedPhase instances.

Source code in src/easydiffraction/experiments/categories/linked_phases.py
81
82
83
84
85
86
class LinkedPhases(CategoryCollection):
    """Collection of LinkedPhase instances."""

    def __init__(self):
        """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/experiments/categories/linked_phases.py
84
85
86
def __init__(self):
    """Create an empty collection of linked phases."""
    super().__init__(item_type=LinkedPhase)

peak

base

Base class for peak profile categories.

PeakBase

Bases: CategoryItem

Base class for peak profile categories.

Source code in src/easydiffraction/experiments/categories/peak/base.py
 8
 9
10
11
12
13
class PeakBase(CategoryItem):
    """Base class for peak profile categories."""

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

cwl

Constant-wavelength peak profile classes.

CwlPseudoVoigt

Bases: PeakBase, CwlBroadeningMixin

Constant-wavelength pseudo-Voigt peak shape.

Source code in src/easydiffraction/experiments/categories/peak/cwl.py
11
12
13
14
15
16
17
18
19
class CwlPseudoVoigt(
    PeakBase,
    CwlBroadeningMixin,
):
    """Constant-wavelength pseudo-Voigt peak shape."""

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

Bases: PeakBase, CwlBroadeningMixin, EmpiricalAsymmetryMixin

Split pseudo-Voigt (empirical asymmetry) for CWL mode.

Source code in src/easydiffraction/experiments/categories/peak/cwl.py
22
23
24
25
26
27
28
29
30
31
32
class CwlSplitPseudoVoigt(
    PeakBase,
    CwlBroadeningMixin,
    EmpiricalAsymmetryMixin,
):
    """Split pseudo-Voigt (empirical asymmetry) for CWL mode."""

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

Bases: PeakBase, CwlBroadeningMixin, FcjAsymmetryMixin

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

Source code in src/easydiffraction/experiments/categories/peak/cwl.py
35
36
37
38
39
40
41
42
43
44
45
class CwlThompsonCoxHastings(
    PeakBase,
    CwlBroadeningMixin,
    FcjAsymmetryMixin,
):
    """Thompson–Cox–Hastings with FCJ asymmetry for CWL mode."""

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

cwl_mixins

Constant-wavelength (CWL) peak-profile mixins.

This module provides mixins that add broadening and asymmetry parameters for constant-wavelength powder diffraction peak profiles. They are composed into concrete peak classes elsewhere.

CwlBroadeningMixin

Mixin that adds CWL Gaussian and Lorentz broadening parameters.

Source code in src/easydiffraction/experiments/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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
class CwlBroadeningMixin:
    """Mixin that adds CWL Gaussian and Lorentz broadening
    parameters.
    """

    # TODO: Rename to cwl. Check other mixins for naming consistency.
    def _add_constant_wavelength_broadening(self) -> None:
        """Create CWL broadening parameters and attach them to the
        class.

        Defines Gaussian (U, V, W) and Lorentz (X, Y) terms
        often used in the TCH formulation. Values are stored as
        ``Parameter`` objects.
        """
        self._broad_gauss_u: Parameter = Parameter(
            name='broad_gauss_u',
            description='Gaussian broadening coefficient (dependent on '
            'sample size and instrument resolution)',
            value_spec=AttributeSpec(
                value=0.01,
                type_=DataTypes.NUMERIC,
                default=0.01,
                content_validator=RangeValidator(),
            ),
            units='deg²',
            cif_handler=CifHandler(
                names=[
                    '_peak.broad_gauss_u',
                ]
            ),
        )
        self._broad_gauss_v: Parameter = Parameter(
            name='broad_gauss_v',
            description='Gaussian broadening coefficient (instrumental broadening contribution)',
            value_spec=AttributeSpec(
                value=-0.01,
                type_=DataTypes.NUMERIC,
                default=-0.01,
                content_validator=RangeValidator(),
            ),
            units='deg²',
            cif_handler=CifHandler(
                names=[
                    '_peak.broad_gauss_v',
                ]
            ),
        )
        self._broad_gauss_w: Parameter = Parameter(
            name='broad_gauss_w',
            description='Gaussian broadening coefficient (instrumental broadening contribution)',
            value_spec=AttributeSpec(
                value=0.02,
                type_=DataTypes.NUMERIC,
                default=0.02,
                content_validator=RangeValidator(),
            ),
            units='deg²',
            cif_handler=CifHandler(
                names=[
                    '_peak.broad_gauss_w',
                ]
            ),
        )
        self._broad_lorentz_x: Parameter = Parameter(
            name='broad_lorentz_x',
            description='Lorentzian broadening coefficient (dependent on sample strain effects)',
            value_spec=AttributeSpec(
                value=0.0,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='deg',
            cif_handler=CifHandler(
                names=[
                    '_peak.broad_lorentz_x',
                ]
            ),
        )
        self._broad_lorentz_y: Parameter = Parameter(
            name='broad_lorentz_y',
            description='Lorentzian broadening coefficient (dependent on '
            'microstructural defects and strain)',
            value_spec=AttributeSpec(
                value=0.0,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='deg',
            cif_handler=CifHandler(
                names=[
                    '_peak.broad_lorentz_y',
                ]
            ),
        )

    @property
    def broad_gauss_u(self) -> Parameter:
        """Get Gaussian U broadening parameter."""
        return self._broad_gauss_u

    @broad_gauss_u.setter
    def broad_gauss_u(self, value: float) -> None:
        """Set Gaussian U broadening parameter."""
        self._broad_gauss_u.value = value

    @property
    def broad_gauss_v(self) -> Parameter:
        """Get Gaussian V broadening parameter."""
        return self._broad_gauss_v

    @broad_gauss_v.setter
    def broad_gauss_v(self, value: float) -> None:
        """Set Gaussian V broadening parameter."""
        self._broad_gauss_v.value = value

    @property
    def broad_gauss_w(self) -> Parameter:
        """Get Gaussian W broadening parameter."""
        return self._broad_gauss_w

    @broad_gauss_w.setter
    def broad_gauss_w(self, value: float) -> None:
        """Set Gaussian W broadening parameter."""
        self._broad_gauss_w.value = value

    @property
    def broad_lorentz_x(self) -> Parameter:
        """Get Lorentz X broadening parameter."""
        return self._broad_lorentz_x

    @broad_lorentz_x.setter
    def broad_lorentz_x(self, value: float) -> None:
        """Set Lorentz X broadening parameter."""
        self._broad_lorentz_x.value = value

    @property
    def broad_lorentz_y(self) -> Parameter:
        """Get Lorentz Y broadening parameter."""
        return self._broad_lorentz_y

    @broad_lorentz_y.setter
    def broad_lorentz_y(self, value: float) -> None:
        """Set Lorentz Y broadening parameter."""
        self._broad_lorentz_y.value = value
broad_gauss_u property writable

Get Gaussian U broadening parameter.

broad_gauss_v property writable

Get Gaussian V broadening parameter.

broad_gauss_w property writable

Get Gaussian W broadening parameter.

broad_lorentz_x property writable

Get Lorentz X broadening parameter.

broad_lorentz_y property writable

Get Lorentz Y broadening parameter.

EmpiricalAsymmetryMixin

Mixin that adds empirical CWL peak asymmetry parameters.

Source code in src/easydiffraction/experiments/categories/peak/cwl_mixins.py
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
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
class EmpiricalAsymmetryMixin:
    """Mixin that adds empirical CWL peak asymmetry parameters."""

    def _add_empirical_asymmetry(self) -> None:
        """Create empirical asymmetry parameters p1..p4."""
        self._asym_empir_1: Parameter = Parameter(
            name='asym_empir_1',
            description='Empirical asymmetry coefficient p1',
            value_spec=AttributeSpec(
                value=0.1,
                type_=DataTypes.NUMERIC,
                default=0.1,
                content_validator=RangeValidator(),
            ),
            units='',
            cif_handler=CifHandler(
                names=[
                    '_peak.asym_empir_1',
                ]
            ),
        )
        self._asym_empir_2: Parameter = Parameter(
            name='asym_empir_2',
            description='Empirical asymmetry coefficient p2',
            value_spec=AttributeSpec(
                value=0.2,
                type_=DataTypes.NUMERIC,
                default=0.2,
                content_validator=RangeValidator(),
            ),
            units='',
            cif_handler=CifHandler(
                names=[
                    '_peak.asym_empir_2',
                ]
            ),
        )
        self._asym_empir_3: Parameter = Parameter(
            name='asym_empir_3',
            description='Empirical asymmetry coefficient p3',
            value_spec=AttributeSpec(
                value=0.3,
                type_=DataTypes.NUMERIC,
                default=0.3,
                content_validator=RangeValidator(),
            ),
            units='',
            cif_handler=CifHandler(
                names=[
                    '_peak.asym_empir_3',
                ]
            ),
        )
        self._asym_empir_4: Parameter = Parameter(
            name='asym_empir_4',
            description='Empirical asymmetry coefficient p4',
            value_spec=AttributeSpec(
                value=0.4,
                type_=DataTypes.NUMERIC,
                default=0.4,
                content_validator=RangeValidator(),
            ),
            units='',
            cif_handler=CifHandler(
                names=[
                    '_peak.asym_empir_4',
                ]
            ),
        )

    @property
    def asym_empir_1(self) -> Parameter:
        """Get empirical asymmetry coefficient p1."""
        return self._asym_empir_1

    @asym_empir_1.setter
    def asym_empir_1(self, value: float) -> None:
        """Set empirical asymmetry coefficient p1."""
        self._asym_empir_1.value = value

    @property
    def asym_empir_2(self) -> Parameter:
        """Get empirical asymmetry coefficient p2."""
        return self._asym_empir_2

    @asym_empir_2.setter
    def asym_empir_2(self, value: float) -> None:
        """Set empirical asymmetry coefficient p2."""
        self._asym_empir_2.value = value

    @property
    def asym_empir_3(self) -> Parameter:
        """Get empirical asymmetry coefficient p3."""
        return self._asym_empir_3

    @asym_empir_3.setter
    def asym_empir_3(self, value: float) -> None:
        """Set empirical asymmetry coefficient p3."""
        self._asym_empir_3.value = value

    @property
    def asym_empir_4(self) -> Parameter:
        """Get empirical asymmetry coefficient p4."""
        return self._asym_empir_4

    @asym_empir_4.setter
    def asym_empir_4(self, value: float) -> None:
        """Set empirical asymmetry coefficient p4."""
        self._asym_empir_4.value = value
asym_empir_1 property writable

Get empirical asymmetry coefficient p1.

asym_empir_2 property writable

Get empirical asymmetry coefficient p2.

asym_empir_3 property writable

Get empirical asymmetry coefficient p3.

asym_empir_4 property writable

Get empirical asymmetry coefficient p4.

FcjAsymmetryMixin

Mixin that adds Finger–Cox–Jephcoat (FCJ) asymmetry params.

Source code in src/easydiffraction/experiments/categories/peak/cwl_mixins.py
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
class FcjAsymmetryMixin:
    """Mixin that adds Finger–Cox–Jephcoat (FCJ) asymmetry params."""

    def _add_fcj_asymmetry(self) -> None:
        """Create FCJ asymmetry parameters."""
        self._asym_fcj_1: Parameter = Parameter(
            name='asym_fcj_1',
            description='Finger-Cox-Jephcoat asymmetry parameter 1',
            value_spec=AttributeSpec(
                value=0.01,
                type_=DataTypes.NUMERIC,
                default=0.01,
                content_validator=RangeValidator(),
            ),
            units='',
            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',
            value_spec=AttributeSpec(
                value=0.02,
                type_=DataTypes.NUMERIC,
                default=0.02,
                content_validator=RangeValidator(),
            ),
            units='',
            cif_handler=CifHandler(
                names=[
                    '_peak.asym_fcj_2',
                ]
            ),
        )

    @property
    def asym_fcj_1(self) -> Parameter:
        """Get FCJ asymmetry parameter 1."""
        return self._asym_fcj_1

    @asym_fcj_1.setter
    def asym_fcj_1(self, value: float) -> None:
        """Set FCJ asymmetry parameter 1."""
        self._asym_fcj_1.value = value

    @property
    def asym_fcj_2(self) -> Parameter:
        """Get FCJ asymmetry parameter 2."""
        return self._asym_fcj_2

    @asym_fcj_2.setter
    def asym_fcj_2(self, value: float) -> None:
        """Set FCJ asymmetry parameter 2."""
        self._asym_fcj_2.value = value
asym_fcj_1 property writable

Get FCJ asymmetry parameter 1.

asym_fcj_2 property writable

Get FCJ asymmetry parameter 2.

factory

PeakFactory

Factory for creating peak profile objects.

Lazily imports implementations to avoid circular dependencies and selects the appropriate class based on scattering type, beam mode and requested profile type.

Source code in src/easydiffraction/experiments/categories/peak/factory.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 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 PeakFactory:
    """Factory for creating peak profile objects.

    Lazily imports implementations to avoid circular dependencies and
    selects the appropriate class based on scattering type, beam mode
    and requested profile type.
    """

    ST = ScatteringTypeEnum
    BM = BeamModeEnum
    PPT = PeakProfileTypeEnum
    _supported = None  # type: ignore[var-annotated]

    @classmethod
    def _supported_map(cls):
        """Return nested mapping of supported profile classes.

        Structure:
            ``{ScatteringType: {BeamMode: {ProfileType: Class}}}``.
        """
        # Lazy import to avoid circular imports between
        # base and cw/tof/pdf modules
        if cls._supported is None:
            from easydiffraction.experiments.categories.peak.cwl import CwlPseudoVoigt as CwPv
            from easydiffraction.experiments.categories.peak.cwl import (
                CwlSplitPseudoVoigt as CwSpv,
            )
            from easydiffraction.experiments.categories.peak.cwl import (
                CwlThompsonCoxHastings as CwTch,
            )
            from easydiffraction.experiments.categories.peak.tof import TofPseudoVoigt as TofPv
            from easydiffraction.experiments.categories.peak.tof import (
                TofPseudoVoigtBackToBack as TofBtb,
            )
            from easydiffraction.experiments.categories.peak.tof import (
                TofPseudoVoigtIkedaCarpenter as TofIc,
            )
            from easydiffraction.experiments.categories.peak.total import (
                TotalGaussianDampedSinc as PdfGds,
            )

            cls._supported = {
                cls.ST.BRAGG: {
                    cls.BM.CONSTANT_WAVELENGTH: {
                        cls.PPT.PSEUDO_VOIGT: CwPv,
                        cls.PPT.SPLIT_PSEUDO_VOIGT: CwSpv,
                        cls.PPT.THOMPSON_COX_HASTINGS: CwTch,
                    },
                    cls.BM.TIME_OF_FLIGHT: {
                        cls.PPT.PSEUDO_VOIGT: TofPv,
                        cls.PPT.PSEUDO_VOIGT_IKEDA_CARPENTER: TofIc,
                        cls.PPT.PSEUDO_VOIGT_BACK_TO_BACK: TofBtb,
                    },
                },
                cls.ST.TOTAL: {
                    cls.BM.CONSTANT_WAVELENGTH: {
                        cls.PPT.GAUSSIAN_DAMPED_SINC: PdfGds,
                    },
                    cls.BM.TIME_OF_FLIGHT: {
                        cls.PPT.GAUSSIAN_DAMPED_SINC: PdfGds,
                    },
                },
            }
        return cls._supported

    @classmethod
    def create(
        cls,
        scattering_type: Optional[ScatteringTypeEnum] = None,
        beam_mode: Optional[BeamModeEnum] = None,
        profile_type: Optional[PeakProfileTypeEnum] = None,
    ):
        """Instantiate a peak profile for the given configuration.

        Args:
            scattering_type: Bragg or Total. Defaults to library
                default.
            beam_mode: CW or TOF. Defaults to library default.
            profile_type: Concrete profile within the mode. If omitted,
                a sensible default is chosen based on the other args.

        Returns:
            A newly created peak profile object.

        Raises:
            ValueError: If a requested option is not supported.
        """
        if beam_mode is None:
            beam_mode = BeamModeEnum.default()
        if scattering_type is None:
            scattering_type = ScatteringTypeEnum.default()
        if profile_type is None:
            profile_type = PeakProfileTypeEnum.default(scattering_type, beam_mode)
        supported = cls._supported_map()
        supported_scattering_types = list(supported.keys())
        if scattering_type not in supported_scattering_types:
            raise ValueError(
                f"Unsupported scattering type: '{scattering_type}'.\n"
                f'Supported scattering types: {supported_scattering_types}'
            )

        supported_beam_modes = list(supported[scattering_type].keys())
        if beam_mode not in supported_beam_modes:
            raise ValueError(
                f"Unsupported beam mode: '{beam_mode}' for scattering type: "
                f"'{scattering_type}'.\n Supported beam modes: '{supported_beam_modes}'"
            )

        supported_profile_types = list(supported[scattering_type][beam_mode].keys())
        if profile_type not in supported_profile_types:
            raise ValueError(
                f"Unsupported profile type '{profile_type}' for beam mode '{beam_mode}'.\n"
                f'Supported profile types: {supported_profile_types}'
            )

        peak_class = supported[scattering_type][beam_mode][profile_type]
        peak_obj = peak_class()

        return peak_obj
create(scattering_type=None, beam_mode=None, profile_type=None) classmethod

Instantiate a peak profile for the given configuration.

Parameters:

Name Type Description Default
scattering_type Optional[ScatteringTypeEnum]

Bragg or Total. Defaults to library default.

None
beam_mode Optional[BeamModeEnum]

CW or TOF. Defaults to library default.

None
profile_type Optional[PeakProfileTypeEnum]

Concrete profile within the mode. If omitted, a sensible default is chosen based on the other args.

None

Returns:

Type Description

A newly created peak profile object.

Raises:

Type Description
ValueError

If a requested option is not supported.

Source code in src/easydiffraction/experiments/categories/peak/factory.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
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
@classmethod
def create(
    cls,
    scattering_type: Optional[ScatteringTypeEnum] = None,
    beam_mode: Optional[BeamModeEnum] = None,
    profile_type: Optional[PeakProfileTypeEnum] = None,
):
    """Instantiate a peak profile for the given configuration.

    Args:
        scattering_type: Bragg or Total. Defaults to library
            default.
        beam_mode: CW or TOF. Defaults to library default.
        profile_type: Concrete profile within the mode. If omitted,
            a sensible default is chosen based on the other args.

    Returns:
        A newly created peak profile object.

    Raises:
        ValueError: If a requested option is not supported.
    """
    if beam_mode is None:
        beam_mode = BeamModeEnum.default()
    if scattering_type is None:
        scattering_type = ScatteringTypeEnum.default()
    if profile_type is None:
        profile_type = PeakProfileTypeEnum.default(scattering_type, beam_mode)
    supported = cls._supported_map()
    supported_scattering_types = list(supported.keys())
    if scattering_type not in supported_scattering_types:
        raise ValueError(
            f"Unsupported scattering type: '{scattering_type}'.\n"
            f'Supported scattering types: {supported_scattering_types}'
        )

    supported_beam_modes = list(supported[scattering_type].keys())
    if beam_mode not in supported_beam_modes:
        raise ValueError(
            f"Unsupported beam mode: '{beam_mode}' for scattering type: "
            f"'{scattering_type}'.\n Supported beam modes: '{supported_beam_modes}'"
        )

    supported_profile_types = list(supported[scattering_type][beam_mode].keys())
    if profile_type not in supported_profile_types:
        raise ValueError(
            f"Unsupported profile type '{profile_type}' for beam mode '{beam_mode}'.\n"
            f'Supported profile types: {supported_profile_types}'
        )

    peak_class = supported[scattering_type][beam_mode][profile_type]
    peak_obj = peak_class()

    return peak_obj

tof

Time-of-flight peak profile classes.

TofPseudoVoigt

Bases: PeakBase, TofBroadeningMixin

Time-of-flight pseudo-Voigt peak shape.

Source code in src/easydiffraction/experiments/categories/peak/tof.py
10
11
12
13
14
15
16
17
18
class TofPseudoVoigt(
    PeakBase,
    TofBroadeningMixin,
):
    """Time-of-flight pseudo-Voigt peak shape."""

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

Bases: PeakBase, TofBroadeningMixin, IkedaCarpenterAsymmetryMixin

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

Source code in src/easydiffraction/experiments/categories/peak/tof.py
34
35
36
37
38
39
40
41
42
43
44
class TofPseudoVoigtBackToBack(
    PeakBase,
    TofBroadeningMixin,
    IkedaCarpenterAsymmetryMixin,
):
    """TOF back-to-back pseudo-Voigt with asymmetry."""

    def __init__(self) -> None:
        super().__init__()
        self._add_time_of_flight_broadening()
        self._add_ikeda_carpenter_asymmetry()
TofPseudoVoigtIkedaCarpenter

Bases: PeakBase, TofBroadeningMixin, IkedaCarpenterAsymmetryMixin

TOF pseudo-Voigt with Ikeda–Carpenter asymmetry.

Source code in src/easydiffraction/experiments/categories/peak/tof.py
21
22
23
24
25
26
27
28
29
30
31
class TofPseudoVoigtIkedaCarpenter(
    PeakBase,
    TofBroadeningMixin,
    IkedaCarpenterAsymmetryMixin,
):
    """TOF pseudo-Voigt with Ikeda–Carpenter asymmetry."""

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

tof_mixins

Time-of-flight (TOF) peak-profile mixins.

Defines mixins that add Gaussian/Lorentz broadening, mixing, and Ikeda–Carpenter asymmetry parameters used by TOF peak shapes.

IkedaCarpenterAsymmetryMixin

Mixin that adds Ikeda–Carpenter asymmetry parameters.

Source code in src/easydiffraction/experiments/categories/peak/tof_mixins.py
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
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
class IkedaCarpenterAsymmetryMixin:
    """Mixin that adds Ikeda–Carpenter asymmetry parameters."""

    def _add_ikeda_carpenter_asymmetry(self) -> None:
        """Create Ikeda–Carpenter asymmetry parameters alpha_0 and
        alpha_1.
        """
        self._asym_alpha_0: Parameter = Parameter(
            name='asym_alpha_0',
            description='Ikeda-Carpenter asymmetry parameter α₀',
            value_spec=AttributeSpec(
                value=0.01,
                type_=DataTypes.NUMERIC,
                default=0.01,
                content_validator=RangeValidator(),
            ),
            units='',
            cif_handler=CifHandler(
                names=[
                    '_peak.asym_alpha_0',
                ]
            ),
        )
        self._asym_alpha_1: Parameter = Parameter(
            name='asym_alpha_1',
            description='Ikeda-Carpenter asymmetry parameter α₁',
            value_spec=AttributeSpec(
                value=0.02,
                type_=DataTypes.NUMERIC,
                default=0.02,
                content_validator=RangeValidator(),
            ),
            units='',
            cif_handler=CifHandler(
                names=[
                    '_peak.asym_alpha_1',
                ]
            ),
        )

    @property
    def asym_alpha_0(self) -> Parameter:
        """Get Ikeda–Carpenter asymmetry alpha_0."""
        return self._asym_alpha_0

    @asym_alpha_0.setter
    def asym_alpha_0(self, value: float) -> None:
        """Set Ikeda–Carpenter asymmetry alpha_0."""
        self._asym_alpha_0.value = value

    @property
    def asym_alpha_1(self) -> Parameter:
        """Get Ikeda–Carpenter asymmetry alpha_1."""
        return self._asym_alpha_1

    @asym_alpha_1.setter
    def asym_alpha_1(self, value: float) -> None:
        """Set Ikeda–Carpenter asymmetry alpha_1."""
        self._asym_alpha_1.value = value
asym_alpha_0 property writable

Get Ikeda–Carpenter asymmetry alpha_0.

asym_alpha_1 property writable

Get Ikeda–Carpenter asymmetry alpha_1.

TofBroadeningMixin

Mixin that adds TOF Gaussian/Lorentz broadening and mixing terms.

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

    def _add_time_of_flight_broadening(self) -> None:
        """Create TOF broadening and mixing parameters."""
        self._broad_gauss_sigma_0: Parameter = Parameter(
            name='gauss_sigma_0',
            description='Gaussian broadening coefficient (instrumental resolution)',
            value_spec=AttributeSpec(
                value=0.0,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='µs²',
            cif_handler=CifHandler(
                names=[
                    '_peak.gauss_sigma_0',
                ]
            ),
        )
        self._broad_gauss_sigma_1: Parameter = Parameter(
            name='gauss_sigma_1',
            description='Gaussian broadening coefficient (dependent on d-spacing)',
            value_spec=AttributeSpec(
                value=0.0,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='µs/Å',
            cif_handler=CifHandler(
                names=[
                    '_peak.gauss_sigma_1',
                ]
            ),
        )
        self._broad_gauss_sigma_2: Parameter = Parameter(
            name='gauss_sigma_2',
            description='Gaussian broadening coefficient (instrument-dependent term)',
            value_spec=AttributeSpec(
                value=0.0,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='µs²/Ų',
            cif_handler=CifHandler(
                names=[
                    '_peak.gauss_sigma_2',
                ]
            ),
        )
        self._broad_lorentz_gamma_0: Parameter = Parameter(
            name='lorentz_gamma_0',
            description='Lorentzian broadening coefficient (dependent on microstrain effects)',
            value_spec=AttributeSpec(
                value=0.0,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='µs',
            cif_handler=CifHandler(
                names=[
                    '_peak.lorentz_gamma_0',
                ]
            ),
        )
        self._broad_lorentz_gamma_1: Parameter = Parameter(
            name='lorentz_gamma_1',
            description='Lorentzian broadening coefficient (dependent on d-spacing)',
            value_spec=AttributeSpec(
                value=0.0,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='µs/Å',
            cif_handler=CifHandler(
                names=[
                    '_peak.lorentz_gamma_1',
                ]
            ),
        )
        self._broad_lorentz_gamma_2: Parameter = Parameter(
            name='lorentz_gamma_2',
            description='Lorentzian broadening coefficient (instrument-dependent term)',
            value_spec=AttributeSpec(
                value=0.0,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='µs²/Ų',
            cif_handler=CifHandler(
                names=[
                    '_peak.lorentz_gamma_2',
                ]
            ),
        )
        self._broad_mix_beta_0: Parameter = Parameter(
            name='mix_beta_0',
            description='Mixing parameter. Defines the ratio of Gaussian '
            'to Lorentzian contributions in TOF profiles',
            value_spec=AttributeSpec(
                value=0.0,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='deg',
            cif_handler=CifHandler(
                names=[
                    '_peak.mix_beta_0',
                ]
            ),
        )
        self._broad_mix_beta_1: Parameter = Parameter(
            name='mix_beta_1',
            description='Mixing parameter. Defines the ratio of Gaussian '
            'to Lorentzian contributions in TOF profiles',
            value_spec=AttributeSpec(
                value=0.0,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='deg',
            cif_handler=CifHandler(
                names=[
                    '_peak.mix_beta_1',
                ]
            ),
        )

    @property
    def broad_gauss_sigma_0(self) -> Parameter:
        """Get Gaussian sigma_0 parameter."""
        return self._broad_gauss_sigma_0

    @broad_gauss_sigma_0.setter
    def broad_gauss_sigma_0(self, value: float) -> None:
        """Set Gaussian sigma_0 parameter."""
        self._broad_gauss_sigma_0.value = value

    @property
    def broad_gauss_sigma_1(self) -> Parameter:
        """Get Gaussian sigma_1 parameter."""
        return self._broad_gauss_sigma_1

    @broad_gauss_sigma_1.setter
    def broad_gauss_sigma_1(self, value: float) -> None:
        """Set Gaussian sigma_1 parameter."""
        self._broad_gauss_sigma_1.value = value

    @property
    def broad_gauss_sigma_2(self) -> Parameter:
        """Get Gaussian sigma_2 parameter."""
        return self._broad_gauss_sigma_2

    @broad_gauss_sigma_2.setter
    def broad_gauss_sigma_2(self, value: float) -> None:
        """Set Gaussian sigma_2 parameter."""
        self._broad_gauss_sigma_2.value = value

    @property
    def broad_lorentz_gamma_0(self) -> Parameter:
        """Get Lorentz gamma_0 parameter."""
        return self._broad_lorentz_gamma_0

    @broad_lorentz_gamma_0.setter
    def broad_lorentz_gamma_0(self, value: float) -> None:
        """Set Lorentz gamma_0 parameter."""
        self._broad_lorentz_gamma_0.value = value

    @property
    def broad_lorentz_gamma_1(self) -> Parameter:
        """Get Lorentz gamma_1 parameter."""
        return self._broad_lorentz_gamma_1

    @broad_lorentz_gamma_1.setter
    def broad_lorentz_gamma_1(self, value: float) -> None:
        """Set Lorentz gamma_1 parameter."""
        self._broad_lorentz_gamma_1.value = value

    @property
    def broad_lorentz_gamma_2(self) -> Parameter:
        """Get Lorentz gamma_2 parameter."""
        return self._broad_lorentz_gamma_2

    @broad_lorentz_gamma_2.setter
    def broad_lorentz_gamma_2(self, value: float) -> None:
        """Set Lorentz gamma_2 parameter."""
        self._broad_lorentz_gamma_2.value = value

    @property
    def broad_mix_beta_0(self) -> Parameter:
        """Get mixing parameter beta_0."""
        return self._broad_mix_beta_0

    @broad_mix_beta_0.setter
    def broad_mix_beta_0(self, value: float) -> None:
        """Set mixing parameter beta_0."""
        self._broad_mix_beta_0.value = value

    @property
    def broad_mix_beta_1(self) -> Parameter:
        """Get mixing parameter beta_1."""
        return self._broad_mix_beta_1

    @broad_mix_beta_1.setter
    def broad_mix_beta_1(self, value: float) -> None:
        """Set mixing parameter beta_1."""
        self._broad_mix_beta_1.value = value
broad_gauss_sigma_0 property writable

Get Gaussian sigma_0 parameter.

broad_gauss_sigma_1 property writable

Get Gaussian sigma_1 parameter.

broad_gauss_sigma_2 property writable

Get Gaussian sigma_2 parameter.

broad_lorentz_gamma_0 property writable

Get Lorentz gamma_0 parameter.

broad_lorentz_gamma_1 property writable

Get Lorentz gamma_1 parameter.

broad_lorentz_gamma_2 property writable

Get Lorentz gamma_2 parameter.

broad_mix_beta_0 property writable

Get mixing parameter beta_0.

broad_mix_beta_1 property writable

Get mixing parameter beta_1.

total

Total-scattering (PDF) peak profile classes.

TotalGaussianDampedSinc

Bases: PeakBase, TotalBroadeningMixin

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

Source code in src/easydiffraction/experiments/categories/peak/total.py
 9
10
11
12
13
14
15
16
17
class TotalGaussianDampedSinc(
    PeakBase,
    TotalBroadeningMixin,
):
    """Gaussian-damped sinc peak for total scattering (PDF)."""

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

total_mixins

Total scattering/PDF peak-profile mixins.

Adds damping, broadening, sharpening and envelope parameters used in pair distribution function (PDF) modeling.

TotalBroadeningMixin

Mixin adding PDF broadening/damping/sharpening parameters.

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

    def _add_pair_distribution_function_broadening(self):
        """Create PDF parameters: damp_q, broad_q, cutoff_q,
        sharp deltas, and particle diameter envelope.
        """
        self._damp_q: Parameter = Parameter(
            name='damp_q',
            description='Instrumental Q-resolution damping factor '
            '(affects high-r PDF peak amplitude)',
            value_spec=AttributeSpec(
                value=0.05,
                type_=DataTypes.NUMERIC,
                default=0.05,
                content_validator=RangeValidator(),
            ),
            units='Å⁻¹',
            cif_handler=CifHandler(
                names=[
                    '_peak.damp_q',
                ]
            ),
        )
        self._broad_q: Parameter = Parameter(
            name='broad_q',
            description='Quadratic PDF peak broadening coefficient '
            '(thermal and model uncertainty contribution)',
            value_spec=AttributeSpec(
                value=0.0,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='Å⁻²',
            cif_handler=CifHandler(
                names=[
                    '_peak.broad_q',
                ]
            ),
        )
        self._cutoff_q: Parameter = Parameter(
            name='cutoff_q',
            description='Q-value cutoff applied to model PDF for Fourier '
            'transform (controls real-space resolution)',
            value_spec=AttributeSpec(
                value=25.0,
                type_=DataTypes.NUMERIC,
                default=25.0,
                content_validator=RangeValidator(),
            ),
            units='Å⁻¹',
            cif_handler=CifHandler(
                names=[
                    '_peak.cutoff_q',
                ]
            ),
        )
        self._sharp_delta_1: Parameter = Parameter(
            name='sharp_delta_1',
            description='PDF peak sharpening coefficient (1/r dependence)',
            value_spec=AttributeSpec(
                value=0.0,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='Å',
            cif_handler=CifHandler(
                names=[
                    '_peak.sharp_delta_1',
                ]
            ),
        )
        self._sharp_delta_2: Parameter = Parameter(
            name='sharp_delta_2',
            description='PDF peak sharpening coefficient (1/r² dependence)',
            value_spec=AttributeSpec(
                value=0.0,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='Ų',
            cif_handler=CifHandler(
                names=[
                    '_peak.sharp_delta_2',
                ]
            ),
        )
        self._damp_particle_diameter: Parameter = Parameter(
            name='damp_particle_diameter',
            description='Particle diameter for spherical envelope damping correction in PDF',
            value_spec=AttributeSpec(
                value=0.0,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='Å',
            cif_handler=CifHandler(
                names=[
                    '_peak.damp_particle_diameter',
                ]
            ),
        )

    @property
    def damp_q(self) -> Parameter:
        """Get Q-resolution damping factor."""
        return self._damp_q

    @damp_q.setter
    def damp_q(self, value: float) -> None:
        """Set Q-resolution damping factor."""
        self._damp_q.value = value

    @property
    def broad_q(self) -> Parameter:
        """Get quadratic PDF broadening coefficient."""
        return self._broad_q

    @broad_q.setter
    def broad_q(self, value: float) -> None:
        """Set quadratic PDF broadening coefficient."""
        self._broad_q.value = value

    @property
    def cutoff_q(self) -> Parameter:
        """Get Q cutoff used for Fourier transform."""
        return self._cutoff_q

    @cutoff_q.setter
    def cutoff_q(self, value: float) -> None:
        """Set Q cutoff used for Fourier transform."""
        self._cutoff_q.value = value

    @property
    def sharp_delta_1(self) -> Parameter:
        """Get sharpening coefficient with 1/r dependence."""
        return self._sharp_delta_1

    @sharp_delta_1.setter
    def sharp_delta_1(self, value: float) -> None:
        """Set sharpening coefficient with 1/r dependence."""
        self._sharp_delta_1.value = value

    @property
    def sharp_delta_2(self) -> Parameter:
        """Get sharpening coefficient with 1/r^2 dependence."""
        return self._sharp_delta_2

    @sharp_delta_2.setter
    def sharp_delta_2(self, value: float) -> None:
        """Set sharpening coefficient with 1/r^2 dependence."""
        self._sharp_delta_2.value = value

    @property
    def damp_particle_diameter(self) -> Parameter:
        """Get particle diameter for spherical envelope damping."""
        return self._damp_particle_diameter

    @damp_particle_diameter.setter
    def damp_particle_diameter(self, value: float) -> None:
        """Set particle diameter for spherical envelope damping."""
        self._damp_particle_diameter.value = value
broad_q property writable

Get quadratic PDF broadening coefficient.

cutoff_q property writable

Get Q cutoff used for Fourier transform.

damp_particle_diameter property writable

Get particle diameter for spherical envelope damping.

damp_q property writable

Get Q-resolution damping factor.

sharp_delta_1 property writable

Get sharpening coefficient with 1/r dependence.

sharp_delta_2 property writable

Get sharpening coefficient with 1/r^2 dependence.

experiment

BraggPdExperiment

Bases: InstrumentMixin, PdExperimentBase

Powder diffraction experiment.

Wraps background model, peak profile and linked phases for Bragg PD.

Source code in src/easydiffraction/experiments/experiment/bragg_pd.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 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
class BraggPdExperiment(InstrumentMixin, PdExperimentBase):
    """Powder diffraction experiment.

    Wraps background model, peak profile and linked phases for Bragg PD.
    """

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

        self._background_type: BackgroundTypeEnum = BackgroundTypeEnum.default()
        self._background = BackgroundFactory.create(background_type=self.background_type)

    @property
    def background(self):
        return self._background

    @background.setter
    def background(self, value):
        self._background = value

    # -------------
    # Measured data
    # -------------

    def _load_ascii_data_to_experiment(self, data_path: str) -> None:
        """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)`` with small values clamped to ``1.0``.
        """
        try:
            data = np.loadtxt(data_path)
        except Exception as e:
            raise IOError(f'Failed to read data from {data_path}: {e}') from e

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

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

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

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

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

        # Replace values smaller than 0.0001 with 1.0
        sy = np.where(sy < 0.0001, 1.0, sy)

        # Set the experiment data
        self.data._set_x(x)
        self.data._set_meas(y)
        self.data._set_meas_su(sy)

        console.paragraph('Data loaded successfully')
        console.print(f"Experiment 🔬 '{self.name}'. Number of data points: {len(x)}")

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

    @background_type.setter
    def background_type(self, new_type):
        """Set and apply a new background type.

        Falls back to printing supported types if the new value is not
        supported.
        """
        if new_type not in BackgroundFactory._supported_map():
            supported_types = list(BackgroundFactory._supported_map().keys())
            log.warning(
                f"Unknown background type '{new_type}'. "
                f'Supported background types: {[bt.value for bt in supported_types]}. '
                f"For more information, use 'show_supported_background_types()'"
            )
            return
        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)

    def show_supported_background_types(self):
        """Print a table of supported background types."""
        columns_headers = ['Background type', 'Description']
        columns_alignment = ['left', 'left']
        columns_data = []
        for bt in BackgroundFactory._supported_map():
            columns_data.append([bt.value, bt.description()])

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

    def show_current_background_type(self):
        """Print the currently used background type."""
        console.paragraph('Current background type')
        console.print(self.background_type)

background_type property writable

Current background type enum value.

show_current_background_type()

Print the currently used background type.

Source code in src/easydiffraction/experiments/experiment/bragg_pd.py
131
132
133
134
def show_current_background_type(self):
    """Print the currently used background type."""
    console.paragraph('Current background type')
    console.print(self.background_type)

show_supported_background_types()

Print a table of supported background types.

Source code in src/easydiffraction/experiments/experiment/bragg_pd.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def show_supported_background_types(self):
    """Print a table of supported background types."""
    columns_headers = ['Background type', 'Description']
    columns_alignment = ['left', 'left']
    columns_data = []
    for bt in BackgroundFactory._supported_map():
        columns_data.append([bt.value, bt.description()])

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

BraggScExperiment

Bases: ExperimentBase

Single crystal experiment class with specific attributes.

Source code in src/easydiffraction/experiments/experiment/bragg_sc.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class BraggScExperiment(ExperimentBase):
    """Single crystal experiment class with specific attributes."""

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

    def show_meas_chart(self) -> None:
        print('Showing measured data chart is not implemented yet.')

ExperimentBase

Bases: DatablockItem

Base class for all experiments with only core attributes.

Wraps experiment type and instrument.

Source code in src/easydiffraction/experiments/experiment/base.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
class ExperimentBase(DatablockItem):
    """Base class for all experiments with only core attributes.

    Wraps experiment type and instrument.
    """

    def __init__(
        self,
        *,
        name: str,
        type: ExperimentType,
    ):
        super().__init__()
        self._name = name
        self._type = type
        # TODO: Should return default calculator based on experiment
        #  type
        from easydiffraction.analysis.calculators.factory import CalculatorFactory

        self._calculator = CalculatorFactory.create_calculator('cryspy')
        self._identity.datablock_entry_name = lambda: self.name

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

        Args:
            new: New name for this experiment.
        """
        self._name = new

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

    @property
    def calculator(self):
        """Calculator engine used for pattern calculations."""
        return self._calculator

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

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

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

        Args:
            data_path: Path to the ASCII file to load.
        """
        raise NotImplementedError()

as_cif property

Serialize this experiment to a CIF fragment.

calculator property

Calculator engine used for pattern calculations.

name property writable

Human-readable name of the experiment.

show_as_cif()

Pretty-print the experiment as CIF text.

Source code in src/easydiffraction/experiments/experiment/base.py
81
82
83
84
85
86
def show_as_cif(self) -> None:
    """Pretty-print the experiment as CIF text."""
    experiment_cif = super().as_cif
    paragraph_title: str = f"Experiment 🔬 '{self.name}' as cif"
    console.paragraph(paragraph_title)
    render_cif(experiment_cif)

type property

Experiment type descriptor (sample form, probe, beam mode).

PdExperimentBase

Bases: ExperimentBase

Base class for all powder experiments.

Source code in src/easydiffraction/experiments/experiment/base.py
 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
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: LinkedPhases = LinkedPhases()
        self._excluded_regions: ExcludedRegions = ExcludedRegions()

        self._peak_profile_type: PeakProfileTypeEnum = PeakProfileTypeEnum.default(
            self.type.scattering_type.value,
            self.type.beam_mode.value,
        )
        self._peak = PeakFactory.create(
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
            profile_type=self._peak_profile_type,
        )

        self._data = DataFactory.create(
            sample_form=self.type.sample_form.value,
            beam_mode=self.type.beam_mode.value,
            scattering_type=self.type.scattering_type.value,
        )

    def _get_valid_linked_phases(
        self,
        sample_models: SampleModels,
    ) -> List[Any]:
        """Get valid linked phases for this experiment.

        Args:
            sample_models: Collection of sample models.

        Returns:
            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 sample_models.names:
                print(
                    f"Warning: Linked phase '{linked_phase.id.value}' not "
                    f'found in Sample Models {sample_models.names}. Skipping it.'
                )
                continue
            valid_linked_phases.append(linked_phase)

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

        return valid_linked_phases

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

        Args:
            data_path: Path to data file with columns compatible with
                the beam mode (e.g. 2θ/I/σ for CWL, TOF/I/σ for TOF).
        """
        pass

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

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

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

    @peak.setter
    def peak(self, value):
        """Replace the peak model used for this powder experiment.

        Args:
            value: New peak object created by the `PeakFactory`.
        """
        self._peak = value

    @property
    def data(self):
        return self._data

    @property
    def peak_profile_type(self):
        """Currently selected peak profile type enum."""
        return self._peak_profile_type

    @peak_profile_type.setter
    def peak_profile_type(self, new_type: str | PeakProfileTypeEnum):
        """Change the active peak profile type, if supported.

        Args:
            new_type: New profile type as enum or its string value.
        """
        if isinstance(new_type, str):
            try:
                new_type = PeakProfileTypeEnum(new_type)
            except ValueError:
                log.warning(f"Unknown peak profile type '{new_type}'")
                return

        supported_types = list(
            PeakFactory._supported[self.type.scattering_type.value][
                self.type.beam_mode.value
            ].keys()
        )

        if new_type not in supported_types:
            log.warning(
                f"Unsupported peak profile '{new_type.value}', "
                f'Supported peak profiles: {supported_types}',
                "For more information, use 'show_supported_peak_profile_types()'",
            )
            return

        self._peak = PeakFactory.create(
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
            profile_type=new_type,
        )
        self._peak_profile_type = new_type
        console.paragraph(f"Peak profile type for experiment '{self.name}' changed to")
        console.print(new_type.value)

    def show_supported_peak_profile_types(self):
        """Print available peak profile types for this experiment."""
        columns_headers = ['Peak profile type', 'Description']
        columns_alignment = ['left', 'left']
        columns_data = []

        scattering_type = self.type.scattering_type.value
        beam_mode = self.type.beam_mode.value

        for profile_type in PeakFactory._supported[scattering_type][beam_mode]:
            columns_data.append([profile_type.value, profile_type.description()])

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

    def show_current_peak_profile_type(self):
        """Print the currently selected peak profile type."""
        console.paragraph('Current peak profile type')
        console.print(self.peak_profile_type)

excluded_regions property

Collection of excluded regions for the x-grid.

linked_phases property

Collection of phases linked to this experiment.

peak property writable

Peak category object with profile parameters and mixins.

peak_profile_type property writable

Currently selected peak profile type enum.

show_current_peak_profile_type()

Print the currently selected peak profile type.

Source code in src/easydiffraction/experiments/experiment/base.py
261
262
263
264
def show_current_peak_profile_type(self):
    """Print the currently selected peak profile type."""
    console.paragraph('Current peak profile type')
    console.print(self.peak_profile_type)

show_supported_peak_profile_types()

Print available peak profile types for this experiment.

Source code in src/easydiffraction/experiments/experiment/base.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
def show_supported_peak_profile_types(self):
    """Print available peak profile types for this experiment."""
    columns_headers = ['Peak profile type', 'Description']
    columns_alignment = ['left', 'left']
    columns_data = []

    scattering_type = self.type.scattering_type.value
    beam_mode = self.type.beam_mode.value

    for profile_type in PeakFactory._supported[scattering_type][beam_mode]:
        columns_data.append([profile_type.value, profile_type.description()])

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

TotalPdExperiment

Bases: PdExperimentBase

PDF experiment class with specific attributes.

Source code in src/easydiffraction/experiments/experiment/total_pd.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
class TotalPdExperiment(PdExperimentBase):
    """PDF experiment class with specific attributes."""

    def __init__(
        self,
        name: str,
        type: ExperimentType,
    ):
        super().__init__(name=name, type=type)

    def _load_ascii_data_to_experiment(self, data_path):
        """Loads x, y, sy values from an ASCII data file into the
        experiment.

        The file must be structured as:
            x  y  sy
        """
        try:
            from diffpy.utils.parsers.loaddata import loadData
        except ImportError:
            raise ImportError('diffpy module not found.') from None
        try:
            data = loadData(data_path)
        except Exception as e:
            raise IOError(f'Failed to read data from {data_path}: {e}') from e

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

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

        x = data[:, 0]
        y = data[:, 1]
        sy = data[:, 2] if data.shape[1] > 2 else np.full_like(y, fill_value=default_sy)

        self.data._set_x(x)
        self.data._set_meas(y)
        self.data._set_meas_su(sy)

        console.paragraph('Data loaded successfully')
        console.print(f"Experiment 🔬 '{self.name}'. Number of data points: {len(x)}")

base

ExperimentBase

Bases: DatablockItem

Base class for all experiments with only core attributes.

Wraps experiment type and instrument.

Source code in src/easydiffraction/experiments/experiment/base.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
class ExperimentBase(DatablockItem):
    """Base class for all experiments with only core attributes.

    Wraps experiment type and instrument.
    """

    def __init__(
        self,
        *,
        name: str,
        type: ExperimentType,
    ):
        super().__init__()
        self._name = name
        self._type = type
        # TODO: Should return default calculator based on experiment
        #  type
        from easydiffraction.analysis.calculators.factory import CalculatorFactory

        self._calculator = CalculatorFactory.create_calculator('cryspy')
        self._identity.datablock_entry_name = lambda: self.name

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

        Args:
            new: New name for this experiment.
        """
        self._name = new

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

    @property
    def calculator(self):
        """Calculator engine used for pattern calculations."""
        return self._calculator

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

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

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

        Args:
            data_path: Path to the ASCII file to load.
        """
        raise NotImplementedError()
as_cif property

Serialize this experiment to a CIF fragment.

calculator property

Calculator engine used for pattern calculations.

name property writable

Human-readable name of the experiment.

show_as_cif()

Pretty-print the experiment as CIF text.

Source code in src/easydiffraction/experiments/experiment/base.py
81
82
83
84
85
86
def show_as_cif(self) -> None:
    """Pretty-print the experiment as CIF text."""
    experiment_cif = super().as_cif
    paragraph_title: str = f"Experiment 🔬 '{self.name}' as cif"
    console.paragraph(paragraph_title)
    render_cif(experiment_cif)
type property

Experiment type descriptor (sample form, probe, beam mode).

PdExperimentBase

Bases: ExperimentBase

Base class for all powder experiments.

Source code in src/easydiffraction/experiments/experiment/base.py
 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
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: LinkedPhases = LinkedPhases()
        self._excluded_regions: ExcludedRegions = ExcludedRegions()

        self._peak_profile_type: PeakProfileTypeEnum = PeakProfileTypeEnum.default(
            self.type.scattering_type.value,
            self.type.beam_mode.value,
        )
        self._peak = PeakFactory.create(
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
            profile_type=self._peak_profile_type,
        )

        self._data = DataFactory.create(
            sample_form=self.type.sample_form.value,
            beam_mode=self.type.beam_mode.value,
            scattering_type=self.type.scattering_type.value,
        )

    def _get_valid_linked_phases(
        self,
        sample_models: SampleModels,
    ) -> List[Any]:
        """Get valid linked phases for this experiment.

        Args:
            sample_models: Collection of sample models.

        Returns:
            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 sample_models.names:
                print(
                    f"Warning: Linked phase '{linked_phase.id.value}' not "
                    f'found in Sample Models {sample_models.names}. Skipping it.'
                )
                continue
            valid_linked_phases.append(linked_phase)

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

        return valid_linked_phases

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

        Args:
            data_path: Path to data file with columns compatible with
                the beam mode (e.g. 2θ/I/σ for CWL, TOF/I/σ for TOF).
        """
        pass

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

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

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

    @peak.setter
    def peak(self, value):
        """Replace the peak model used for this powder experiment.

        Args:
            value: New peak object created by the `PeakFactory`.
        """
        self._peak = value

    @property
    def data(self):
        return self._data

    @property
    def peak_profile_type(self):
        """Currently selected peak profile type enum."""
        return self._peak_profile_type

    @peak_profile_type.setter
    def peak_profile_type(self, new_type: str | PeakProfileTypeEnum):
        """Change the active peak profile type, if supported.

        Args:
            new_type: New profile type as enum or its string value.
        """
        if isinstance(new_type, str):
            try:
                new_type = PeakProfileTypeEnum(new_type)
            except ValueError:
                log.warning(f"Unknown peak profile type '{new_type}'")
                return

        supported_types = list(
            PeakFactory._supported[self.type.scattering_type.value][
                self.type.beam_mode.value
            ].keys()
        )

        if new_type not in supported_types:
            log.warning(
                f"Unsupported peak profile '{new_type.value}', "
                f'Supported peak profiles: {supported_types}',
                "For more information, use 'show_supported_peak_profile_types()'",
            )
            return

        self._peak = PeakFactory.create(
            scattering_type=self.type.scattering_type.value,
            beam_mode=self.type.beam_mode.value,
            profile_type=new_type,
        )
        self._peak_profile_type = new_type
        console.paragraph(f"Peak profile type for experiment '{self.name}' changed to")
        console.print(new_type.value)

    def show_supported_peak_profile_types(self):
        """Print available peak profile types for this experiment."""
        columns_headers = ['Peak profile type', 'Description']
        columns_alignment = ['left', 'left']
        columns_data = []

        scattering_type = self.type.scattering_type.value
        beam_mode = self.type.beam_mode.value

        for profile_type in PeakFactory._supported[scattering_type][beam_mode]:
            columns_data.append([profile_type.value, profile_type.description()])

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

    def show_current_peak_profile_type(self):
        """Print the currently selected peak profile type."""
        console.paragraph('Current peak profile type')
        console.print(self.peak_profile_type)
excluded_regions property

Collection of excluded regions for the x-grid.

linked_phases property

Collection of phases linked to this experiment.

peak property writable

Peak category object with profile parameters and mixins.

peak_profile_type property writable

Currently selected peak profile type enum.

show_current_peak_profile_type()

Print the currently selected peak profile type.

Source code in src/easydiffraction/experiments/experiment/base.py
261
262
263
264
def show_current_peak_profile_type(self):
    """Print the currently selected peak profile type."""
    console.paragraph('Current peak profile type')
    console.print(self.peak_profile_type)
show_supported_peak_profile_types()

Print available peak profile types for this experiment.

Source code in src/easydiffraction/experiments/experiment/base.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
def show_supported_peak_profile_types(self):
    """Print available peak profile types for this experiment."""
    columns_headers = ['Peak profile type', 'Description']
    columns_alignment = ['left', 'left']
    columns_data = []

    scattering_type = self.type.scattering_type.value
    beam_mode = self.type.beam_mode.value

    for profile_type in PeakFactory._supported[scattering_type][beam_mode]:
        columns_data.append([profile_type.value, profile_type.description()])

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

bragg_pd

BraggPdExperiment

Bases: InstrumentMixin, PdExperimentBase

Powder diffraction experiment.

Wraps background model, peak profile and linked phases for Bragg PD.

Source code in src/easydiffraction/experiments/experiment/bragg_pd.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 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
class BraggPdExperiment(InstrumentMixin, PdExperimentBase):
    """Powder diffraction experiment.

    Wraps background model, peak profile and linked phases for Bragg PD.
    """

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

        self._background_type: BackgroundTypeEnum = BackgroundTypeEnum.default()
        self._background = BackgroundFactory.create(background_type=self.background_type)

    @property
    def background(self):
        return self._background

    @background.setter
    def background(self, value):
        self._background = value

    # -------------
    # Measured data
    # -------------

    def _load_ascii_data_to_experiment(self, data_path: str) -> None:
        """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)`` with small values clamped to ``1.0``.
        """
        try:
            data = np.loadtxt(data_path)
        except Exception as e:
            raise IOError(f'Failed to read data from {data_path}: {e}') from e

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

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

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

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

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

        # Replace values smaller than 0.0001 with 1.0
        sy = np.where(sy < 0.0001, 1.0, sy)

        # Set the experiment data
        self.data._set_x(x)
        self.data._set_meas(y)
        self.data._set_meas_su(sy)

        console.paragraph('Data loaded successfully')
        console.print(f"Experiment 🔬 '{self.name}'. Number of data points: {len(x)}")

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

    @background_type.setter
    def background_type(self, new_type):
        """Set and apply a new background type.

        Falls back to printing supported types if the new value is not
        supported.
        """
        if new_type not in BackgroundFactory._supported_map():
            supported_types = list(BackgroundFactory._supported_map().keys())
            log.warning(
                f"Unknown background type '{new_type}'. "
                f'Supported background types: {[bt.value for bt in supported_types]}. '
                f"For more information, use 'show_supported_background_types()'"
            )
            return
        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)

    def show_supported_background_types(self):
        """Print a table of supported background types."""
        columns_headers = ['Background type', 'Description']
        columns_alignment = ['left', 'left']
        columns_data = []
        for bt in BackgroundFactory._supported_map():
            columns_data.append([bt.value, bt.description()])

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

    def show_current_background_type(self):
        """Print the currently used background type."""
        console.paragraph('Current background type')
        console.print(self.background_type)
background_type property writable

Current background type enum value.

show_current_background_type()

Print the currently used background type.

Source code in src/easydiffraction/experiments/experiment/bragg_pd.py
131
132
133
134
def show_current_background_type(self):
    """Print the currently used background type."""
    console.paragraph('Current background type')
    console.print(self.background_type)
show_supported_background_types()

Print a table of supported background types.

Source code in src/easydiffraction/experiments/experiment/bragg_pd.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def show_supported_background_types(self):
    """Print a table of supported background types."""
    columns_headers = ['Background type', 'Description']
    columns_alignment = ['left', 'left']
    columns_data = []
    for bt in BackgroundFactory._supported_map():
        columns_data.append([bt.value, bt.description()])

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

bragg_sc

Single crystal experiment types and helpers.

BraggScExperiment

Bases: ExperimentBase

Single crystal experiment class with specific attributes.

Source code in src/easydiffraction/experiments/experiment/bragg_sc.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class BraggScExperiment(ExperimentBase):
    """Single crystal experiment class with specific attributes."""

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

    def show_meas_chart(self) -> None:
        print('Showing measured data chart is not implemented yet.')

enums

Enumerations for experiment configuration (forms, modes, types).

BeamModeEnum

Bases: str, Enum

Beam delivery mode for the instrument.

Source code in src/easydiffraction/experiments/experiment/enums.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class BeamModeEnum(str, Enum):
    """Beam delivery mode for the instrument."""

    # TODO: Rename to CWL and TOF
    CONSTANT_WAVELENGTH = 'constant wavelength'
    TIME_OF_FLIGHT = 'time-of-flight'

    @classmethod
    def default(cls) -> 'BeamModeEnum':
        return cls.CONSTANT_WAVELENGTH

    def description(self) -> str:
        if self is BeamModeEnum.CONSTANT_WAVELENGTH:
            return 'Constant wavelength (CW) diffraction.'
        elif self is BeamModeEnum.TIME_OF_FLIGHT:
            return 'Time-of-flight (TOF) diffraction.'

PeakProfileTypeEnum

Bases: str, Enum

Available peak profile types per scattering and beam mode.

Source code in src/easydiffraction/experiments/experiment/enums.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
class PeakProfileTypeEnum(str, Enum):
    """Available peak profile types per scattering and beam mode."""

    PSEUDO_VOIGT = 'pseudo-voigt'
    SPLIT_PSEUDO_VOIGT = 'split pseudo-voigt'
    THOMPSON_COX_HASTINGS = 'thompson-cox-hastings'
    PSEUDO_VOIGT_IKEDA_CARPENTER = 'pseudo-voigt * ikeda-carpenter'
    PSEUDO_VOIGT_BACK_TO_BACK = 'pseudo-voigt * back-to-back'
    GAUSSIAN_DAMPED_SINC = 'gaussian-damped-sinc'

    @classmethod
    def default(
        cls,
        scattering_type: ScatteringTypeEnum | None = None,
        beam_mode: BeamModeEnum | None = None,
    ) -> 'PeakProfileTypeEnum':
        if scattering_type is None:
            scattering_type = ScatteringTypeEnum.default()
        if beam_mode is None:
            beam_mode = BeamModeEnum.default()
        return {
            (ScatteringTypeEnum.BRAGG, BeamModeEnum.CONSTANT_WAVELENGTH): cls.PSEUDO_VOIGT,
            (
                ScatteringTypeEnum.BRAGG,
                BeamModeEnum.TIME_OF_FLIGHT,
            ): cls.PSEUDO_VOIGT_IKEDA_CARPENTER,
            (ScatteringTypeEnum.TOTAL, BeamModeEnum.CONSTANT_WAVELENGTH): cls.GAUSSIAN_DAMPED_SINC,
            (ScatteringTypeEnum.TOTAL, BeamModeEnum.TIME_OF_FLIGHT): cls.GAUSSIAN_DAMPED_SINC,
        }[(scattering_type, beam_mode)]

    def description(self) -> str:
        if self is PeakProfileTypeEnum.PSEUDO_VOIGT:
            return 'Pseudo-Voigt profile'
        elif self is PeakProfileTypeEnum.SPLIT_PSEUDO_VOIGT:
            return 'Split pseudo-Voigt profile with empirical asymmetry correction.'
        elif self is PeakProfileTypeEnum.THOMPSON_COX_HASTINGS:
            return 'Thompson-Cox-Hastings profile with FCJ asymmetry correction.'
        elif self is PeakProfileTypeEnum.PSEUDO_VOIGT_IKEDA_CARPENTER:
            return 'Pseudo-Voigt profile with Ikeda-Carpenter asymmetry correction.'
        elif self is PeakProfileTypeEnum.PSEUDO_VOIGT_BACK_TO_BACK:
            return 'Pseudo-Voigt profile with Back-to-Back Exponential asymmetry correction.'
        elif self is PeakProfileTypeEnum.GAUSSIAN_DAMPED_SINC:
            return 'Gaussian-damped sinc profile for pair distribution function (PDF) analysis.'

RadiationProbeEnum

Bases: str, Enum

Incident radiation probe used in the experiment.

Source code in src/easydiffraction/experiments/experiment/enums.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class RadiationProbeEnum(str, Enum):
    """Incident radiation probe used in the experiment."""

    NEUTRON = 'neutron'
    XRAY = 'xray'

    @classmethod
    def default(cls) -> 'RadiationProbeEnum':
        return cls.NEUTRON

    def description(self) -> str:
        if self is RadiationProbeEnum.NEUTRON:
            return 'Neutron diffraction.'
        elif self is RadiationProbeEnum.XRAY:
            return 'X-ray diffraction.'

SampleFormEnum

Bases: str, Enum

Physical sample form supported by experiments.

Source code in src/easydiffraction/experiments/experiment/enums.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SampleFormEnum(str, Enum):
    """Physical sample form supported by experiments."""

    POWDER = 'powder'
    SINGLE_CRYSTAL = 'single crystal'

    @classmethod
    def default(cls) -> 'SampleFormEnum':
        return cls.POWDER

    def description(self) -> str:
        if self is SampleFormEnum.POWDER:
            return 'Powdered or polycrystalline sample.'
        elif self is SampleFormEnum.SINGLE_CRYSTAL:
            return 'Single crystal sample.'

ScatteringTypeEnum

Bases: str, Enum

Type of scattering modeled in an experiment.

Source code in src/easydiffraction/experiments/experiment/enums.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class ScatteringTypeEnum(str, Enum):
    """Type of scattering modeled in an experiment."""

    BRAGG = 'bragg'
    TOTAL = 'total'

    @classmethod
    def default(cls) -> 'ScatteringTypeEnum':
        return cls.BRAGG

    def description(self) -> str:
        if self is ScatteringTypeEnum.BRAGG:
            return 'Bragg diffraction for conventional structure refinement.'
        elif self is ScatteringTypeEnum.TOTAL:
            return 'Total scattering for pair distribution function analysis (PDF).'

factory

ExperimentFactory

Bases: FactoryBase

Creates Experiment instances with only relevant attributes.

Source code in src/easydiffraction/experiments/experiment/factory.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
class ExperimentFactory(FactoryBase):
    """Creates Experiment instances with only relevant attributes."""

    _ALLOWED_ARG_SPECS = [
        {'required': ['cif_path'], 'optional': []},
        {'required': ['cif_str'], 'optional': []},
        {
            'required': ['name', 'data_path'],
            'optional': ['sample_form', 'beam_mode', 'radiation_probe', 'scattering_type'],
        },
        {
            'required': ['name'],
            'optional': ['sample_form', 'beam_mode', 'radiation_probe', 'scattering_type'],
        },
    ]

    _SUPPORTED = {
        ScatteringTypeEnum.BRAGG: {
            SampleFormEnum.POWDER: BraggPdExperiment,
            SampleFormEnum.SINGLE_CRYSTAL: BraggScExperiment,
        },
        ScatteringTypeEnum.TOTAL: {
            SampleFormEnum.POWDER: TotalPdExperiment,
        },
    }

    @classmethod
    def _make_experiment_type(cls, kwargs):
        """Helper to construct an ExperimentType from keyword arguments,
        using defaults as needed.
        """
        # TODO: Defaults are already in the experiment type...
        # TODO: Merging with experiment_type_from_block from
        #  io.cif.parse
        return ExperimentType(
            sample_form=kwargs.get('sample_form', SampleFormEnum.default().value),
            beam_mode=kwargs.get('beam_mode', BeamModeEnum.default().value),
            radiation_probe=kwargs.get('radiation_probe', RadiationProbeEnum.default().value),
            scattering_type=kwargs.get('scattering_type', ScatteringTypeEnum.default().value),
        )

    # TODO: Move to a common CIF utility module? io.cif.parse?
    @classmethod
    def _create_from_gemmi_block(
        cls,
        block: gemmi.cif.Block,
    ) -> ExperimentBase:
        """Build a model instance from a single CIF block."""
        name = name_from_block(block)

        # TODO: move to io.cif.parse?
        expt_type = ExperimentType()
        for param in expt_type.parameters:
            param.from_cif(block)

        # Create experiment instance of appropriate class
        # TODO: make helper method to create experiment from type
        scattering_type = expt_type.scattering_type.value
        sample_form = expt_type.sample_form.value
        expt_class = cls._SUPPORTED[scattering_type][sample_form]
        expt_obj = expt_class(name=name, type=expt_type)

        # Read all categories from CIF block
        # TODO: move to io.cif.parse?
        for category in expt_obj.categories:
            category.from_cif(block)

        return expt_obj

    @classmethod
    def _create_from_cif_path(
        cls,
        cif_path: str,
    ) -> ExperimentBase:
        """Create an experiment from a CIF file path."""
        doc = document_from_path(cif_path)
        block = pick_sole_block(doc)
        return cls._create_from_gemmi_block(block)

    @classmethod
    def _create_from_cif_str(
        cls,
        cif_str: str,
    ) -> ExperimentBase:
        """Create an experiment from a CIF string."""
        doc = document_from_string(cif_str)
        block = pick_sole_block(doc)
        return cls._create_from_gemmi_block(block)

    @classmethod
    def _create_from_data_path(cls, kwargs):
        """Create an experiment from a raw data ASCII file.

        Loads the experiment and attaches measured data from the
        specified file.
        """
        expt_type = cls._make_experiment_type(kwargs)
        scattering_type = expt_type.scattering_type.value
        sample_form = expt_type.sample_form.value
        expt_class = cls._SUPPORTED[scattering_type][sample_form]
        expt_name = kwargs['name']
        expt_obj = expt_class(name=expt_name, type=expt_type)
        data_path = kwargs['data_path']
        expt_obj._load_ascii_data_to_experiment(data_path)
        return expt_obj

    @classmethod
    def _create_without_data(cls, kwargs):
        """Create an experiment without measured data.

        Returns an experiment instance with only metadata and
        configuration.
        """
        expt_type = cls._make_experiment_type(kwargs)
        scattering_type = expt_type.scattering_type.value
        sample_form = expt_type.sample_form.value
        expt_class = cls._SUPPORTED[scattering_type][sample_form]
        expt_name = kwargs['name']
        expt_obj = expt_class(name=expt_name, type=expt_type)
        return expt_obj

    @classmethod
    def create(cls, **kwargs):
        """Create an `ExperimentBase` using a validated argument
        combination.
        """
        # TODO: move to FactoryBase
        # Check for valid argument combinations
        user_args = {k for k, v in kwargs.items() if v is not None}
        cls._validate_args(
            present=user_args,
            allowed_specs=cls._ALLOWED_ARG_SPECS,
            factory_name=cls.__name__,  # TODO: move to FactoryBase
        )

        # Validate enum arguments if provided
        if 'sample_form' in kwargs:
            SampleFormEnum(kwargs['sample_form'])
        if 'beam_mode' in kwargs:
            BeamModeEnum(kwargs['beam_mode'])
        if 'radiation_probe' in kwargs:
            RadiationProbeEnum(kwargs['radiation_probe'])
        if 'scattering_type' in kwargs:
            ScatteringTypeEnum(kwargs['scattering_type'])

        # Dispatch to the appropriate creation method
        if 'cif_path' in kwargs:
            return cls._create_from_cif_path(kwargs['cif_path'])
        elif 'cif_str' in kwargs:
            return cls._create_from_cif_str(kwargs['cif_str'])
        elif 'data_path' in kwargs:
            return cls._create_from_data_path(kwargs)
        elif 'name' in kwargs:
            return cls._create_without_data(kwargs)
create(**kwargs) classmethod

Create an ExperimentBase using a validated argument combination.

Source code in src/easydiffraction/experiments/experiment/factory.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
@classmethod
def create(cls, **kwargs):
    """Create an `ExperimentBase` using a validated argument
    combination.
    """
    # TODO: move to FactoryBase
    # Check for valid argument combinations
    user_args = {k for k, v in kwargs.items() if v is not None}
    cls._validate_args(
        present=user_args,
        allowed_specs=cls._ALLOWED_ARG_SPECS,
        factory_name=cls.__name__,  # TODO: move to FactoryBase
    )

    # Validate enum arguments if provided
    if 'sample_form' in kwargs:
        SampleFormEnum(kwargs['sample_form'])
    if 'beam_mode' in kwargs:
        BeamModeEnum(kwargs['beam_mode'])
    if 'radiation_probe' in kwargs:
        RadiationProbeEnum(kwargs['radiation_probe'])
    if 'scattering_type' in kwargs:
        ScatteringTypeEnum(kwargs['scattering_type'])

    # Dispatch to the appropriate creation method
    if 'cif_path' in kwargs:
        return cls._create_from_cif_path(kwargs['cif_path'])
    elif 'cif_str' in kwargs:
        return cls._create_from_cif_str(kwargs['cif_str'])
    elif 'data_path' in kwargs:
        return cls._create_from_data_path(kwargs)
    elif 'name' in kwargs:
        return cls._create_without_data(kwargs)

instrument_mixin

InstrumentMixin

Mixin that wires an experiment to an instrument category.

Creates a default instrument via InstrumentFactory using the experiment type (scattering type and beam mode) at initialization.

Source code in src/easydiffraction/experiments/experiment/instrument_mixin.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
class InstrumentMixin:
    """Mixin that wires an experiment to an instrument category.

    Creates a default instrument via `InstrumentFactory` using the
    experiment type (scattering type and beam mode) at initialization.
    """

    def __init__(self, *args, **kwargs):
        expt_type = kwargs.get('type')
        super().__init__(*args, **kwargs)
        self._instrument = InstrumentFactory.create(
            scattering_type=expt_type.scattering_type.value,
            beam_mode=expt_type.beam_mode.value,
        )

    @property
    def instrument(self):
        """Instrument category object associated with the experiment."""
        return self._instrument

    @instrument.setter
    @typechecked
    def instrument(self, new_instrument: InstrumentBase):
        """Replace the instrument and re-parent it to this experiment.

        Args:
            new_instrument: Instrument instance compatible with the
                experiment type.
        """
        self._instrument = new_instrument
        self._instrument._parent = self
instrument property writable

Instrument category object associated with the experiment.

total_pd

TotalPdExperiment

Bases: PdExperimentBase

PDF experiment class with specific attributes.

Source code in src/easydiffraction/experiments/experiment/total_pd.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
class TotalPdExperiment(PdExperimentBase):
    """PDF experiment class with specific attributes."""

    def __init__(
        self,
        name: str,
        type: ExperimentType,
    ):
        super().__init__(name=name, type=type)

    def _load_ascii_data_to_experiment(self, data_path):
        """Loads x, y, sy values from an ASCII data file into the
        experiment.

        The file must be structured as:
            x  y  sy
        """
        try:
            from diffpy.utils.parsers.loaddata import loadData
        except ImportError:
            raise ImportError('diffpy module not found.') from None
        try:
            data = loadData(data_path)
        except Exception as e:
            raise IOError(f'Failed to read data from {data_path}: {e}') from e

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

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

        x = data[:, 0]
        y = data[:, 1]
        sy = data[:, 2] if data.shape[1] > 2 else np.full_like(y, fill_value=default_sy)

        self.data._set_x(x)
        self.data._set_meas(y)
        self.data._set_meas_su(sy)

        console.paragraph('Data loaded successfully')
        console.print(f"Experiment 🔬 '{self.name}'. Number of data points: {len(x)}")

experiments

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/experiments/experiments.py
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 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
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)

    # --------------------
    # Add / Remove methods
    # --------------------

    # TODO: Move to DatablockCollection?
    # TODO: Disallow args and only allow kwargs?
    def add(self, **kwargs):
        experiment = kwargs.pop('experiment', None)

        if experiment is None:
            experiment = ExperimentFactory.create(**kwargs)

        self._add(experiment)

    # @typechecked
    # def add_from_cif_path(self, cif_path: str):
    #    """Add an experiment from a CIF file path.
    #
    #    Args:
    #        cif_path: Path to a CIF document.
    #    """
    #    experiment = ExperimentFactory.create(cif_path=cif_path)
    #    self.add(experiment)

    # @typechecked
    # def add_from_cif_str(self, cif_str: str):
    #    """Add an experiment from a CIF string.
    #
    #    Args:
    #        cif_str: Full CIF document as a string.
    #    """
    #    experiment = ExperimentFactory.create(cif_str=cif_str)
    #    self.add(experiment)

    # @typechecked
    # def add_from_data_path(
    #    self,
    #    name: str,
    #    data_path: str,
    #    sample_form: str = SampleFormEnum.default().value,
    #    beam_mode: str = BeamModeEnum.default().value,
    #    radiation_probe: str = RadiationProbeEnum.default().value,
    #    scattering_type: str = ScatteringTypeEnum.default().value,
    # ):
    #    """Add an experiment from a data file path.
    #
    #    Args:
    #        name: Experiment identifier.
    #        data_path: Path to the measured data file.
    #        sample_form: Sample form (powder or single crystal).
    #        beam_mode: Beam mode (constant wavelength or TOF).
    #        radiation_probe: Radiation probe (neutron or xray).
    #        scattering_type: Scattering type (bragg or total).
    #    """
    #    experiment = ExperimentFactory.create(
    #        name=name,
    #        data_path=data_path,
    #        sample_form=sample_form,
    #        beam_mode=beam_mode,
    #        radiation_probe=radiation_probe,
    #        scattering_type=scattering_type,
    #    )
    #    self.add(experiment)

    # @typechecked
    # def add_without_data(
    #    self,
    #    name: str,
    #    sample_form: str = SampleFormEnum.default().value,
    #    beam_mode: str = BeamModeEnum.default().value,
    #    radiation_probe: str = RadiationProbeEnum.default().value,
    #    scattering_type: str = ScatteringTypeEnum.default().value,
    # ):
    #    """Add an experiment without associating a data file.
    #
    #    Args:
    #        name: Experiment identifier.
    #        sample_form: Sample form (powder or single crystal).
    #        beam_mode: Beam mode (constant wavelength or TOF).
    #        radiation_probe: Radiation probe (neutron or xray).
    #        scattering_type: Scattering type (bragg or total).
    #    """
    #    experiment = ExperimentFactory.create(
    #        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 remove(self, name: str) -> None:
        """Remove an experiment by name if it exists."""
        if name in self:
            del self[name]

    # ------------
    # Show methods
    # ------------

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

    # TODO: Move to DatablockCollection?
    def show_params(self) -> None:
        """Print parameters for each experiment in the collection."""
        for exp in self.values():
            exp.show_params()

remove(name)

Remove an experiment by name if it exists.

Source code in src/easydiffraction/experiments/experiments.py
114
115
116
117
118
@typechecked
def remove(self, name: str) -> None:
    """Remove an experiment by name if it exists."""
    if name in self:
        del self[name]

show_names()

Print the list of experiment names.

Source code in src/easydiffraction/experiments/experiments.py
125
126
127
128
def show_names(self) -> None:
    """Print the list of experiment names."""
    console.paragraph('Defined experiments' + ' 🔬')
    console.print(self.names)

show_params()

Print parameters for each experiment in the collection.

Source code in src/easydiffraction/experiments/experiments.py
131
132
133
134
def show_params(self) -> None:
    """Print parameters for each experiment in the collection."""
    for exp in self.values():
        exp.show_params()