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
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class BackgroundBase(CategoryCollection):
    """Abstract base for background subcategories in experiments.

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

    @abstractmethod
    def calculate(self, x_data: Any) -> Any:
        """Compute background values for the provided x grid.

        Args:
            x_data: X positions (e.g. 2θ, TOF) at which to evaluate.

        Returns:
            Background intensity array aligned with ``x_data``.
        """
        pass

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

Compute background values for the provided x grid.

Parameters:

Name Type Description Default
x_data Any

X positions (e.g. 2θ, TOF) at which to evaluate.

required

Returns:

Type Description
Any

Background intensity array aligned with x_data.

Source code in src/easydiffraction/experiments/categories/background/base.py
19
20
21
22
23
24
25
26
27
28
29
@abstractmethod
def calculate(self, x_data: Any) -> Any:
    """Compute background values for the provided x grid.

    Args:
        x_data: X positions (e.g. 2θ, TOF) at which to evaluate.

    Returns:
        Background intensity array aligned with ``x_data``.
    """
    pass
show() abstractmethod

Print a human-readable view of background components.

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

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

    def calculate(self, x_data):
        """Evaluate polynomial background over x_data."""
        if not self._items:
            log.warning('No background points found. Setting background to zero.')
            return np.zeros_like(x_data)

        u = (x_data - x_data.min()) / (x_data.max() - x_data.min()) * 2 - 1
        coefs = [term.coef.value for term in self._items]
        y_data = chebval(u, coefs)
        return y_data

    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,
        )
calculate(x_data)

Evaluate polynomial background over x_data.

Source code in src/easydiffraction/experiments/categories/background/chebyshev.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
def calculate(self, x_data):
    """Evaluate polynomial background over x_data."""
    if not self._items:
        log.warning('No background points found. Setting background to zero.')
        return np.zeros_like(x_data)

    u = (x_data - x_data.min()) / (x_data.max() - x_data.min()) * 2 - 1
    coefs = [term.coef.value for term in self._items]
    y_data = chebval(u, coefs)
    return y_data
show()

Print a table of polynomial orders and coefficients.

Source code in src/easydiffraction/experiments/categories/background/chebyshev.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
class 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, *, order: int, coef: float) -> None:
        super().__init__()

        # Canonical descriptors
        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.order.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
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
class LineSegment(CategoryItem):
    """Single background control point for interpolation."""

    def __init__(self, *, x: float, y: float):
        super().__init__()

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

        self._identity.category_code = 'background'
        self._identity.category_entry_name = lambda: str(self.x.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
 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
class LineSegmentBackground(BackgroundBase):
    _description: str = 'Linear interpolation between points'

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

    def calculate(self, x_data):
        """Interpolate background points over x_data."""
        if not self:
            log.warning('No background points found. Setting background to zero.')
            return np.zeros_like(x_data)

        background_x = np.array([point.x.value for point in self.values()])
        background_y = np.array([point.y.value for point in self.values()])
        interp_func = interp1d(
            background_x,
            background_y,
            kind='linear',
            bounds_error=False,
            fill_value=(background_y[0], background_y[-1]),
        )
        y_data = interp_func(x_data)
        return y_data

    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,
        )
calculate(x_data)

Interpolate background points over x_data.

Source code in src/easydiffraction/experiments/categories/background/line_segment.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def calculate(self, x_data):
    """Interpolate background points over x_data."""
    if not self:
        log.warning('No background points found. Setting background to zero.')
        return np.zeros_like(x_data)

    background_x = np.array([point.x.value for point in self.values()])
    background_y = np.array([point.y.value for point in self.values()])
    interp_func = interp1d(
        background_x,
        background_y,
        kind='linear',
        bounds_error=False,
        fill_value=(background_y[0], background_y[-1]),
    )
    y_data = interp_func(x_data)
    return y_data
show()

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

Source code in src/easydiffraction/experiments/categories/background/line_segment.py
107
108
109
110
111
112
113
114
115
116
117
118
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
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
class ExcludedRegion(CategoryItem):
    """Closed interval [start, end] to be excluded."""

    def __init__(
        self,
        *,
        start: float,
        end: float,
    ):
        super().__init__()

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

Source code in src/easydiffraction/experiments/categories/excluded_regions.py
 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
class ExcludedRegions(CategoryCollection):
    """Collection of ExcludedRegion instances."""

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

    def add(self, item: ExcludedRegion) -> None:
        """Mark excluded points in the pattern when a region is
        added.
        """
        # 1. Call parent add first

        super().add(item)

        # 2. Now add extra behavior specific to ExcludedRegions

        datastore = self._parent.datastore

        # Boolean mask for points within the new excluded region
        in_region = (datastore.full_x >= item.start.value) & (datastore.full_x <= item.end.value)

        # Update the exclusion mask
        datastore.excluded[in_region] = True

        # Update the excluded points in the datastore
        datastore.x = datastore.full_x[~datastore.excluded]
        datastore.meas = datastore.full_meas[~datastore.excluded]
        datastore.meas_su = datastore.full_meas_su[~datastore.excluded]

    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,
        )
add(item)

Mark excluded points in the pattern when a region is added.

Source code in src/easydiffraction/experiments/categories/excluded_regions.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def add(self, item: ExcludedRegion) -> None:
    """Mark excluded points in the pattern when a region is
    added.
    """
    # 1. Call parent add first

    super().add(item)

    # 2. Now add extra behavior specific to ExcludedRegions

    datastore = self._parent.datastore

    # Boolean mask for points within the new excluded region
    in_region = (datastore.full_x >= item.start.value) & (datastore.full_x <= item.end.value)

    # Update the exclusion mask
    datastore.excluded[in_region] = True

    # Update the excluded points in the datastore
    datastore.x = datastore.full_x[~datastore.excluded]
    datastore.meas = datastore.full_meas[~datastore.excluded]
    datastore.meas_su = datastore.full_meas_su[~datastore.excluded]
show()

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

Source code in src/easydiffraction/experiments/categories/excluded_regions.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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 str

Powder or Single crystal.

required
beam_mode str

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

required
radiation_probe str

Neutrons or X-rays.

required
scattering_type str

Bragg or Total.

required
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: str,
        beam_mode: str,
        radiation_probe: str,
        scattering_type: str,
    ):
        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(),
                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(),
                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(),
                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(),
                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: str,  # TODO: need new name instead of id
        scale: float,
    ):
        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
class CwlBroadeningMixin:
    """Mixin that adds CWL Gaussian and Lorentz broadening
    parameters.
    """

    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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
class 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
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
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.

datastore

base

DatastoreBase

Base class for all data stores.

Attributes:

Name Type Description
meas Optional[ndarray]

Measured intensities.

meas_su Optional[ndarray]

Standard uncertainties of measured intensities.

excluded Optional[ndarray]

Flags for excluded points.

_calc Optional[ndarray]

Stored calculated intensities.

Source code in src/easydiffraction/experiments/datastore/base.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class DatastoreBase:
    """Base class for all data stores.

    Attributes:
        meas (Optional[np.ndarray]): Measured intensities.
        meas_su (Optional[np.ndarray]): Standard uncertainties of
            measured intensities.
        excluded (Optional[np.ndarray]): Flags for excluded points.
        _calc (Optional[np.ndarray]): Stored calculated intensities.
    """

    def __init__(self) -> None:
        self.meas: Optional[np.ndarray] = None
        self.meas_su: Optional[np.ndarray] = None
        self.excluded: Optional[np.ndarray] = None
        self._calc: Optional[np.ndarray] = None

    @property
    def calc(self) -> Optional[np.ndarray]:
        """Access calculated intensities. Should be updated via external
        calculation.

        Returns:
            Optional[np.ndarray]: Calculated intensities array or None
                if not set.
        """
        return self._calc

    @calc.setter
    @typechecked
    def calc(self, values: np.ndarray) -> None:
        """Set calculated intensities (from
        Analysis.calculate_pattern()).

        Args:
            values (np.ndarray): Array of calculated intensities.
        """
        self._calc = values

    @abstractmethod
    def _cif_mapping(self) -> dict[str, str]:
        """Must be implemented in subclasses to return a mapping from
        attribute names to CIF tags.

        Returns:
            dict[str, str]: Mapping from attribute names to CIF tags.
        """
        pass

    @property
    def as_cif(self) -> str:
        """Generate a CIF-formatted string representing the datastore
        data.
        """
        return datastore_to_cif(self)

    @property
    def as_truncated_cif(self) -> str:
        """Generate a CIF-formatted string representing the datastore
        data.
        """
        return datastore_to_cif(self, max_points=5)
as_cif property

Generate a CIF-formatted string representing the datastore data.

as_truncated_cif property

Generate a CIF-formatted string representing the datastore data.

calc property writable

Access calculated intensities. Should be updated via external calculation.

Returns:

Type Description
Optional[ndarray]

Optional[np.ndarray]: Calculated intensities array or None if not set.

factory

Factory for experiment datastores based on sample form and beam mode.

DatastoreFactory

Create PD or SC datastores depending on sample form.

Source code in src/easydiffraction/experiments/datastore/factory.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class DatastoreFactory:
    """Create PD or SC datastores depending on sample form."""

    _supported = {
        'powder': PdDatastore,
        'single crystal': ScDatastore,
    }

    @classmethod
    def create(
        cls,
        sample_form: str = SampleFormEnum.default(),
        beam_mode: str = BeamModeEnum.default(),
    ) -> DatastoreBase:
        """Create and return a datastore object for the given sample
        form.

        Args:
            sample_form (str): Sample form type, e.g. 'powder' or
                'single crystal'.
            beam_mode (str): Beam mode for powder sample form.

        Returns:
            DatastoreBase: Instance of a datastore class corresponding
                to sample form.

        Raises:
            ValueError: If the sample_form or beam_mode is not
                supported.
        """
        supported_sample_forms = list(cls._supported.keys())
        if sample_form not in supported_sample_forms:
            raise ValueError(
                f"Unsupported sample form: '{sample_form}'.\n"
                f'Supported sample forms: {supported_sample_forms}'
            )

        supported_beam_modes = ['time-of-flight', 'constant wavelength']
        if beam_mode not in supported_beam_modes:
            raise ValueError(
                f"Unsupported beam mode: '{beam_mode}'.\n"
                f'Supported beam modes: {supported_beam_modes}'
            )

        datastore_class = cls._supported[sample_form]
        if sample_form == 'powder':
            datastore_obj = datastore_class(beam_mode=beam_mode)
        else:
            datastore_obj = datastore_class()

        return datastore_obj
create(sample_form=SampleFormEnum.default(), beam_mode=BeamModeEnum.default()) classmethod

Create and return a datastore object for the given sample form.

Parameters:

Name Type Description Default
sample_form str

Sample form type, e.g. 'powder' or 'single crystal'.

default()
beam_mode str

Beam mode for powder sample form.

default()

Returns:

Name Type Description
DatastoreBase DatastoreBase

Instance of a datastore class corresponding to sample form.

Raises:

Type Description
ValueError

If the sample_form or beam_mode is not supported.

Source code in src/easydiffraction/experiments/datastore/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
@classmethod
def create(
    cls,
    sample_form: str = SampleFormEnum.default(),
    beam_mode: str = BeamModeEnum.default(),
) -> DatastoreBase:
    """Create and return a datastore object for the given sample
    form.

    Args:
        sample_form (str): Sample form type, e.g. 'powder' or
            'single crystal'.
        beam_mode (str): Beam mode for powder sample form.

    Returns:
        DatastoreBase: Instance of a datastore class corresponding
            to sample form.

    Raises:
        ValueError: If the sample_form or beam_mode is not
            supported.
    """
    supported_sample_forms = list(cls._supported.keys())
    if sample_form not in supported_sample_forms:
        raise ValueError(
            f"Unsupported sample form: '{sample_form}'.\n"
            f'Supported sample forms: {supported_sample_forms}'
        )

    supported_beam_modes = ['time-of-flight', 'constant wavelength']
    if beam_mode not in supported_beam_modes:
        raise ValueError(
            f"Unsupported beam mode: '{beam_mode}'.\n"
            f'Supported beam modes: {supported_beam_modes}'
        )

    datastore_class = cls._supported[sample_form]
    if sample_form == 'powder':
        datastore_obj = datastore_class(beam_mode=beam_mode)
    else:
        datastore_obj = datastore_class()

    return datastore_obj

pd

PdDatastore

Bases: DatastoreBase

Class for powder diffraction data.

Attributes:

Name Type Description
x Optional[ndarray]

Scan variable (e.g. 2θ or time-of-flight values).

d Optional[ndarray]

d-spacing values.

bkg Optional[ndarray]

Background values.

Source code in src/easydiffraction/experiments/datastore/pd.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
class PdDatastore(DatastoreBase):
    """Class for powder diffraction data.

    Attributes:
        x (Optional[np.ndarray]): Scan variable (e.g. 2θ or
            time-of-flight values).
        d (Optional[np.ndarray]): d-spacing values.
        bkg (Optional[np.ndarray]): Background values.
    """

    def __init__(self, beam_mode: Optional[BeamModeEnum] = None) -> None:
        """Initialize PowderDatastore.

        Args:
            beam_mode (str): Beam mode, e.g. 'time-of-flight' or
                'constant wavelength'.
        """
        super().__init__()

        if beam_mode is None:
            beam_mode = BeamModeEnum.default()

        self.beam_mode = beam_mode
        self.x: Optional[np.ndarray] = None
        self.d: Optional[np.ndarray] = None
        self.bkg: Optional[np.ndarray] = None

    def _cif_mapping(self) -> dict[str, str]:
        """Return mapping from attribute names to CIF tags based on beam
        mode.

        Returns:
            dict[str, str]: Mapping dictionary.
        """
        # TODO: Decide where to have validation for beam_mode,
        #  here or in Experiment class or somewhere else.
        return {
            'time-of-flight': {
                'x': '_pd_meas.time_of_flight',
                'meas': '_pd_meas.intensity_total',
                'meas_su': '_pd_meas.intensity_total_su',
            },
            'constant wavelength': {
                'x': '_pd_meas.2theta_scan',
                'meas': '_pd_meas.intensity_total',
                'meas_su': '_pd_meas.intensity_total_su',
            },
        }[self.beam_mode]
__init__(beam_mode=None)

Initialize PowderDatastore.

Parameters:

Name Type Description Default
beam_mode str

Beam mode, e.g. 'time-of-flight' or 'constant wavelength'.

None
Source code in src/easydiffraction/experiments/datastore/pd.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def __init__(self, beam_mode: Optional[BeamModeEnum] = None) -> None:
    """Initialize PowderDatastore.

    Args:
        beam_mode (str): Beam mode, e.g. 'time-of-flight' or
            'constant wavelength'.
    """
    super().__init__()

    if beam_mode is None:
        beam_mode = BeamModeEnum.default()

    self.beam_mode = beam_mode
    self.x: Optional[np.ndarray] = None
    self.d: Optional[np.ndarray] = None
    self.bkg: Optional[np.ndarray] = None

sc

ScDatastore

Bases: DatastoreBase

Class for single crystal diffraction data.

Attributes:

Name Type Description
sin_theta_over_lambda Optional[ndarray]

sin(θ)/λ values.

index_h Optional[ndarray]

Miller index h.

index_k Optional[ndarray]

Miller index k.

index_l Optional[ndarray]

Miller index l.

Source code in src/easydiffraction/experiments/datastore/sc.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class ScDatastore(DatastoreBase):
    """Class for single crystal diffraction data.

    Attributes:
        sin_theta_over_lambda (Optional[np.ndarray]): sin(θ)/λ values.
        index_h (Optional[np.ndarray]): Miller index h.
        index_k (Optional[np.ndarray]): Miller index k.
        index_l (Optional[np.ndarray]): Miller index l.
    """

    def __init__(self) -> None:
        """Initialize SingleCrystalDatastore."""
        super().__init__()
        self.sin_theta_over_lambda: Optional[np.ndarray] = None
        self.index_h: Optional[np.ndarray] = None
        self.index_k: Optional[np.ndarray] = None
        self.index_l: Optional[np.ndarray] = None

    def _cif_mapping(self) -> dict[str, str]:
        """Return mapping from attribute names to CIF tags for single
        crystal data.

        Returns:
            dict[str, str]: Mapping dictionary.
        """
        return {
            'index_h': '_refln.index_h',
            'index_k': '_refln.index_k',
            'index_l': '_refln.index_l',
            'meas': '_refln.intensity_meas',
            'meas_su': '_refln.intensity_meas_su',
        }
__init__()

Initialize SingleCrystalDatastore.

Source code in src/easydiffraction/experiments/datastore/sc.py
25
26
27
28
29
30
31
def __init__(self) -> None:
    """Initialize SingleCrystalDatastore."""
    super().__init__()
    self.sin_theta_over_lambda: Optional[np.ndarray] = None
    self.index_h: Optional[np.ndarray] = None
    self.index_k: Optional[np.ndarray] = None
    self.index_l: Optional[np.ndarray] = None

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
135
136
137
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 datastore.

        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)

        # Attach the data to the experiment's datastore
        self.datastore.full_x = x
        self.datastore.full_meas = y
        self.datastore.full_meas_su = sy
        self.datastore.x = x
        self.datastore.meas = y
        self.datastore.meas_su = sy
        self.datastore.excluded = np.full(x.shape, fill_value=False, dtype=bool)

        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
134
135
136
137
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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, instrument and datastore.

Source code in src/easydiffraction/experiments/experiment/base.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class ExperimentBase(DatablockItem):
    """Base class for all experiments with only core attributes.

    Wraps experiment type, instrument and datastore.
    """

    def __init__(
        self,
        *,
        name: str,
        type: ExperimentType,
    ):
        super().__init__()
        self._name = name
        self._type = type
        self._datastore = DatastoreFactory.create(
            sample_form=self.type.sample_form.value,
            beam_mode=self.type.beam_mode.value,
        )
        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 datastore(self):
        """Data container with x, y, error, calc and background
        arrays.
        """
        return self._datastore

    @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 and datastore as CIF text."""
        experiment_cif = super().as_cif
        datastore_cif = self.datastore.as_truncated_cif
        cif_text: str = f'{experiment_cif}\n\n{datastore_cif}'
        paragraph_title: str = f"Experiment 🔬 '{self.name}' as cif"
        console.paragraph(paragraph_title)
        render_cif(cif_text)

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

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

as_cif property

Serialize this experiment to a CIF fragment.

datastore property

Data container with x, y, error, calc and background arrays.

name property writable

Human-readable name of the experiment.

show_as_cif()

Pretty-print the experiment and datastore as CIF text.

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

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
class PdExperimentBase(ExperimentBase):
    """Base class for all powder experiments."""

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

        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._linked_phases: LinkedPhases = LinkedPhases()
        self._excluded_regions: ExcludedRegions = ExcludedRegions()

    @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 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 linked_phases(self) -> str:
        """Collection of phases linked to this experiment."""
        return self._linked_phases

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

    @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
217
218
219
220
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
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.datastore.x = x
        self.datastore.meas = y
        self.datastore.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, instrument and datastore.

Source code in src/easydiffraction/experiments/experiment/base.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class ExperimentBase(DatablockItem):
    """Base class for all experiments with only core attributes.

    Wraps experiment type, instrument and datastore.
    """

    def __init__(
        self,
        *,
        name: str,
        type: ExperimentType,
    ):
        super().__init__()
        self._name = name
        self._type = type
        self._datastore = DatastoreFactory.create(
            sample_form=self.type.sample_form.value,
            beam_mode=self.type.beam_mode.value,
        )
        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 datastore(self):
        """Data container with x, y, error, calc and background
        arrays.
        """
        return self._datastore

    @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 and datastore as CIF text."""
        experiment_cif = super().as_cif
        datastore_cif = self.datastore.as_truncated_cif
        cif_text: str = f'{experiment_cif}\n\n{datastore_cif}'
        paragraph_title: str = f"Experiment 🔬 '{self.name}' as cif"
        console.paragraph(paragraph_title)
        render_cif(cif_text)

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

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

Serialize this experiment to a CIF fragment.

datastore property

Data container with x, y, error, calc and background arrays.

name property writable

Human-readable name of the experiment.

show_as_cif()

Pretty-print the experiment and datastore as CIF text.

Source code in src/easydiffraction/experiments/experiment/base.py
79
80
81
82
83
84
85
86
def show_as_cif(self) -> None:
    """Pretty-print the experiment and datastore as CIF text."""
    experiment_cif = super().as_cif
    datastore_cif = self.datastore.as_truncated_cif
    cif_text: str = f'{experiment_cif}\n\n{datastore_cif}'
    paragraph_title: str = f"Experiment 🔬 '{self.name}' as cif"
    console.paragraph(paragraph_title)
    render_cif(cif_text)
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
class PdExperimentBase(ExperimentBase):
    """Base class for all powder experiments."""

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

        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._linked_phases: LinkedPhases = LinkedPhases()
        self._excluded_regions: ExcludedRegions = ExcludedRegions()

    @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 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 linked_phases(self) -> str:
        """Collection of phases linked to this experiment."""
        return self._linked_phases

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

    @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
217
218
219
220
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
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
135
136
137
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 datastore.

        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)

        # Attach the data to the experiment's datastore
        self.datastore.full_x = x
        self.datastore.full_meas = y
        self.datastore.full_meas_su = sy
        self.datastore.x = x
        self.datastore.meas = y
        self.datastore.meas_su = sy
        self.datastore.excluded = np.full(x.shape, fill_value=False, dtype=bool)

        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
134
135
136
137
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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
class BeamModeEnum(str, Enum):
    """Beam delivery mode for the instrument."""

    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
 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
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
 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
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 create(cls, **kwargs):
        """Create an `ExperimentBase` using a validated argument
        combination.
        """
        # Check for valid argument combinations
        user_args = {k for k, v in kwargs.items() if v is not None}
        cls._validate_args(user_args, cls._ALLOWED_ARG_SPECS, cls.__name__)

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

    # -------------------------------
    # Private creation helper methods
    # -------------------------------

    @staticmethod
    def _create_from_cif_path(cif_path):
        """Create an experiment from a CIF file path.

        Not yet implemented.
        """
        # TODO: Implement CIF file loading logic
        raise NotImplementedError('CIF file loading not implemented yet.')

    @staticmethod
    def _create_from_cif_str(cif_str):
        """Create an experiment from a CIF string.

        Not yet implemented.
        """
        # TODO: Implement CIF string loading logic
        raise NotImplementedError('CIF string loading not implemented yet.')

    @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 _make_experiment_type(cls, kwargs):
        """Helper to construct an ExperimentType from keyword arguments,
        using defaults as needed.
        """
        return ExperimentType(
            sample_form=kwargs.get('sample_form', SampleFormEnum.default()),
            beam_mode=kwargs.get('beam_mode', BeamModeEnum.default()),
            radiation_probe=kwargs.get('radiation_probe', RadiationProbeEnum.default()),
            scattering_type=kwargs.get('scattering_type', ScatteringTypeEnum.default()),
        )

    @staticmethod
    def _is_valid_args(user_args):
        """Validate user argument set against allowed combinations.

        Returns True if the argument set matches any valid combination,
        else False.
        """
        user_arg_set = set(user_args)
        for arg_set in ExperimentFactory._valid_arg_sets:
            required = set(arg_set['required'])
            optional = set(arg_set['optional'])
            # Must have all required, and only required+optional
            if required.issubset(user_arg_set) and user_arg_set <= (required | optional):
                return True
        return False
create(**kwargs) classmethod

Create an ExperimentBase using a validated argument combination.

Source code in src/easydiffraction/experiments/experiment/factory.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@classmethod
def create(cls, **kwargs):
    """Create an `ExperimentBase` using a validated argument
    combination.
    """
    # Check for valid argument combinations
    user_args = {k for k, v in kwargs.items() if v is not None}
    cls._validate_args(user_args, cls._ALLOWED_ARG_SPECS, cls.__name__)

    # 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.datastore.x = x
        self.datastore.meas = y
        self.datastore.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
 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
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
    # --------------------

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

    @typechecked
    def remove(self, name: str) -> None:
        """Remove an experiment by name if it exists."""
        if name in self:
            del self[name]

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

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

    def show_params(self) -> None:
        """Print parameters for each experiment in the collection."""
        for exp in self.values():
            exp.show_params()

add_from_cif_path(cif_path)

Add an experiment from a CIF file path.

Parameters:

Name Type Description Default
cif_path str

Path to a CIF document.

required
Source code in src/easydiffraction/experiments/experiments.py
30
31
32
33
34
35
36
37
38
@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)

add_from_cif_str(cif_str)

Add an experiment from a CIF string.

Parameters:

Name Type Description Default
cif_str str

Full CIF document as a string.

required
Source code in src/easydiffraction/experiments/experiments.py
40
41
42
43
44
45
46
47
48
@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)

add_from_data_path(name, data_path, sample_form=SampleFormEnum.default().value, beam_mode=BeamModeEnum.default().value, radiation_probe=RadiationProbeEnum.default().value, scattering_type=ScatteringTypeEnum.default().value)

Add an experiment from a data file path.

Parameters:

Name Type Description Default
name str

Experiment identifier.

required
data_path str

Path to the measured data file.

required
sample_form str

Sample form (powder or single crystal).

value
beam_mode str

Beam mode (constant wavelength or TOF).

value
radiation_probe str

Radiation probe (neutron or xray).

value
scattering_type str

Scattering type (bragg or total).

value
Source code in src/easydiffraction/experiments/experiments.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
@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)

add_without_data(name, sample_form=SampleFormEnum.default().value, beam_mode=BeamModeEnum.default().value, radiation_probe=RadiationProbeEnum.default().value, scattering_type=ScatteringTypeEnum.default().value)

Add an experiment without associating a data file.

Parameters:

Name Type Description Default
name str

Experiment identifier.

required
sample_form str

Sample form (powder or single crystal).

value
beam_mode str

Beam mode (constant wavelength or TOF).

value
radiation_probe str

Radiation probe (neutron or xray).

value
scattering_type str

Scattering type (bragg or total).

value
Source code in src/easydiffraction/experiments/experiments.py
 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
@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)

remove(name)

Remove an experiment by name if it exists.

Source code in src/easydiffraction/experiments/experiments.py
107
108
109
110
111
@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
117
118
119
120
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
122
123
124
125
def show_params(self) -> None:
    """Print parameters for each experiment in the collection."""
    for exp in self.values():
        exp.show_params()