Skip to content

core

category

CategoryCollection

Bases: CollectionBase

Handles loop-style category containers (e.g. AtomSites).

Each item is a CategoryItem (component).

Source code in src/easydiffraction/core/category.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
class CategoryCollection(CollectionBase):
    """
    Handles loop-style category containers (e.g. AtomSites).

    Each item is a CategoryItem (component).
    """

    # TODO: Common for all categories
    _update_priority = 10  # Default. Lower values run first.

    def _key_for(self, item: object) -> str | None:
        """Return the category-level identity key for *item*."""
        return item._identity.category_entry_name

    def _mark_parent_dirty(self) -> None:
        """
        Set ``_need_categories_update`` on the parent datablock.

        Called whenever the collection content changes (items added or
        removed) so that subsequent ``_update_categories()`` calls re-
        run all category updates.
        """
        parent = getattr(self, '_parent', None)
        if parent is not None and hasattr(parent, '_need_categories_update'):
            parent._need_categories_update = True

    def __str__(self) -> str:
        """Human-readable representation of this component."""
        name = self._log_name
        size = len(self)
        return f'<{name} collection ({size} items)>'

    # TODO: Common for all categories
    def _update(self, called_by_minimizer: bool = False) -> None:
        del called_by_minimizer
        pass

    @property
    def unique_name(self) -> str | None:
        """Return None; collections have no unique name."""
        return None

    @property
    def parameters(self) -> list:
        """All parameters from all items in this collection."""
        params = []
        for item in self._items:
            params.extend(item.parameters)
        return params

    @property
    def as_cif(self) -> str:
        """Return CIF representation of this object."""
        return category_collection_to_cif(self)

    def from_cif(self, block: object) -> None:
        """Populate this collection from a CIF block."""
        category_collection_from_cif(self, block)

    def add(self, item: object) -> None:
        """
        Insert or replace a pre-built item into the collection.

        Parameters
        ----------
        item : object
            A ``CategoryItem`` instance to add.
        """
        self[item._identity.category_entry_name] = item
        self._mark_parent_dirty()

    def create(self, **kwargs: object) -> None:
        """
        Create a new item with the given attributes and add it.

        A default instance of the collection's item type is created,
        then each keyword argument is applied via ``setattr``.

        Parameters
        ----------
        **kwargs : object
            Attribute names and values for the new item.
        """
        child_obj = self._item_type()

        for attr, val in kwargs.items():
            setattr(child_obj, attr, val)

        self.add(child_obj)

__str__()

Human-readable representation of this component.

Source code in src/easydiffraction/core/category.py
190
191
192
193
194
def __str__(self) -> str:
    """Human-readable representation of this component."""
    name = self._log_name
    size = len(self)
    return f'<{name} collection ({size} items)>'

add(item)

Insert or replace a pre-built item into the collection.

Parameters:

Name Type Description Default
item object

A CategoryItem instance to add.

required
Source code in src/easydiffraction/core/category.py
223
224
225
226
227
228
229
230
231
232
233
def add(self, item: object) -> None:
    """
    Insert or replace a pre-built item into the collection.

    Parameters
    ----------
    item : object
        A ``CategoryItem`` instance to add.
    """
    self[item._identity.category_entry_name] = item
    self._mark_parent_dirty()

as_cif property

Return CIF representation of this object.

create(**kwargs)

Create a new item with the given attributes and add it.

A default instance of the collection's item type is created, then each keyword argument is applied via setattr.

Parameters:

Name Type Description Default
**kwargs object

Attribute names and values for the new item.

{}
Source code in src/easydiffraction/core/category.py
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def create(self, **kwargs: object) -> None:
    """
    Create a new item with the given attributes and add it.

    A default instance of the collection's item type is created,
    then each keyword argument is applied via ``setattr``.

    Parameters
    ----------
    **kwargs : object
        Attribute names and values for the new item.
    """
    child_obj = self._item_type()

    for attr, val in kwargs.items():
        setattr(child_obj, attr, val)

    self.add(child_obj)

from_cif(block)

Populate this collection from a CIF block.

Source code in src/easydiffraction/core/category.py
219
220
221
def from_cif(self, block: object) -> None:
    """Populate this collection from a CIF block."""
    category_collection_from_cif(self, block)

parameters property

All parameters from all items in this collection.

unique_name property

Return None; collections have no unique name.

CategoryItem

Bases: GuardedBase

Base class for items in a category collection.

Source code in src/easydiffraction/core/category.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 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
class CategoryItem(GuardedBase):
    """Base class for items in a category collection."""

    # TODO: Set different default priorities for CategoryItem and
    #  CategoryCollection and use them when serializing to CIF!
    # TODO: Common for all categories
    _update_priority = 10  # Default. Lower values run first.

    def __str__(self) -> str:
        """Human-readable representation of this component."""
        name = self._log_name
        params = ', '.join(f'{p.name}={p.value!r}' for p in self.parameters)
        return f'<{name} ({params})>'

    # TODO: Common for all categories
    def _update(self, called_by_minimizer: bool = False) -> None:
        del called_by_minimizer
        pass

    @property
    def unique_name(self) -> str:
        """Fully qualified name: datablock, category, entry."""
        parts = [
            self._identity.datablock_entry_name,
            self._identity.category_code,
            self._identity.category_entry_name,
        ]
        # Convert all parts to strings and filter out None/empty values
        str_parts = [str(part) for part in parts if part is not None]
        return '.'.join(str_parts)

    @property
    def parameters(self) -> list:
        """All GenericDescriptorBase instances on this item."""
        return [v for v in vars(self).values() if isinstance(v, GenericDescriptorBase)]

    @property
    def as_cif(self) -> str:
        """Return CIF representation of this object."""
        return category_item_to_cif(self)

    def from_cif(self, block: object, idx: int = 0) -> None:
        """Populate this item from a CIF block."""
        category_item_from_cif(self, block, idx)

    def help(self) -> None:
        """Print parameters, other properties, and methods."""
        from easydiffraction.utils.logging import console
        from easydiffraction.utils.utils import render_table

        cls = type(self)
        console.paragraph(f"Help for '{cls.__name__}'")

        # Deduplicate properties
        seen: dict = {}
        for key, prop in cls._iter_properties():
            if key not in seen:
                seen[key] = prop

        # Split into descriptor-backed and other
        param_rows = []
        other_rows = []
        p_idx = 0
        o_idx = 0
        for key in sorted(seen):
            prop = seen[key]
            try:
                val = getattr(self, key)
            except Exception:
                val = None
            if isinstance(val, GenericDescriptorBase):
                p_idx += 1
                type_str = 'string' if isinstance(val, GenericStringDescriptor) else 'numeric'
                writable = '✓' if prop.fset else '✗'
                param_rows.append([
                    str(p_idx),
                    key,
                    type_str,
                    str(val.value),
                    writable,
                    val.description or '',
                ])
            else:
                o_idx += 1
                writable = '✓' if prop.fset else '✗'
                doc = self._first_sentence(prop.fget.__doc__ if prop.fget else None)
                other_rows.append([str(o_idx), key, writable, doc])

        if param_rows:
            console.paragraph('Parameters')
            render_table(
                columns_headers=[
                    '#',
                    'Name',
                    'Type',
                    'Value',
                    'Writable',
                    'Description',
                ],
                columns_alignment=[
                    'right',
                    'left',
                    'left',
                    'right',
                    'center',
                    'left',
                ],
                columns_data=param_rows,
            )

        if other_rows:
            console.paragraph('Other properties')
            render_table(
                columns_headers=[
                    '#',
                    'Name',
                    'Writable',
                    'Description',
                ],
                columns_alignment=[
                    'right',
                    'left',
                    'center',
                    'left',
                ],
                columns_data=other_rows,
            )

        methods = dict(cls._iter_methods())
        method_rows = []
        for i, key in enumerate(sorted(methods), 1):
            doc = self._first_sentence(getattr(methods[key], '__doc__', None))
            method_rows.append([str(i), f'{key}()', doc])

        if method_rows:
            console.paragraph('Methods')
            render_table(
                columns_headers=['#', 'Name', 'Description'],
                columns_alignment=['right', 'left', 'left'],
                columns_data=method_rows,
            )

__str__()

Human-readable representation of this component.

Source code in src/easydiffraction/core/category.py
26
27
28
29
30
def __str__(self) -> str:
    """Human-readable representation of this component."""
    name = self._log_name
    params = ', '.join(f'{p.name}={p.value!r}' for p in self.parameters)
    return f'<{name} ({params})>'

as_cif property

Return CIF representation of this object.

from_cif(block, idx=0)

Populate this item from a CIF block.

Source code in src/easydiffraction/core/category.py
59
60
61
def from_cif(self, block: object, idx: int = 0) -> None:
    """Populate this item from a CIF block."""
    category_item_from_cif(self, block, idx)

help()

Print parameters, other properties, and methods.

Source code in src/easydiffraction/core/category.py
 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
def help(self) -> None:
    """Print parameters, other properties, and methods."""
    from easydiffraction.utils.logging import console
    from easydiffraction.utils.utils import render_table

    cls = type(self)
    console.paragraph(f"Help for '{cls.__name__}'")

    # Deduplicate properties
    seen: dict = {}
    for key, prop in cls._iter_properties():
        if key not in seen:
            seen[key] = prop

    # Split into descriptor-backed and other
    param_rows = []
    other_rows = []
    p_idx = 0
    o_idx = 0
    for key in sorted(seen):
        prop = seen[key]
        try:
            val = getattr(self, key)
        except Exception:
            val = None
        if isinstance(val, GenericDescriptorBase):
            p_idx += 1
            type_str = 'string' if isinstance(val, GenericStringDescriptor) else 'numeric'
            writable = '✓' if prop.fset else '✗'
            param_rows.append([
                str(p_idx),
                key,
                type_str,
                str(val.value),
                writable,
                val.description or '',
            ])
        else:
            o_idx += 1
            writable = '✓' if prop.fset else '✗'
            doc = self._first_sentence(prop.fget.__doc__ if prop.fget else None)
            other_rows.append([str(o_idx), key, writable, doc])

    if param_rows:
        console.paragraph('Parameters')
        render_table(
            columns_headers=[
                '#',
                'Name',
                'Type',
                'Value',
                'Writable',
                'Description',
            ],
            columns_alignment=[
                'right',
                'left',
                'left',
                'right',
                'center',
                'left',
            ],
            columns_data=param_rows,
        )

    if other_rows:
        console.paragraph('Other properties')
        render_table(
            columns_headers=[
                '#',
                'Name',
                'Writable',
                'Description',
            ],
            columns_alignment=[
                'right',
                'left',
                'center',
                'left',
            ],
            columns_data=other_rows,
        )

    methods = dict(cls._iter_methods())
    method_rows = []
    for i, key in enumerate(sorted(methods), 1):
        doc = self._first_sentence(getattr(methods[key], '__doc__', None))
        method_rows.append([str(i), f'{key}()', doc])

    if method_rows:
        console.paragraph('Methods')
        render_table(
            columns_headers=['#', 'Name', 'Description'],
            columns_alignment=['right', 'left', 'left'],
            columns_data=method_rows,
        )

parameters property

All GenericDescriptorBase instances on this item.

unique_name property

Fully qualified name: datablock, category, entry.

collection

Lightweight container for guarded items with name-based indexing.

CollectionBase maintains an ordered list of items and a lazily rebuilt index by the item's identity key. It supports dict-like access for get, set and delete, along with iteration over the items.

CollectionBase

Bases: GuardedBase

A minimal collection with stable iteration and name indexing.

Parameters:

Name Type Description Default
item_type type

Type of items accepted by the collection. Used for validation and tooling; not enforced at runtime here.

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

    Parameters
    ----------
    item_type : type
        Type of items accepted by the collection. Used for validation
        and tooling; not enforced at runtime here.
    """

    def __init__(self, item_type: type) -> None:
        super().__init__()
        self._items: list = []
        self._index: dict = {}
        self._item_type = item_type

    def __getitem__(self, key: str | int) -> GuardedBase:
        """
        Return an item by name or positional index.

        Parameters
        ----------
        key : str | int
            Identity key (str) or zero-based positional index (int).

        Returns
        -------
        GuardedBase
            The item matching the given key or index.

        Raises
        ------
        TypeError
            If *key* is neither ``str`` nor ``int``.
        """
        if isinstance(key, int):
            return self._items[key]
        if isinstance(key, str):
            try:
                return self._index[key]
            except KeyError:
                self._rebuild_index()
                return self._index[key]
        raise TypeError(f'Collection indices must be str or int, not {type(key).__name__}')

    def __setitem__(self, name: str, item: GuardedBase) -> None:
        """Insert or replace an item under the given identity key."""
        # Check if item with same key exists; if so, replace it
        for i, existing_item in enumerate(self._items):
            if self._key_for(existing_item) == name:
                self._items[i] = item
                self._rebuild_index()
                return
        # Otherwise append new item
        item._parent = self  # Explicitly set the parent for the item
        self._items.append(item)
        self._rebuild_index()

    def __delitem__(self, name: str) -> None:
        """Delete an item by key or raise ``KeyError`` if missing."""
        for i, item in enumerate(self._items):
            if self._key_for(item) == name:
                object.__setattr__(item, '_parent', None)  # Unlink the parent before removal
                del self._items[i]
                self._rebuild_index()
                return
        raise KeyError(name)

    def __contains__(self, name: str) -> bool:
        """Check whether an item with the given key exists."""
        self._rebuild_index()
        return name in self._index

    def __iter__(self) -> Iterator[GuardedBase]:
        """Iterate over items in insertion order."""
        return iter(self._items)

    def __len__(self) -> int:
        """Return the number of items in the collection."""
        return len(self._items)

    def remove(self, name: str) -> None:
        """
        Remove an item by its key.

        Parameters
        ----------
        name : str
            Identity key of the item to remove.

        Raises
        ------
        KeyError
            If no item with the given key exists.
        """
        try:
            del self[name]
        except KeyError:
            raise

    def _key_for(self, item: GuardedBase) -> str | None:
        """
        Return the identity key for *item*.

        Subclasses must override to return the appropriate key
        (``category_entry_name`` or ``datablock_entry_name``).
        """
        return item._identity.category_entry_name or item._identity.datablock_entry_name

    def _rebuild_index(self) -> None:
        """Rebuild the name-to-item index from the ordered item list."""
        self._index.clear()
        for item in self._items:
            key = self._key_for(item)
            if key:
                self._index[key] = item

    def keys(self) -> Generator[str | None, None, None]:
        """Yield keys for all items in insertion order."""
        return (self._key_for(item) for item in self._items)

    def values(self) -> Generator[GuardedBase, None, None]:
        """Yield items in insertion order."""
        return (item for item in self._items)

    def items(self) -> Generator[tuple[str | None, GuardedBase], None, None]:
        """Yield ``(key, item)`` pairs in insertion order."""
        return ((self._key_for(item), item) for item in self._items)

    @property
    def names(self) -> list[str | None]:
        """List of all item keys in the collection."""
        return list(self.keys())

    def help(self) -> None:
        """Print a summary of public attributes and contained items."""
        super().help()

        from easydiffraction.utils.logging import console
        from easydiffraction.utils.utils import render_table

        if self._items:
            console.paragraph(f'Items ({len(self._items)})')
            rows = []
            for i, item in enumerate(self._items, 1):
                key = self._key_for(item)
                rows.append([str(i), str(key), f"['{key}']"])
            render_table(
                columns_headers=['#', 'Name', 'Access'],
                columns_alignment=['right', 'left', 'left'],
                columns_data=rows,
            )
        else:
            console.paragraph('Items')
            console.print('(empty)')

__contains__(name)

Check whether an item with the given key exists.

Source code in src/easydiffraction/core/collection.py
88
89
90
91
def __contains__(self, name: str) -> bool:
    """Check whether an item with the given key exists."""
    self._rebuild_index()
    return name in self._index

__delitem__(name)

Delete an item by key or raise KeyError if missing.

Source code in src/easydiffraction/core/collection.py
78
79
80
81
82
83
84
85
86
def __delitem__(self, name: str) -> None:
    """Delete an item by key or raise ``KeyError`` if missing."""
    for i, item in enumerate(self._items):
        if self._key_for(item) == name:
            object.__setattr__(item, '_parent', None)  # Unlink the parent before removal
            del self._items[i]
            self._rebuild_index()
            return
    raise KeyError(name)

__getitem__(key)

Return an item by name or positional index.

Parameters:

Name Type Description Default
key str | int

Identity key (str) or zero-based positional index (int).

required

Returns:

Type Description
GuardedBase

The item matching the given key or index.

Raises:

Type Description
TypeError

If key is neither str nor int.

Source code in src/easydiffraction/core/collection.py
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def __getitem__(self, key: str | int) -> GuardedBase:
    """
    Return an item by name or positional index.

    Parameters
    ----------
    key : str | int
        Identity key (str) or zero-based positional index (int).

    Returns
    -------
    GuardedBase
        The item matching the given key or index.

    Raises
    ------
    TypeError
        If *key* is neither ``str`` nor ``int``.
    """
    if isinstance(key, int):
        return self._items[key]
    if isinstance(key, str):
        try:
            return self._index[key]
        except KeyError:
            self._rebuild_index()
            return self._index[key]
    raise TypeError(f'Collection indices must be str or int, not {type(key).__name__}')

__iter__()

Iterate over items in insertion order.

Source code in src/easydiffraction/core/collection.py
93
94
95
def __iter__(self) -> Iterator[GuardedBase]:
    """Iterate over items in insertion order."""
    return iter(self._items)

__len__()

Return the number of items in the collection.

Source code in src/easydiffraction/core/collection.py
97
98
99
def __len__(self) -> int:
    """Return the number of items in the collection."""
    return len(self._items)

__setitem__(name, item)

Insert or replace an item under the given identity key.

Source code in src/easydiffraction/core/collection.py
65
66
67
68
69
70
71
72
73
74
75
76
def __setitem__(self, name: str, item: GuardedBase) -> None:
    """Insert or replace an item under the given identity key."""
    # Check if item with same key exists; if so, replace it
    for i, existing_item in enumerate(self._items):
        if self._key_for(existing_item) == name:
            self._items[i] = item
            self._rebuild_index()
            return
    # Otherwise append new item
    item._parent = self  # Explicitly set the parent for the item
    self._items.append(item)
    self._rebuild_index()

help()

Print a summary of public attributes and contained items.

Source code in src/easydiffraction/core/collection.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def help(self) -> None:
    """Print a summary of public attributes and contained items."""
    super().help()

    from easydiffraction.utils.logging import console
    from easydiffraction.utils.utils import render_table

    if self._items:
        console.paragraph(f'Items ({len(self._items)})')
        rows = []
        for i, item in enumerate(self._items, 1):
            key = self._key_for(item)
            rows.append([str(i), str(key), f"['{key}']"])
        render_table(
            columns_headers=['#', 'Name', 'Access'],
            columns_alignment=['right', 'left', 'left'],
            columns_data=rows,
        )
    else:
        console.paragraph('Items')
        console.print('(empty)')

items()

Yield (key, item) pairs in insertion order.

Source code in src/easydiffraction/core/collection.py
145
146
147
def items(self) -> Generator[tuple[str | None, GuardedBase], None, None]:
    """Yield ``(key, item)`` pairs in insertion order."""
    return ((self._key_for(item), item) for item in self._items)

keys()

Yield keys for all items in insertion order.

Source code in src/easydiffraction/core/collection.py
137
138
139
def keys(self) -> Generator[str | None, None, None]:
    """Yield keys for all items in insertion order."""
    return (self._key_for(item) for item in self._items)

names property

List of all item keys in the collection.

remove(name)

Remove an item by its key.

Parameters:

Name Type Description Default
name str

Identity key of the item to remove.

required

Raises:

Type Description
KeyError

If no item with the given key exists.

Source code in src/easydiffraction/core/collection.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def remove(self, name: str) -> None:
    """
    Remove an item by its key.

    Parameters
    ----------
    name : str
        Identity key of the item to remove.

    Raises
    ------
    KeyError
        If no item with the given key exists.
    """
    try:
        del self[name]
    except KeyError:
        raise

values()

Yield items in insertion order.

Source code in src/easydiffraction/core/collection.py
141
142
143
def values(self) -> Generator[GuardedBase, None, None]:
    """Yield items in insertion order."""
    return (item for item in self._items)

datablock

DatablockCollection

Bases: CollectionBase

Collection of top-level datablocks (e.g. Structures, Experiments).

Each item is a DatablockItem.

Subclasses provide explicit add_from_* convenience methods that delegate to the corresponding factory classmethods, then call :meth:add with the resulting item.

Source code in src/easydiffraction/core/datablock.py
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 DatablockCollection(CollectionBase):
    """
    Collection of top-level datablocks (e.g. Structures, Experiments).

    Each item is a DatablockItem.

    Subclasses provide explicit ``add_from_*`` convenience methods that
    delegate to the corresponding factory classmethods, then call
    :meth:`add` with the resulting item.
    """

    def _key_for(self, item: object) -> str | None:
        """Return the datablock-level identity key for *item*."""
        return item._identity.datablock_entry_name

    def add(self, item: object) -> None:
        """
        Add a pre-built item to the collection.

        Parameters
        ----------
        item : object
            A ``DatablockItem`` instance (e.g. a ``Structure`` or
            ``ExperimentBase`` subclass).
        """
        self[item._identity.datablock_entry_name] = item

    def __str__(self) -> str:
        """Human-readable representation of this component."""
        name = self._log_name
        size = len(self)
        return f'<{name} collection ({size} items)>'

    @property
    def unique_name(self) -> str | None:
        """Return None; collections have no unique name."""
        return None

    @property
    def parameters(self) -> list:
        """All parameters from all datablocks in this collection."""
        params = []
        for db in self._items:
            params.extend(db.parameters)
        return params

    @property
    def fittable_parameters(self) -> list:
        """All non-constrained Parameters in this collection."""
        return [p for p in self.parameters if isinstance(p, Parameter) and not p.constrained]

    @property
    def free_parameters(self) -> list:
        """All fittable parameters that are currently marked as free."""
        return [p for p in self.fittable_parameters if p.free]

    @property
    def as_cif(self) -> str:
        """Return CIF representation of this object."""
        from easydiffraction.io.cif.serialize import datablock_collection_to_cif

        return datablock_collection_to_cif(self)

__str__()

Human-readable representation of this component.

Source code in src/easydiffraction/core/datablock.py
147
148
149
150
151
def __str__(self) -> str:
    """Human-readable representation of this component."""
    name = self._log_name
    size = len(self)
    return f'<{name} collection ({size} items)>'

add(item)

Add a pre-built item to the collection.

Parameters:

Name Type Description Default
item object

A DatablockItem instance (e.g. a Structure or ExperimentBase subclass).

required
Source code in src/easydiffraction/core/datablock.py
135
136
137
138
139
140
141
142
143
144
145
def add(self, item: object) -> None:
    """
    Add a pre-built item to the collection.

    Parameters
    ----------
    item : object
        A ``DatablockItem`` instance (e.g. a ``Structure`` or
        ``ExperimentBase`` subclass).
    """
    self[item._identity.datablock_entry_name] = item

as_cif property

Return CIF representation of this object.

fittable_parameters property

All non-constrained Parameters in this collection.

free_parameters property

All fittable parameters that are currently marked as free.

parameters property

All parameters from all datablocks in this collection.

unique_name property

Return None; collections have no unique name.

DatablockItem

Bases: GuardedBase

Base class for items in a datablock collection.

Source code in src/easydiffraction/core/datablock.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
class DatablockItem(GuardedBase):
    """Base class for items in a datablock collection."""

    def __init__(self) -> None:
        super().__init__()
        self._need_categories_update = True

    def __str__(self) -> str:
        """Human-readable representation of this component."""
        name = self.unique_name
        cls = type(self).__name__
        categories = '\n'.join(f'  - {c}' for c in self.categories)
        return f"{cls} datablock '{name}':\n{categories}"

    def __repr__(self) -> str:
        """Developer-oriented representation of this component."""
        name = self.unique_name
        cls = type(self).__name__
        num_categories = len(self.categories)
        return f'<{cls} datablock "{name}" ({num_categories} categories)>'

    def _update_categories(
        self,
        called_by_minimizer: bool = False,
    ) -> None:
        # TODO: Make abstract method and implement in subclasses.
        # This should call apply_symmetry and apply_constraints in the
        # case of structures. In the case of experiments, it should
        # run calculations to update the "data" categories.
        # Any parameter change should set _need_categories_update to
        # True.
        # Calling as_cif or data getter should first check this flag
        # and call this method if True.
        # Should this be also called when parameters are accessed? E.g.
        # if one change background coefficients, then access the
        # background points in the data category?
        #
        # Dirty-flag guard: skip if no parameter has changed since the
        # last update.  Minimisers use _set_value_from_minimizer()
        # which bypasses validation but still sets this flag.
        # During fitting the guard is bypassed because experiment
        # calculations depend on structure parameters owned by a
        # different DatablockItem whose flag changes are invisible here.
        if not called_by_minimizer and not self._need_categories_update:
            return

        for category in self.categories:
            category._update(called_by_minimizer=called_by_minimizer)

        self._need_categories_update = False

    @property
    def unique_name(self) -> str | None:
        """Unique name of this datablock item (from identity)."""
        return self._identity.datablock_entry_name

    @property
    def categories(self) -> list:
        """All category objects in this datablock by priority."""
        cats = [
            v for v in vars(self).values() if isinstance(v, (CategoryItem, CategoryCollection))
        ]
        # Sort by _update_priority (lower values first)
        return sorted(cats, key=lambda c: type(c)._update_priority)

    @property
    def parameters(self) -> list:
        """All parameters from all categories in this datablock."""
        params = []
        for v in self.categories:
            params.extend(v.parameters)
        return params

    @property
    def as_cif(self) -> str:
        """Return CIF representation of this object."""
        from easydiffraction.io.cif.serialize import datablock_item_to_cif

        self._update_categories()
        return datablock_item_to_cif(self)

    def help(self) -> None:
        """Print a summary of public attributes and categories."""
        super().help()

        from easydiffraction.utils.logging import console
        from easydiffraction.utils.utils import render_table

        cats = self.categories
        if cats:
            console.paragraph('Categories')
            rows = []
            for c in cats:
                code = c._identity.category_code or type(c).__name__
                type_name = type(c).__name__
                num_params = len(c.parameters)
                rows.append([code, type_name, str(num_params)])
            render_table(
                columns_headers=['Category', 'Type', '# Parameters'],
                columns_alignment=['left', 'left', 'right'],
                columns_data=rows,
            )

__repr__()

Developer-oriented representation of this component.

Source code in src/easydiffraction/core/datablock.py
27
28
29
30
31
32
def __repr__(self) -> str:
    """Developer-oriented representation of this component."""
    name = self.unique_name
    cls = type(self).__name__
    num_categories = len(self.categories)
    return f'<{cls} datablock "{name}" ({num_categories} categories)>'

__str__()

Human-readable representation of this component.

Source code in src/easydiffraction/core/datablock.py
20
21
22
23
24
25
def __str__(self) -> str:
    """Human-readable representation of this component."""
    name = self.unique_name
    cls = type(self).__name__
    categories = '\n'.join(f'  - {c}' for c in self.categories)
    return f"{cls} datablock '{name}':\n{categories}"

as_cif property

Return CIF representation of this object.

categories property

All category objects in this datablock by priority.

help()

Print a summary of public attributes and categories.

Source code in src/easydiffraction/core/datablock.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def help(self) -> None:
    """Print a summary of public attributes and categories."""
    super().help()

    from easydiffraction.utils.logging import console
    from easydiffraction.utils.utils import render_table

    cats = self.categories
    if cats:
        console.paragraph('Categories')
        rows = []
        for c in cats:
            code = c._identity.category_code or type(c).__name__
            type_name = type(c).__name__
            num_params = len(c.parameters)
            rows.append([code, type_name, str(num_params)])
        render_table(
            columns_headers=['Category', 'Type', '# Parameters'],
            columns_alignment=['left', 'left', 'right'],
            columns_data=rows,
        )

parameters property

All parameters from all categories in this datablock.

unique_name property

Unique name of this datablock item (from identity).

diagnostic

Diagnostics helpers for logging validation messages.

This module centralizes human-friendly error and debug logs for attribute validation and configuration checks.

Diagnostics

Centralized logger for attribute errors and validation hints.

Source code in src/easydiffraction/core/diagnostic.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
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
class Diagnostics:
    """Centralized logger for attribute errors and validation hints."""

    # ==============================================================
    # Configuration / definition diagnostics
    # ==============================================================

    @staticmethod
    def type_override_error(cls_name: str, expected: object, got: object) -> None:
        """
        Report an invalid DataTypes override.

        Used when descriptor and AttributeSpec types conflict.
        """
        expected_label = str(expected)
        got_label = str(got)
        msg = (
            f'Invalid type override in <{cls_name}>. '
            f'Descriptor enforces `{expected_label}`, '
            f'but AttributeSpec defines `{got_label}`.'
        )
        Diagnostics._log_error(msg, exc_type=TypeError)

    # ==============================================================
    # Attribute diagnostics
    # ==============================================================

    @staticmethod
    def readonly_error(
        name: str,
        key: str | None = None,
    ) -> None:
        """Log an attempt to change a read-only attribute."""
        Diagnostics._log_error(
            f"Cannot modify read-only attribute '{key}' of <{name}>.",
            exc_type=AttributeError,
        )

    @staticmethod
    def attr_error(
        name: str,
        key: str,
        allowed: set[str],
        label: str = 'Allowed',
    ) -> None:
        """Log unknown attribute access and suggest closest key."""
        suggestion = Diagnostics._build_suggestion(key, allowed)
        # Use consistent (label) logic for allowed
        hint = suggestion or Diagnostics._build_allowed(allowed, label=label)
        Diagnostics._log_error(
            f"Unknown attribute '{key}' of <{name}>.{hint}",
            exc_type=AttributeError,
        )

    # ==============================================================
    # Validation diagnostics
    # ==============================================================

    @staticmethod
    def type_mismatch(
        name: str,
        value: object,
        expected_type: object,
        current: object = None,
        default: object = None,
    ) -> None:
        """Log a type mismatch and keep current or default value."""
        got_type = type(value).__name__
        msg = (
            f'Type mismatch for <{name}>. '
            f'Expected `{expected_type}`, got `{got_type}` ({value!r}).'
        )
        Diagnostics._log_error_with_fallback(
            msg, current=current, default=default, exc_type=TypeError
        )

    @staticmethod
    def range_mismatch(
        name: str,
        value: object,
        ge: float,
        le: float,
        current: object = None,
        default: object = None,
    ) -> None:
        """Log range violation for a numeric value."""
        msg = f'Value mismatch for <{name}>. Provided {value!r} outside [{ge}, {le}].'
        Diagnostics._log_error_with_fallback(
            msg, current=current, default=default, exc_type=TypeError
        )

    @staticmethod
    def choice_mismatch(
        name: str,
        value: object,
        allowed: object,
        current: object = None,
        default: object = None,
    ) -> None:
        """Log an invalid choice against allowed values."""
        msg = f'Value mismatch for <{name}>. Provided {value!r} is unknown.'
        if allowed is not None:
            msg += Diagnostics._build_allowed(allowed, label='Allowed values')
        Diagnostics._log_error_with_fallback(
            msg, current=current, default=default, exc_type=TypeError
        )

    @staticmethod
    def regex_mismatch(
        name: str,
        value: object,
        pattern: str,
        current: object = None,
        default: object = None,
    ) -> None:
        """Log a regex mismatch with the expected pattern."""
        msg = (
            f"Value mismatch for <{name}>. Provided {value!r} does not match pattern '{pattern}'."
        )
        Diagnostics._log_error_with_fallback(
            msg, current=current, default=default, exc_type=TypeError
        )

    @staticmethod
    def no_value(name: str, default: object) -> None:
        """Log that default will be used due to missing value."""
        Diagnostics._log_debug(f'No value provided for <{name}>. Using default {default!r}.')

    @staticmethod
    def none_value(name: str) -> None:
        """Log explicit None provided by a user."""
        Diagnostics._log_debug(f'Using `None` explicitly provided for <{name}>.')

    @staticmethod
    def none_value_skip_range(name: str) -> None:
        """Log that range validation is skipped due to None."""
        Diagnostics._log_debug(
            f'Skipping range validation as `None` is explicitly provided for <{name}>.'
        )

    @staticmethod
    def validated(name: str, value: object, stage: str | None = None) -> None:
        """Log that a value passed a validation stage."""
        stage_info = f' {stage}' if stage else ''
        Diagnostics._log_debug(f'Value {value!r} for <{name}> passed{stage_info} validation.')

    # ==============================================================
    # Helper log methods
    # ==============================================================

    @staticmethod
    def _log_error(msg: str, exc_type: type[Exception] = Exception) -> None:
        """Emit an error-level message via shared logger."""
        log.error(msg, exc_type=exc_type)

    @staticmethod
    def _log_error_with_fallback(
        msg: str,
        current: object = None,
        default: object = None,
        exc_type: type[Exception] = Exception,
    ) -> None:
        """Emit an error message and mention kept or default value."""
        if current is not None:
            msg += f' Keeping current {current!r}.'
        else:
            msg += f' Using default {default!r}.'
        log.error(msg, exc_type=exc_type)

    @staticmethod
    def _log_debug(msg: str) -> None:
        """Emit a debug-level message via shared logger."""
        log.debug(msg)

    # ==============================================================
    # Suggestion and allowed value helpers
    # ==============================================================

    @staticmethod
    def _suggest(key: str, allowed: set[str]) -> str | None:
        """Suggest closest allowed key using string similarity."""
        if not allowed:
            return None
        # Return the allowed key with smallest Levenshtein distance
        matches = difflib.get_close_matches(key, allowed, n=1)
        return matches[0] if matches else None

    @staticmethod
    def _build_suggestion(key: str, allowed: set[str]) -> str:
        s = Diagnostics._suggest(key, allowed)
        return f" Did you mean '{s}'?" if s else ''

    @staticmethod
    def _build_allowed(allowed: object, label: str = 'Allowed attributes') -> str:
        # allowed may be a set, list, or other iterable
        if allowed:
            allowed_list = list(allowed)
            if len(allowed_list) <= 10:
                s = ', '.join(map(repr, sorted(allowed_list)))
                return f' {label}: {s}.'
            else:
                return f' ({len(allowed_list)} {label.lower()} not listed here).'
        return ''

attr_error(name, key, allowed, label='Allowed') staticmethod

Log unknown attribute access and suggest closest key.

Source code in src/easydiffraction/core/diagnostic.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@staticmethod
def attr_error(
    name: str,
    key: str,
    allowed: set[str],
    label: str = 'Allowed',
) -> None:
    """Log unknown attribute access and suggest closest key."""
    suggestion = Diagnostics._build_suggestion(key, allowed)
    # Use consistent (label) logic for allowed
    hint = suggestion or Diagnostics._build_allowed(allowed, label=label)
    Diagnostics._log_error(
        f"Unknown attribute '{key}' of <{name}>.{hint}",
        exc_type=AttributeError,
    )

choice_mismatch(name, value, allowed, current=None, default=None) staticmethod

Log an invalid choice against allowed values.

Source code in src/easydiffraction/core/diagnostic.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
@staticmethod
def choice_mismatch(
    name: str,
    value: object,
    allowed: object,
    current: object = None,
    default: object = None,
) -> None:
    """Log an invalid choice against allowed values."""
    msg = f'Value mismatch for <{name}>. Provided {value!r} is unknown.'
    if allowed is not None:
        msg += Diagnostics._build_allowed(allowed, label='Allowed values')
    Diagnostics._log_error_with_fallback(
        msg, current=current, default=default, exc_type=TypeError
    )

no_value(name, default) staticmethod

Log that default will be used due to missing value.

Source code in src/easydiffraction/core/diagnostic.py
138
139
140
141
@staticmethod
def no_value(name: str, default: object) -> None:
    """Log that default will be used due to missing value."""
    Diagnostics._log_debug(f'No value provided for <{name}>. Using default {default!r}.')

none_value(name) staticmethod

Log explicit None provided by a user.

Source code in src/easydiffraction/core/diagnostic.py
143
144
145
146
@staticmethod
def none_value(name: str) -> None:
    """Log explicit None provided by a user."""
    Diagnostics._log_debug(f'Using `None` explicitly provided for <{name}>.')

none_value_skip_range(name) staticmethod

Log that range validation is skipped due to None.

Source code in src/easydiffraction/core/diagnostic.py
148
149
150
151
152
153
@staticmethod
def none_value_skip_range(name: str) -> None:
    """Log that range validation is skipped due to None."""
    Diagnostics._log_debug(
        f'Skipping range validation as `None` is explicitly provided for <{name}>.'
    )

range_mismatch(name, value, ge, le, current=None, default=None) staticmethod

Log range violation for a numeric value.

Source code in src/easydiffraction/core/diagnostic.py
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
@staticmethod
def range_mismatch(
    name: str,
    value: object,
    ge: float,
    le: float,
    current: object = None,
    default: object = None,
) -> None:
    """Log range violation for a numeric value."""
    msg = f'Value mismatch for <{name}>. Provided {value!r} outside [{ge}, {le}].'
    Diagnostics._log_error_with_fallback(
        msg, current=current, default=default, exc_type=TypeError
    )

readonly_error(name, key=None) staticmethod

Log an attempt to change a read-only attribute.

Source code in src/easydiffraction/core/diagnostic.py
42
43
44
45
46
47
48
49
50
51
@staticmethod
def readonly_error(
    name: str,
    key: str | None = None,
) -> None:
    """Log an attempt to change a read-only attribute."""
    Diagnostics._log_error(
        f"Cannot modify read-only attribute '{key}' of <{name}>.",
        exc_type=AttributeError,
    )

regex_mismatch(name, value, pattern, current=None, default=None) staticmethod

Log a regex mismatch with the expected pattern.

Source code in src/easydiffraction/core/diagnostic.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
@staticmethod
def regex_mismatch(
    name: str,
    value: object,
    pattern: str,
    current: object = None,
    default: object = None,
) -> None:
    """Log a regex mismatch with the expected pattern."""
    msg = (
        f"Value mismatch for <{name}>. Provided {value!r} does not match pattern '{pattern}'."
    )
    Diagnostics._log_error_with_fallback(
        msg, current=current, default=default, exc_type=TypeError
    )

type_mismatch(name, value, expected_type, current=None, default=None) staticmethod

Log a type mismatch and keep current or default value.

Source code in src/easydiffraction/core/diagnostic.py
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@staticmethod
def type_mismatch(
    name: str,
    value: object,
    expected_type: object,
    current: object = None,
    default: object = None,
) -> None:
    """Log a type mismatch and keep current or default value."""
    got_type = type(value).__name__
    msg = (
        f'Type mismatch for <{name}>. '
        f'Expected `{expected_type}`, got `{got_type}` ({value!r}).'
    )
    Diagnostics._log_error_with_fallback(
        msg, current=current, default=default, exc_type=TypeError
    )

type_override_error(cls_name, expected, got) staticmethod

Report an invalid DataTypes override.

Used when descriptor and AttributeSpec types conflict.

Source code in src/easydiffraction/core/diagnostic.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@staticmethod
def type_override_error(cls_name: str, expected: object, got: object) -> None:
    """
    Report an invalid DataTypes override.

    Used when descriptor and AttributeSpec types conflict.
    """
    expected_label = str(expected)
    got_label = str(got)
    msg = (
        f'Invalid type override in <{cls_name}>. '
        f'Descriptor enforces `{expected_label}`, '
        f'but AttributeSpec defines `{got_label}`.'
    )
    Diagnostics._log_error(msg, exc_type=TypeError)

validated(name, value, stage=None) staticmethod

Log that a value passed a validation stage.

Source code in src/easydiffraction/core/diagnostic.py
155
156
157
158
159
@staticmethod
def validated(name: str, value: object, stage: str | None = None) -> None:
    """Log that a value passed a validation stage."""
    stage_info = f' {stage}' if stage else ''
    Diagnostics._log_debug(f'Value {value!r} for <{name}> passed{stage_info} validation.')

factory

Base factory with registration, lookup, and context-dependent defaults.

Concrete factories inherit from FactoryBase and only need to define _default_rules.

FactoryBase

Shared base for all factories.

Subclasses must set:

  • _default_rules -- mapping of frozenset conditions to tag strings. Use frozenset(): 'tag' for a universal default.

The __init_subclass__ hook ensures every subclass gets its own independent _registry list.

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

    Subclasses must set:

    * ``_default_rules`` -- mapping of ``frozenset`` conditions to tag
    strings.  Use ``frozenset(): 'tag'`` for a universal default.

    The ``__init_subclass__`` hook ensures every subclass gets its own
    independent ``_registry`` list.
    """

    _registry: List[Type] = []
    _default_rules: Dict[FrozenSet[Tuple[str, Any]], str] = {}

    def __init_subclass__(cls, **kwargs: object) -> None:
        """Give each subclass its own independent registry and rules."""
        super().__init_subclass__(**kwargs)
        cls._registry = []
        if '_default_rules' not in cls.__dict__:
            cls._default_rules = {}

    # ------------------------------------------------------------------
    # Registration
    # ------------------------------------------------------------------

    @classmethod
    def register(cls, klass: type) -> type:
        """
        Class decorator to register a concrete class.

        Usage::

        @SomeFactory.register class MyClass(SomeBase):     type_info =
        TypeInfo(...)

        Returns the class unmodified.
        """
        cls._registry.append(klass)
        return klass

    # ------------------------------------------------------------------
    # Supported-map helpers
    # ------------------------------------------------------------------

    @classmethod
    def _supported_map(cls) -> Dict[str, Type]:
        """Build ``{tag: class}`` from all registered classes."""
        return {klass.type_info.tag: klass for klass in cls._registry}

    @classmethod
    def supported_tags(cls) -> List[str]:
        """Return list of all supported tags."""
        return list(cls._supported_map().keys())

    # ------------------------------------------------------------------
    # Default resolution
    # ------------------------------------------------------------------

    @classmethod
    def default_tag(cls, **conditions: object) -> str:
        """
        Resolve the default tag for a given experimental context.

        Uses *largest-subset matching*: the rule whose key is the
        biggest subset of the given conditions wins. A rule with an
        empty key (``frozenset()``) acts as a universal fallback.

        Parameters
        ----------
        **conditions : object
            Experimental-axis values, e.g.
            ``scattering_type=ScatteringTypeEnum.BRAGG``.

        Returns
        -------
        str
            The resolved default tag string.

        Raises
        ------
        ValueError
            If no rule matches the given conditions.
        """
        condition_set = frozenset(conditions.items())
        best_match_tag: str | None = None
        best_match_size = -1

        for rule_key, rule_tag in cls._default_rules.items():
            if rule_key <= condition_set and len(rule_key) > best_match_size:
                best_match_tag = rule_tag
                best_match_size = len(rule_key)

        if best_match_tag is None:
            raise ValueError(
                f'No default rule matches conditions {dict(conditions)}. '
                f'Available rules: {cls._default_rules}'
            )
        return best_match_tag

    # ------------------------------------------------------------------
    # Creation
    # ------------------------------------------------------------------

    @classmethod
    def create(cls, tag: str, **kwargs: object) -> object:
        """
        Instantiate a registered class by *tag*.

        Parameters
        ----------
        tag : str
            ``type_info.tag`` value.
        **kwargs : object
            Forwarded to the class constructor.

        Returns
        -------
        object
            A new instance of the registered class.

        Raises
        ------
        ValueError
            If *tag* is not in the registry.
        """
        supported = cls._supported_map()
        if tag not in supported:
            raise ValueError(f"Unsupported type: '{tag}'. Supported: {list(supported.keys())}")
        return supported[tag](**kwargs)

    @classmethod
    def create_default_for(cls, **conditions: object) -> object:
        """
        Instantiate the default class for a given context.

        Combines ``default_tag(**conditions)`` with ``create(tag)``.

        Parameters
        ----------
        **conditions : object
            Experimental-axis values.

        Returns
        -------
        object
            A new instance of the default class.
        """
        tag = cls.default_tag(**conditions)
        return cls.create(tag)

    # ------------------------------------------------------------------
    # Querying
    # ------------------------------------------------------------------

    @classmethod
    def supported_for(
        cls,
        *,
        calculator: object = None,
        sample_form: object = None,
        scattering_type: object = None,
        beam_mode: object = None,
        radiation_probe: object = None,
    ) -> List[Type]:
        """
        Return classes matching conditions and/or calculator.

        Parameters
        ----------
        calculator : object, default=None
            Optional ``CalculatorEnum`` value.
        sample_form : object, default=None
            Optional ``SampleFormEnum`` value.
        scattering_type : object, default=None
            Optional ``ScatteringTypeEnum`` value.
        beam_mode : object, default=None
            Optional ``BeamModeEnum`` value.
        radiation_probe : object, default=None
            Optional ``RadiationProbeEnum`` value.

        Returns
        -------
        List[Type]
            Classes matching the given conditions.
        """
        result = []
        for klass in cls._supported_map().values():
            compat = getattr(klass, 'compatibility', None)
            if compat and not compat.supports(
                sample_form=sample_form,
                scattering_type=scattering_type,
                beam_mode=beam_mode,
                radiation_probe=radiation_probe,
            ):
                continue
            calc_support = getattr(klass, 'calculator_support', None)
            if calculator and calc_support and not calc_support.supports(calculator):
                continue
            result.append(klass)
        return result

    # ------------------------------------------------------------------
    # Display
    # ------------------------------------------------------------------

    @classmethod
    def show_supported(
        cls,
        *,
        calculator: object = None,
        sample_form: object = None,
        scattering_type: object = None,
        beam_mode: object = None,
        radiation_probe: object = None,
    ) -> None:
        """
        Pretty-print a table of supported types.

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

__init_subclass__(**kwargs)

Give each subclass its own independent registry and rules.

Source code in src/easydiffraction/core/factory.py
39
40
41
42
43
44
def __init_subclass__(cls, **kwargs: object) -> None:
    """Give each subclass its own independent registry and rules."""
    super().__init_subclass__(**kwargs)
    cls._registry = []
    if '_default_rules' not in cls.__dict__:
        cls._default_rules = {}

create(tag, **kwargs) classmethod

Instantiate a registered class by tag.

Parameters:

Name Type Description Default
tag str

type_info.tag value.

required
**kwargs object

Forwarded to the class constructor.

{}

Returns:

Type Description
object

A new instance of the registered class.

Raises:

Type Description
ValueError

If tag is not in the registry.

Source code in src/easydiffraction/core/factory.py
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
@classmethod
def create(cls, tag: str, **kwargs: object) -> object:
    """
    Instantiate a registered class by *tag*.

    Parameters
    ----------
    tag : str
        ``type_info.tag`` value.
    **kwargs : object
        Forwarded to the class constructor.

    Returns
    -------
    object
        A new instance of the registered class.

    Raises
    ------
    ValueError
        If *tag* is not in the registry.
    """
    supported = cls._supported_map()
    if tag not in supported:
        raise ValueError(f"Unsupported type: '{tag}'. Supported: {list(supported.keys())}")
    return supported[tag](**kwargs)

create_default_for(**conditions) classmethod

Instantiate the default class for a given context.

Combines default_tag(**conditions) with create(tag).

Parameters:

Name Type Description Default
**conditions object

Experimental-axis values.

{}

Returns:

Type Description
object

A new instance of the default class.

Source code in src/easydiffraction/core/factory.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
@classmethod
def create_default_for(cls, **conditions: object) -> object:
    """
    Instantiate the default class for a given context.

    Combines ``default_tag(**conditions)`` with ``create(tag)``.

    Parameters
    ----------
    **conditions : object
        Experimental-axis values.

    Returns
    -------
    object
        A new instance of the default class.
    """
    tag = cls.default_tag(**conditions)
    return cls.create(tag)

default_tag(**conditions) classmethod

Resolve the default tag for a given experimental context.

Uses largest-subset matching: the rule whose key is the biggest subset of the given conditions wins. A rule with an empty key (frozenset()) acts as a universal fallback.

Parameters:

Name Type Description Default
**conditions object

Experimental-axis values, e.g. scattering_type=ScatteringTypeEnum.BRAGG.

{}

Returns:

Type Description
str

The resolved default tag string.

Raises:

Type Description
ValueError

If no rule matches the given conditions.

Source code in src/easydiffraction/core/factory.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
119
120
121
122
@classmethod
def default_tag(cls, **conditions: object) -> str:
    """
    Resolve the default tag for a given experimental context.

    Uses *largest-subset matching*: the rule whose key is the
    biggest subset of the given conditions wins. A rule with an
    empty key (``frozenset()``) acts as a universal fallback.

    Parameters
    ----------
    **conditions : object
        Experimental-axis values, e.g.
        ``scattering_type=ScatteringTypeEnum.BRAGG``.

    Returns
    -------
    str
        The resolved default tag string.

    Raises
    ------
    ValueError
        If no rule matches the given conditions.
    """
    condition_set = frozenset(conditions.items())
    best_match_tag: str | None = None
    best_match_size = -1

    for rule_key, rule_tag in cls._default_rules.items():
        if rule_key <= condition_set and len(rule_key) > best_match_size:
            best_match_tag = rule_tag
            best_match_size = len(rule_key)

    if best_match_tag is None:
        raise ValueError(
            f'No default rule matches conditions {dict(conditions)}. '
            f'Available rules: {cls._default_rules}'
        )
    return best_match_tag

register(klass) classmethod

Class decorator to register a concrete class.

Usage::

@SomeFactory.register class MyClass(SomeBase): type_info = TypeInfo(...)

Returns the class unmodified.

Source code in src/easydiffraction/core/factory.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@classmethod
def register(cls, klass: type) -> type:
    """
    Class decorator to register a concrete class.

    Usage::

    @SomeFactory.register class MyClass(SomeBase):     type_info =
    TypeInfo(...)

    Returns the class unmodified.
    """
    cls._registry.append(klass)
    return klass

show_supported(*, calculator=None, sample_form=None, scattering_type=None, beam_mode=None, radiation_probe=None) classmethod

Pretty-print a table of supported types.

Parameters:

Name Type Description Default
calculator object

Optional CalculatorEnum filter.

None
sample_form object

Optional SampleFormEnum filter.

None
scattering_type object

Optional ScatteringTypeEnum filter.

None
beam_mode object

Optional BeamModeEnum filter.

None
radiation_probe object

Optional RadiationProbeEnum filter.

None
Source code in src/easydiffraction/core/factory.py
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
@classmethod
def show_supported(
    cls,
    *,
    calculator: object = None,
    sample_form: object = None,
    scattering_type: object = None,
    beam_mode: object = None,
    radiation_probe: object = None,
) -> None:
    """
    Pretty-print a table of supported types.

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

supported_for(*, calculator=None, sample_form=None, scattering_type=None, beam_mode=None, radiation_probe=None) classmethod

Return classes matching conditions and/or calculator.

Parameters:

Name Type Description Default
calculator object

Optional CalculatorEnum value.

None
sample_form object

Optional SampleFormEnum value.

None
scattering_type object

Optional ScatteringTypeEnum value.

None
beam_mode object

Optional BeamModeEnum value.

None
radiation_probe object

Optional RadiationProbeEnum value.

None

Returns:

Type Description
List[Type]

Classes matching the given conditions.

Source code in src/easydiffraction/core/factory.py
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
@classmethod
def supported_for(
    cls,
    *,
    calculator: object = None,
    sample_form: object = None,
    scattering_type: object = None,
    beam_mode: object = None,
    radiation_probe: object = None,
) -> List[Type]:
    """
    Return classes matching conditions and/or calculator.

    Parameters
    ----------
    calculator : object, default=None
        Optional ``CalculatorEnum`` value.
    sample_form : object, default=None
        Optional ``SampleFormEnum`` value.
    scattering_type : object, default=None
        Optional ``ScatteringTypeEnum`` value.
    beam_mode : object, default=None
        Optional ``BeamModeEnum`` value.
    radiation_probe : object, default=None
        Optional ``RadiationProbeEnum`` value.

    Returns
    -------
    List[Type]
        Classes matching the given conditions.
    """
    result = []
    for klass in cls._supported_map().values():
        compat = getattr(klass, 'compatibility', None)
        if compat and not compat.supports(
            sample_form=sample_form,
            scattering_type=scattering_type,
            beam_mode=beam_mode,
            radiation_probe=radiation_probe,
        ):
            continue
        calc_support = getattr(klass, 'calculator_support', None)
        if calculator and calc_support and not calc_support.supports(calculator):
            continue
        result.append(klass)
    return result

supported_tags() classmethod

Return list of all supported tags.

Source code in src/easydiffraction/core/factory.py
74
75
76
77
@classmethod
def supported_tags(cls) -> List[str]:
    """Return list of all supported tags."""
    return list(cls._supported_map().keys())

guard

GuardedBase

Bases: ABC

Base class enforcing controlled attribute access and linkage.

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

    _diagnoser = Diagnostics()

    def __init__(self) -> None:
        super().__init__()
        self._identity = Identity(owner=self)

    def __str__(self) -> str:
        """Return the string representation of this object."""
        return f'<{self.unique_name}>'

    def __repr__(self) -> str:
        """Return the developer representation of this object."""
        return self.__str__()

    def __getattr__(self, key: str) -> None:
        """Raise a descriptive error for unknown attribute access."""
        cls = type(self)
        allowed = cls._public_attrs()
        if key not in allowed:
            type(self)._diagnoser.attr_error(
                self._log_name,
                key,
                allowed,
                label='Allowed readable/writable',
            )

    def __setattr__(self, key: str, value: object) -> None:
        """Set an attribute with access-control diagnostics."""
        # Always allow private or special attributes without diagnostics
        if key.startswith('_'):
            object.__setattr__(self, key, value)
            # Also maintain parent linkage for nested objects
            if key != '_parent' and isinstance(value, GuardedBase):
                object.__setattr__(value, '_parent', self)
            return

        # Handle public attributes with diagnostics
        cls = type(self)
        # Prevent modification of read-only attributes
        if key in cls._public_readonly_attrs():
            cls._diagnoser.readonly_error(
                self._log_name,
                key,
            )
            return
        # Prevent assignment to unknown attributes
        # Show writable attributes only as allowed
        if key not in cls._public_attrs():
            allowed = cls._public_writable_attrs()
            cls._diagnoser.attr_error(
                self._log_name,
                key,
                allowed,
                label='Allowed writable',
            )
            return

        self._assign_attr(key, value)

    def _assign_attr(self, key: str, value: object) -> None:
        """Low-level assignment with parent linkage."""
        object.__setattr__(self, key, value)
        if key != '_parent' and isinstance(value, GuardedBase):
            object.__setattr__(value, '_parent', self)

    @classmethod
    def _iter_properties(cls) -> Generator[tuple[str, property], None, None]:
        """
        Iterate over all public properties in the class hierarchy.

        Yields
        ------
        tuple[str, property]
            Each (key, property) pair for public attributes.
        """
        for base in cls.mro():
            for key, attr in base.__dict__.items():
                if key.startswith('_') or not isinstance(attr, property):
                    continue
                yield key, attr

    @classmethod
    def _public_attrs(cls) -> set[str]:
        """All public properties (read-only + writable)."""
        return {key for key, _ in cls._iter_properties()}

    @classmethod
    def _public_readonly_attrs(cls) -> set[str]:
        """Public properties without a setter."""
        return {key for key, prop in cls._iter_properties() if prop.fset is None}

    @classmethod
    def _public_writable_attrs(cls) -> set[str]:
        """Public properties with a setter."""
        return {key for key, prop in cls._iter_properties() if prop.fset is not None}

    def _allowed_attrs(self, writable_only: bool = False) -> set[str]:
        cls = type(self)
        if writable_only:
            return cls._public_writable_attrs()
        return cls._public_attrs()

    @property
    def _log_name(self) -> str:
        return self.unique_name or type(self).__name__

    @property
    def unique_name(self) -> str:
        """Fallback unique name: the class name."""
        return type(self).__name__

    # @property
    # def identity(self):
    #    """Expose a limited read-only view of identity attributes."""
    #    return SimpleNamespace(
    #        datablock_entry_name=self._identity.datablock_entry_name,
    #        category_code=self._identity.category_code,
    #        category_entry_name=self._identity.category_entry_name,
    #    )

    @property
    @abstractmethod
    def parameters(self) -> list:
        """Return a list of parameters (implemented by subclasses)."""
        raise NotImplementedError

    @property
    @abstractmethod
    def as_cif(self) -> str:
        """Return CIF representation (implemented by subclasses)."""
        raise NotImplementedError

    @staticmethod
    def _first_sentence(docstring: str | None) -> str:
        """
        Extract the first paragraph from a docstring.

        Returns text before the first blank line, with continuation
        lines joined into a single string.
        """
        if not docstring:
            return ''
        first_para = docstring.strip().split('\n\n')[0]
        return ' '.join(line.strip() for line in first_para.splitlines())

    @classmethod
    def _iter_methods(cls) -> Generator[tuple[str, object], None, None]:
        """
        Iterate over public methods in the class hierarchy.

        Yields
        ------
        tuple[str, object]
            Each (name, function) pair.
        """
        seen: set = set()
        for base in cls.mro():
            for key, attr in base.__dict__.items():
                if key.startswith('_') or key in seen:
                    continue
                if isinstance(attr, property):
                    continue
                raw = attr
                if isinstance(raw, (staticmethod, classmethod)):
                    raw = raw.__func__
                if callable(raw):
                    seen.add(key)
                    yield key, raw

    def help(self) -> None:
        """Print a summary of public properties and methods."""
        from easydiffraction.utils.logging import console
        from easydiffraction.utils.utils import render_table

        cls = type(self)
        console.paragraph(f"Help for '{cls.__name__}'")

        # Deduplicate (MRO may yield the same name)
        seen: dict = {}
        for key, prop in cls._iter_properties():
            if key not in seen:
                seen[key] = prop

        prop_rows = []
        for i, key in enumerate(sorted(seen), 1):
            prop = seen[key]
            writable = '✓' if prop.fset else '✗'
            doc = self._first_sentence(prop.fget.__doc__ if prop.fget else None)
            prop_rows.append([str(i), key, writable, doc])

        if prop_rows:
            console.paragraph('Properties')
            render_table(
                columns_headers=['#', 'Name', 'Writable', 'Description'],
                columns_alignment=['right', 'left', 'center', 'left'],
                columns_data=prop_rows,
            )

        methods = dict(cls._iter_methods())
        method_rows = []
        for i, key in enumerate(sorted(methods), 1):
            doc = self._first_sentence(getattr(methods[key], '__doc__', None))
            method_rows.append([str(i), f'{key}()', doc])

        if method_rows:
            console.paragraph('Methods')
            render_table(
                columns_headers=['#', 'Name', 'Description'],
                columns_alignment=['right', 'left', 'left'],
                columns_data=method_rows,
            )

__getattr__(key)

Raise a descriptive error for unknown attribute access.

Source code in src/easydiffraction/core/guard.py
31
32
33
34
35
36
37
38
39
40
41
def __getattr__(self, key: str) -> None:
    """Raise a descriptive error for unknown attribute access."""
    cls = type(self)
    allowed = cls._public_attrs()
    if key not in allowed:
        type(self)._diagnoser.attr_error(
            self._log_name,
            key,
            allowed,
            label='Allowed readable/writable',
        )

__repr__()

Return the developer representation of this object.

Source code in src/easydiffraction/core/guard.py
27
28
29
def __repr__(self) -> str:
    """Return the developer representation of this object."""
    return self.__str__()

__setattr__(key, value)

Set an attribute with access-control diagnostics.

Source code in src/easydiffraction/core/guard.py
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
def __setattr__(self, key: str, value: object) -> None:
    """Set an attribute with access-control diagnostics."""
    # Always allow private or special attributes without diagnostics
    if key.startswith('_'):
        object.__setattr__(self, key, value)
        # Also maintain parent linkage for nested objects
        if key != '_parent' and isinstance(value, GuardedBase):
            object.__setattr__(value, '_parent', self)
        return

    # Handle public attributes with diagnostics
    cls = type(self)
    # Prevent modification of read-only attributes
    if key in cls._public_readonly_attrs():
        cls._diagnoser.readonly_error(
            self._log_name,
            key,
        )
        return
    # Prevent assignment to unknown attributes
    # Show writable attributes only as allowed
    if key not in cls._public_attrs():
        allowed = cls._public_writable_attrs()
        cls._diagnoser.attr_error(
            self._log_name,
            key,
            allowed,
            label='Allowed writable',
        )
        return

    self._assign_attr(key, value)

__str__()

Return the string representation of this object.

Source code in src/easydiffraction/core/guard.py
23
24
25
def __str__(self) -> str:
    """Return the string representation of this object."""
    return f'<{self.unique_name}>'

as_cif abstractmethod property

Return CIF representation (implemented by subclasses).

help()

Print a summary of public properties and methods.

Source code in src/easydiffraction/core/guard.py
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
def help(self) -> None:
    """Print a summary of public properties and methods."""
    from easydiffraction.utils.logging import console
    from easydiffraction.utils.utils import render_table

    cls = type(self)
    console.paragraph(f"Help for '{cls.__name__}'")

    # Deduplicate (MRO may yield the same name)
    seen: dict = {}
    for key, prop in cls._iter_properties():
        if key not in seen:
            seen[key] = prop

    prop_rows = []
    for i, key in enumerate(sorted(seen), 1):
        prop = seen[key]
        writable = '✓' if prop.fset else '✗'
        doc = self._first_sentence(prop.fget.__doc__ if prop.fget else None)
        prop_rows.append([str(i), key, writable, doc])

    if prop_rows:
        console.paragraph('Properties')
        render_table(
            columns_headers=['#', 'Name', 'Writable', 'Description'],
            columns_alignment=['right', 'left', 'center', 'left'],
            columns_data=prop_rows,
        )

    methods = dict(cls._iter_methods())
    method_rows = []
    for i, key in enumerate(sorted(methods), 1):
        doc = self._first_sentence(getattr(methods[key], '__doc__', None))
        method_rows.append([str(i), f'{key}()', doc])

    if method_rows:
        console.paragraph('Methods')
        render_table(
            columns_headers=['#', 'Name', 'Description'],
            columns_alignment=['right', 'left', 'left'],
            columns_data=method_rows,
        )

parameters abstractmethod property

Return a list of parameters (implemented by subclasses).

unique_name property

Fallback unique name: the class name.

identity

Identity helpers to build CIF-like hierarchical names.

Used by containers and items to expose datablock/category/entry names without tight coupling.

Identity

Resolve datablock/category/entry relationships lazily.

Source code in src/easydiffraction/core/identity.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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 Identity:
    """Resolve datablock/category/entry relationships lazily."""

    def __init__(
        self,
        *,
        owner: object,
        datablock_entry: Callable | None = None,
        category_code: str | None = None,
        category_entry: Callable | None = None,
    ) -> None:
        self._owner = owner
        self._datablock_entry = datablock_entry
        self._category_code = category_code
        self._category_entry = category_entry

    def _resolve_up(self, attr: str, visited: set[int] | None = None) -> str | None:
        """Resolve attribute by walking up parent chain safely."""
        if visited is None:
            visited = set()
        if id(self) in visited:
            return None
        visited.add(id(self))

        # Direct callable or value on self
        value = getattr(self, f'_{attr}', None)
        if callable(value):
            return value()
        if isinstance(value, str):
            return value

        # Climb to parent if available
        parent = getattr(self._owner, '__dict__', {}).get('_parent')
        if parent and hasattr(parent, '_identity'):
            return parent._identity._resolve_up(attr, visited)
        return None

    @property
    def datablock_entry_name(self) -> str | None:
        """Datablock entry name or None if not set."""
        return self._resolve_up('datablock_entry')

    @datablock_entry_name.setter
    def datablock_entry_name(self, func: callable) -> None:
        """Set callable returning datablock entry name."""
        self._datablock_entry = func

    @property
    def category_code(self) -> str | None:
        """Category code like 'atom_site' or 'background'."""
        return self._resolve_up('category_code')

    @category_code.setter
    def category_code(self, value: str) -> None:
        """Set category code value."""
        self._category_code = value

    @property
    def category_entry_name(self) -> str | None:
        """Category entry name or None if not set."""
        return self._resolve_up('category_entry')

    @category_entry_name.setter
    def category_entry_name(self, func: callable) -> None:
        """Set callable returning category entry name."""
        self._category_entry = func

category_code property writable

Category code like 'atom_site' or 'background'.

category_entry_name property writable

Category entry name or None if not set.

datablock_entry_name property writable

Datablock entry name or None if not set.

metadata

Metadata dataclasses for factory-created classes.

Three frozen dataclasses describe a concrete class:

  • TypeInfo — stable tag and human-readable description. - Compatibility — experimental conditions (multiple fields). - CalculatorSupport — which calculation engines can handle it.

CalculatorSupport dataclass

Which calculation engines can handle this class.

Attributes:

Name Type Description
calculators FrozenSet, default=frozenset()

Frozenset of CalculatorEnum values. Empty means "any calculator" (no restriction).

Source code in src/easydiffraction/core/metadata.py
 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
@dataclass(frozen=True)
class CalculatorSupport:
    """
    Which calculation engines can handle this class.

    Attributes
    ----------
    calculators : FrozenSet, default=frozenset()
        Frozenset of ``CalculatorEnum`` values. Empty means "any
        calculator" (no restriction).
    """

    calculators: FrozenSet = frozenset()

    def supports(self, calculator: object) -> bool:
        """
        Check if a specific calculator can handle this class.

        Parameters
        ----------
        calculator : object
            A ``CalculatorEnum`` value.

        Returns
        -------
        bool
            ``True`` if the calculator is in the set, or if the set is
            empty (meaning any calculator is accepted).
        """
        if not self.calculators:
            return True
        return calculator in self.calculators

supports(calculator)

Check if a specific calculator can handle this class.

Parameters:

Name Type Description Default
calculator object

A CalculatorEnum value.

required

Returns:

Type Description
bool

True if the calculator is in the set, or if the set is empty (meaning any calculator is accepted).

Source code in src/easydiffraction/core/metadata.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
def supports(self, calculator: object) -> bool:
    """
    Check if a specific calculator can handle this class.

    Parameters
    ----------
    calculator : object
        A ``CalculatorEnum`` value.

    Returns
    -------
    bool
        ``True`` if the calculator is in the set, or if the set is
        empty (meaning any calculator is accepted).
    """
    if not self.calculators:
        return True
    return calculator in self.calculators

Compatibility dataclass

Experimental conditions under which a class can be used.

Each field is a frozenset of enum values representing the set of supported values for that axis. An empty frozenset means "compatible with any value of this axis" (i.e. no restriction).

Source code in src/easydiffraction/core/metadata.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@dataclass(frozen=True)
class Compatibility:
    """
    Experimental conditions under which a class can be used.

    Each field is a frozenset of enum values representing the set of
    supported values for that axis.  An empty frozenset means
    "compatible with any value of this axis" (i.e. no restriction).
    """

    sample_form: FrozenSet = frozenset()
    scattering_type: FrozenSet = frozenset()
    beam_mode: FrozenSet = frozenset()
    radiation_probe: FrozenSet = frozenset()

    def supports(
        self,
        sample_form: object = None,
        scattering_type: object = None,
        beam_mode: object = None,
        radiation_probe: object = None,
    ) -> bool:
        """
        Check if this compatibility matches the given conditions.

        Each argument is an optional enum member.  Returns ``True`` if
        every provided value is in the corresponding frozenset (or the
        frozenset is empty, meaning *any*).

        Example::

        compat.supports(     scattering_type=ScatteringTypeEnum.BRAGG,
        beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH, )
        """
        for axis, value in (
            ('sample_form', sample_form),
            ('scattering_type', scattering_type),
            ('beam_mode', beam_mode),
            ('radiation_probe', radiation_probe),
        ):
            if value is None:
                continue
            allowed = getattr(self, axis)
            if allowed and value not in allowed:
                return False
        return True

supports(sample_form=None, scattering_type=None, beam_mode=None, radiation_probe=None)

Check if this compatibility matches the given conditions.

Each argument is an optional enum member. Returns True if every provided value is in the corresponding frozenset (or the frozenset is empty, meaning any).

Example::

compat.supports( scattering_type=ScatteringTypeEnum.BRAGG, beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH, )

Source code in src/easydiffraction/core/metadata.py
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
def supports(
    self,
    sample_form: object = None,
    scattering_type: object = None,
    beam_mode: object = None,
    radiation_probe: object = None,
) -> bool:
    """
    Check if this compatibility matches the given conditions.

    Each argument is an optional enum member.  Returns ``True`` if
    every provided value is in the corresponding frozenset (or the
    frozenset is empty, meaning *any*).

    Example::

    compat.supports(     scattering_type=ScatteringTypeEnum.BRAGG,
    beam_mode=BeamModeEnum.CONSTANT_WAVELENGTH, )
    """
    for axis, value in (
        ('sample_form', sample_form),
        ('scattering_type', scattering_type),
        ('beam_mode', beam_mode),
        ('radiation_probe', radiation_probe),
    ):
        if value is None:
            continue
        allowed = getattr(self, axis)
        if allowed and value not in allowed:
            return False
    return True

TypeInfo dataclass

Stable identity and description for a factory-created class.

Attributes:

Name Type Description
tag str

Short, stable string identifier used for serialization, user-facing selection, and factory lookup. Must be unique within a factory's registry. Examples: 'line-segment', 'pseudo-voigt', 'cryspy'.

description str, default=''

One-line human-readable explanation. Used in show_supported() tables and documentation.

Source code in src/easydiffraction/core/metadata.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@dataclass(frozen=True)
class TypeInfo:
    """
    Stable identity and description for a factory-created class.

    Attributes
    ----------
    tag : str
        Short, stable string identifier used for serialization,
        user-facing selection, and factory lookup. Must be unique within
        a factory's registry. Examples: ``'line-segment'``,
        ``'pseudo-voigt'``, ``'cryspy'``.
    description : str, default=''
        One-line human-readable explanation. Used in
        ``show_supported()`` tables and documentation.
    """

    tag: str
    description: str = ''

singleton

ConstraintsHandler

Bases: SingletonBase

Manage parameter constraints using aliases and expressions.

Uses the asteval interpreter for safe evaluation of mathematical expressions. Constraints are defined as: lhs_alias = expression(rhs_aliases).

Source code in src/easydiffraction/core/singleton.py
 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
class ConstraintsHandler(SingletonBase):
    """
    Manage parameter constraints using aliases and expressions.

    Uses the asteval interpreter for safe evaluation of mathematical
    expressions. Constraints are defined as: lhs_alias =
    expression(rhs_aliases).
    """

    def __init__(self) -> None:
        # Maps alias names
        # (like 'biso_La') → ConstraintAlias(param=Parameter)
        self._alias_to_param: Dict[str, Any] = {}

        # Stores raw user-defined constraints indexed by lhs_alias
        # Each value should contain: lhs_alias, rhs_expr
        self._constraints = {}

        # Internally parsed constraints as (lhs_alias, rhs_expr) tuples
        self._parsed_constraints: List[Tuple[str, str]] = []

    def set_aliases(self, aliases: object) -> None:
        """
        Set the alias map (name → parameter wrapper).

        Called when user registers parameter aliases like:
        alias='biso_La', param=model.atom_sites['La'].b_iso
        """
        self._alias_to_param = dict(aliases.items())

    def set_constraints(self, constraints: object) -> None:
        """
        Set the constraints and triggers parsing into internal format.

        Called when user registers expressions like: lhs_alias='occ_Ba',
        rhs_expr='1 - occ_La'
        """
        self._constraints = constraints._items
        self._parse_constraints()

    def _parse_constraints(self) -> None:
        """Parse raw expressions into (lhs_alias, rhs_expr) pairs."""
        self._parsed_constraints = []

        for expr_obj in self._constraints:
            lhs_alias = expr_obj.lhs_alias
            rhs_expr = expr_obj.rhs_expr

            if lhs_alias and rhs_expr:
                constraint = (lhs_alias.strip(), rhs_expr.strip())
                self._parsed_constraints.append(constraint)

    def apply(self) -> None:
        """
        Evaluate constraints and applies them to dependent parameters.

        For each constraint: - Evaluate RHS using current values of
        aliases - Locate the dependent parameter by alias → uid → param
        - Update its value and mark it as constrained
        """
        if not self._parsed_constraints:
            return  # Nothing to apply

        # Retrieve global UID → Parameter object map
        uid_map = UidMapHandler.get().get_uid_map()

        # Prepare a flat dict of {alias: value} for use in expressions
        param_values = {}
        for alias, alias_obj in self._alias_to_param.items():
            uid = alias_obj.param_uid.value
            param = uid_map[uid]
            value = param.value
            param_values[alias] = value

        # Create an asteval interpreter for safe expression evaluation
        ae = Interpreter()
        ae.symtable.update(param_values)

        for lhs_alias, rhs_expr in self._parsed_constraints:
            try:
                # Evaluate the RHS expression using the current values
                rhs_value = ae(rhs_expr)

                # Get the actual parameter object we want to update
                dependent_uid = self._alias_to_param[lhs_alias].param_uid.value
                param = uid_map[dependent_uid]

                # Update its value and mark it as constrained
                param._set_value_constrained(rhs_value)

            except Exception as error:
                print(f"Failed to apply constraint '{lhs_alias} = {rhs_expr}': {error}")

apply()

Evaluate constraints and applies them to dependent parameters.

For each constraint: - Evaluate RHS using current values of aliases - Locate the dependent parameter by alias → uid → param - Update its value and mark it as constrained

Source code in src/easydiffraction/core/singleton.py
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
def apply(self) -> None:
    """
    Evaluate constraints and applies them to dependent parameters.

    For each constraint: - Evaluate RHS using current values of
    aliases - Locate the dependent parameter by alias → uid → param
    - Update its value and mark it as constrained
    """
    if not self._parsed_constraints:
        return  # Nothing to apply

    # Retrieve global UID → Parameter object map
    uid_map = UidMapHandler.get().get_uid_map()

    # Prepare a flat dict of {alias: value} for use in expressions
    param_values = {}
    for alias, alias_obj in self._alias_to_param.items():
        uid = alias_obj.param_uid.value
        param = uid_map[uid]
        value = param.value
        param_values[alias] = value

    # Create an asteval interpreter for safe expression evaluation
    ae = Interpreter()
    ae.symtable.update(param_values)

    for lhs_alias, rhs_expr in self._parsed_constraints:
        try:
            # Evaluate the RHS expression using the current values
            rhs_value = ae(rhs_expr)

            # Get the actual parameter object we want to update
            dependent_uid = self._alias_to_param[lhs_alias].param_uid.value
            param = uid_map[dependent_uid]

            # Update its value and mark it as constrained
            param._set_value_constrained(rhs_value)

        except Exception as error:
            print(f"Failed to apply constraint '{lhs_alias} = {rhs_expr}': {error}")

set_aliases(aliases)

Set the alias map (name → parameter wrapper).

Called when user registers parameter aliases like: alias='biso_La', param=model.atom_sites['La'].b_iso

Source code in src/easydiffraction/core/singleton.py
108
109
110
111
112
113
114
115
def set_aliases(self, aliases: object) -> None:
    """
    Set the alias map (name → parameter wrapper).

    Called when user registers parameter aliases like:
    alias='biso_La', param=model.atom_sites['La'].b_iso
    """
    self._alias_to_param = dict(aliases.items())

set_constraints(constraints)

Set the constraints and triggers parsing into internal format.

Called when user registers expressions like: lhs_alias='occ_Ba', rhs_expr='1 - occ_La'

Source code in src/easydiffraction/core/singleton.py
117
118
119
120
121
122
123
124
125
def set_constraints(self, constraints: object) -> None:
    """
    Set the constraints and triggers parsing into internal format.

    Called when user registers expressions like: lhs_alias='occ_Ba',
    rhs_expr='1 - occ_La'
    """
    self._constraints = constraints._items
    self._parse_constraints()

SingletonBase

Base class to implement Singleton pattern.

Ensures only one shared instance of a class is ever created. Useful for managing shared state across the library.

Source code in src/easydiffraction/core/singleton.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class SingletonBase:
    """
    Base class to implement Singleton pattern.

    Ensures only one shared instance of a class is ever created. Useful
    for managing shared state across the library.
    """

    _instance = None  # Class-level shared instance

    @classmethod
    def get(cls: Type[T]) -> T:
        """Return the shared instance, creating it if needed."""
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

get() classmethod

Return the shared instance, creating it if needed.

Source code in src/easydiffraction/core/singleton.py
28
29
30
31
32
33
@classmethod
def get(cls: Type[T]) -> T:
    """Return the shared instance, creating it if needed."""
    if cls._instance is None:
        cls._instance = cls()
    return cls._instance

UidMapHandler

Bases: SingletonBase

Global handler to manage UID-to-Parameter object mapping.

Source code in src/easydiffraction/core/singleton.py
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
class UidMapHandler(SingletonBase):
    """Global handler to manage UID-to-Parameter object mapping."""

    def __init__(self) -> None:
        # Internal map: uid (str) → Parameter instance
        self._uid_map: Dict[str, Any] = {}

    def get_uid_map(self) -> Dict[str, Any]:
        """Return the current UID-to-Parameter map."""
        return self._uid_map

    def add_to_uid_map(self, parameter: object) -> None:
        """
        Add a single Parameter or Descriptor object to the UID map.

        Only Descriptor or Parameter instances are allowed (not
        Components or others).
        """
        from easydiffraction.core.variable import GenericDescriptorBase

        if not isinstance(parameter, GenericDescriptorBase):
            raise TypeError(
                f'Cannot add object of type {type(parameter).__name__} to UID map. '
                'Only Descriptor or Parameter instances are allowed.'
            )
        self._uid_map[parameter.uid] = parameter

    def replace_uid(self, old_uid: str, new_uid: str) -> None:
        """
        Replace an existing UID key in the UID map with a new UID.

        Moves the associated parameter from old_uid to new_uid. Raises a
        KeyError if the old_uid doesn't exist.
        """
        if old_uid not in self._uid_map:
            # Only raise if old_uid is not None and not empty
            print('DEBUG: replace_uid failed', old_uid, 'current map:', list(self._uid_map.keys()))
            raise KeyError(f"UID '{old_uid}' not found in the UID map.")
        self._uid_map[new_uid] = self._uid_map.pop(old_uid)

add_to_uid_map(parameter)

Add a single Parameter or Descriptor object to the UID map.

Only Descriptor or Parameter instances are allowed (not Components or others).

Source code in src/easydiffraction/core/singleton.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def add_to_uid_map(self, parameter: object) -> None:
    """
    Add a single Parameter or Descriptor object to the UID map.

    Only Descriptor or Parameter instances are allowed (not
    Components or others).
    """
    from easydiffraction.core.variable import GenericDescriptorBase

    if not isinstance(parameter, GenericDescriptorBase):
        raise TypeError(
            f'Cannot add object of type {type(parameter).__name__} to UID map. '
            'Only Descriptor or Parameter instances are allowed.'
        )
    self._uid_map[parameter.uid] = parameter

get_uid_map()

Return the current UID-to-Parameter map.

Source code in src/easydiffraction/core/singleton.py
46
47
48
def get_uid_map(self) -> Dict[str, Any]:
    """Return the current UID-to-Parameter map."""
    return self._uid_map

replace_uid(old_uid, new_uid)

Replace an existing UID key in the UID map with a new UID.

Moves the associated parameter from old_uid to new_uid. Raises a KeyError if the old_uid doesn't exist.

Source code in src/easydiffraction/core/singleton.py
66
67
68
69
70
71
72
73
74
75
76
77
def replace_uid(self, old_uid: str, new_uid: str) -> None:
    """
    Replace an existing UID key in the UID map with a new UID.

    Moves the associated parameter from old_uid to new_uid. Raises a
    KeyError if the old_uid doesn't exist.
    """
    if old_uid not in self._uid_map:
        # Only raise if old_uid is not None and not empty
        print('DEBUG: replace_uid failed', old_uid, 'current map:', list(self._uid_map.keys()))
        raise KeyError(f"UID '{old_uid}' not found in the UID map.")
    self._uid_map[new_uid] = self._uid_map.pop(old_uid)

validation

Lightweight runtime validation utilities.

Provides DataTypes, type/content validators, and AttributeSpec used by descriptors and parameters. Only documentation was added here.

AttributeSpec

Hold metadata and validators for a single attribute.

Source code in src/easydiffraction/core/validation.py
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
class AttributeSpec:
    """Hold metadata and validators for a single attribute."""

    def __init__(
        self,
        *,
        default: object = None,
        data_type: DataTypes | None = None,
        validator: ValidatorBase | None = None,
        allow_none: bool = False,
    ) -> None:
        self.default = default
        self.allow_none = allow_none
        self._data_type_validator = TypeValidator(data_type) if data_type else None
        self._validator = validator

    def validated(
        self,
        value: object,
        name: str,
        current: object = None,
    ) -> object:
        """
        Validate through type and content validators.

        Returns validated value, possibly default or current if errors
        occur. None may short-circuit further checks when allowed.
        """
        val = value
        # Evaluate callable defaults dynamically
        default = self.default() if callable(self.default) else self.default

        # Type validation
        if self._data_type_validator:
            val = self._data_type_validator.validated(
                val,
                name,
                default=default,
                current=current,
                allow_none=self.allow_none,
            )

        # Skip further validation: Special case for None
        if val is None and self.allow_none:
            Diagnostics.none_value_skip_range(name)
            return None

        # Content validation
        if self._validator and val is not None:
            val = self._validator.validated(
                val,
                name,
                default=default,
                current=current,
            )

        return val

validated(value, name, current=None)

Validate through type and content validators.

Returns validated value, possibly default or current if errors occur. None may short-circuit further checks when allowed.

Source code in src/easydiffraction/core/validation.py
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
def validated(
    self,
    value: object,
    name: str,
    current: object = None,
) -> object:
    """
    Validate through type and content validators.

    Returns validated value, possibly default or current if errors
    occur. None may short-circuit further checks when allowed.
    """
    val = value
    # Evaluate callable defaults dynamically
    default = self.default() if callable(self.default) else self.default

    # Type validation
    if self._data_type_validator:
        val = self._data_type_validator.validated(
            val,
            name,
            default=default,
            current=current,
            allow_none=self.allow_none,
        )

    # Skip further validation: Special case for None
    if val is None and self.allow_none:
        Diagnostics.none_value_skip_range(name)
        return None

    # Content validation
    if self._validator and val is not None:
        val = self._validator.validated(
            val,
            name,
            default=default,
            current=current,
        )

    return val

DataTypeHints

Type hint aliases for numeric, string, and boolean types.

Source code in src/easydiffraction/core/validation.py
26
27
28
29
30
31
class DataTypeHints:
    """Type hint aliases for numeric, string, and boolean types."""

    Numeric = int | float | np.integer | np.floating
    String = str
    Bool = bool

DataTypes

Bases: Enum

Enumeration of supported data types for descriptors.

Source code in src/easydiffraction/core/validation.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class DataTypes(Enum):
    """Enumeration of supported data types for descriptors."""

    NUMERIC = (int, float, np.integer, np.floating)
    STRING = (str,)
    BOOL = (bool,)
    ANY = (object,)  # fallback for unconstrained

    def __str__(self) -> str:
        """Return the lowercase name of the data type."""
        return self.name.lower()

    @property
    def expected_type(self) -> tuple:
        """Convenience alias for tuple of allowed Python types."""
        return self.value

__str__()

Return the lowercase name of the data type.

Source code in src/easydiffraction/core/validation.py
45
46
47
def __str__(self) -> str:
    """Return the lowercase name of the data type."""
    return self.name.lower()

expected_type property

Convenience alias for tuple of allowed Python types.

MembershipValidator

Bases: ValidatorBase

Ensure that a value is among allowed choices.

allowed may be an iterable or a callable returning a collection.

Source code in src/easydiffraction/core/validation.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
class MembershipValidator(ValidatorBase):
    """
    Ensure that a value is among allowed choices.

    ``allowed`` may be an iterable or a callable returning a collection.
    """

    def __init__(self, allowed: object) -> None:
        # Do not convert immediately to list — may be callable
        self.allowed = allowed

    def validated(
        self,
        value: object,
        name: str,
        default: object = None,
        current: object = None,
    ) -> object:
        """Validate membership and return value or fallback."""
        # Dynamically evaluate allowed if callable (e.g. lambda)
        allowed_values = self.allowed() if callable(self.allowed) else self.allowed

        if value not in allowed_values:
            Diagnostics.choice_mismatch(
                name,
                value,
                allowed_values,
                current=current,
                default=default,
            )
            return self._fallback(current, default)

        Diagnostics.validated(
            name,
            value,
            stage=ValidationStage.MEMBERSHIP,
        )
        return value

validated(value, name, default=None, current=None)

Validate membership and return value or fallback.

Source code in src/easydiffraction/core/validation.py
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
def validated(
    self,
    value: object,
    name: str,
    default: object = None,
    current: object = None,
) -> object:
    """Validate membership and return value or fallback."""
    # Dynamically evaluate allowed if callable (e.g. lambda)
    allowed_values = self.allowed() if callable(self.allowed) else self.allowed

    if value not in allowed_values:
        Diagnostics.choice_mismatch(
            name,
            value,
            allowed_values,
            current=current,
            default=default,
        )
        return self._fallback(current, default)

    Diagnostics.validated(
        name,
        value,
        stage=ValidationStage.MEMBERSHIP,
    )
    return value

RangeValidator

Bases: ValidatorBase

Ensure a numeric value lies within [ge, le].

Source code in src/easydiffraction/core/validation.py
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
class RangeValidator(ValidatorBase):
    """Ensure a numeric value lies within [ge, le]."""

    def __init__(
        self,
        *,
        ge: float = -np.inf,
        le: float = np.inf,
    ) -> None:
        self.ge, self.le = ge, le

    def validated(
        self,
        value: object,
        name: str,
        default: object = None,
        current: object = None,
    ) -> object:
        """Validate range and return value or fallback."""
        if not (self.ge <= value <= self.le):
            Diagnostics.range_mismatch(
                name,
                value,
                self.ge,
                self.le,
                current=current,
                default=default,
            )
            return self._fallback(current, default)

        Diagnostics.validated(
            name,
            value,
            stage=ValidationStage.RANGE,
        )
        return value

validated(value, name, default=None, current=None)

Validate range and return value or fallback.

Source code in src/easydiffraction/core/validation.py
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
def validated(
    self,
    value: object,
    name: str,
    default: object = None,
    current: object = None,
) -> object:
    """Validate range and return value or fallback."""
    if not (self.ge <= value <= self.le):
        Diagnostics.range_mismatch(
            name,
            value,
            self.ge,
            self.le,
            current=current,
            default=default,
        )
        return self._fallback(current, default)

    Diagnostics.validated(
        name,
        value,
        stage=ValidationStage.RANGE,
    )
    return value

RegexValidator

Bases: ValidatorBase

Ensure that a string matches a given regular expression.

Source code in src/easydiffraction/core/validation.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
class RegexValidator(ValidatorBase):
    """Ensure that a string matches a given regular expression."""

    def __init__(self, pattern: str) -> None:
        self.pattern = re.compile(pattern)

    def validated(
        self,
        value: object,
        name: str,
        default: object = None,
        current: object = None,
    ) -> object:
        """Validate regex and return value or fallback."""
        if not self.pattern.fullmatch(value):
            Diagnostics.regex_mismatch(
                name,
                value,
                self.pattern.pattern,
                current=current,
                default=default,
            )
            return self._fallback(current, default)

        Diagnostics.validated(
            name,
            value,
            stage=ValidationStage.REGEX,
        )
        return value

validated(value, name, default=None, current=None)

Validate regex and return value or fallback.

Source code in src/easydiffraction/core/validation.py
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def validated(
    self,
    value: object,
    name: str,
    default: object = None,
    current: object = None,
) -> object:
    """Validate regex and return value or fallback."""
    if not self.pattern.fullmatch(value):
        Diagnostics.regex_mismatch(
            name,
            value,
            self.pattern.pattern,
            current=current,
            default=default,
        )
        return self._fallback(current, default)

    Diagnostics.validated(
        name,
        value,
        stage=ValidationStage.REGEX,
    )
    return value

TypeValidator

Bases: ValidatorBase

Ensure a value is of the expected data type.

Source code in src/easydiffraction/core/validation.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
class TypeValidator(ValidatorBase):
    """Ensure a value is of the expected data type."""

    def __init__(self, expected_type: DataTypes) -> None:
        if isinstance(expected_type, DataTypes):
            self.expected_type = expected_type
            self.expected_label = str(expected_type)
        else:
            raise TypeError(f'TypeValidator expected a DataTypes member, got {expected_type!r}')

    def validated(
        self,
        value: object,
        name: str,
        default: object = None,
        current: object = None,
        allow_none: bool = False,
    ) -> object:
        """
        Validate type and return value or fallback.

        If allow_none is True, None bypasses content checks.
        """
        # Fresh initialization, use default
        if current is None and value is None:
            Diagnostics.no_value(name, default)
            return default

        # Explicit None (allowed)
        if value is None and allow_none:
            Diagnostics.none_value(name)
            return None

        # Normal type validation
        if not isinstance(value, self.expected_type.value):
            Diagnostics.type_mismatch(
                name,
                value,
                expected_type=self.expected_label,
                current=current,
                default=default,
            )
            return self._fallback(current, default)

        Diagnostics.validated(
            name,
            value,
            stage=ValidationStage.TYPE,
        )
        return value

validated(value, name, default=None, current=None, allow_none=False)

Validate type and return value or fallback.

If allow_none is True, None bypasses content checks.

Source code in src/easydiffraction/core/validation.py
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
def validated(
    self,
    value: object,
    name: str,
    default: object = None,
    current: object = None,
    allow_none: bool = False,
) -> object:
    """
    Validate type and return value or fallback.

    If allow_none is True, None bypasses content checks.
    """
    # Fresh initialization, use default
    if current is None and value is None:
        Diagnostics.no_value(name, default)
        return default

    # Explicit None (allowed)
    if value is None and allow_none:
        Diagnostics.none_value(name)
        return None

    # Normal type validation
    if not isinstance(value, self.expected_type.value):
        Diagnostics.type_mismatch(
            name,
            value,
            expected_type=self.expected_label,
            current=current,
            default=default,
        )
        return self._fallback(current, default)

    Diagnostics.validated(
        name,
        value,
        stage=ValidationStage.TYPE,
    )
    return value

ValidationStage

Bases: Enum

Phases of validation for diagnostic logging.

Source code in src/easydiffraction/core/validation.py
60
61
62
63
64
65
66
67
68
69
70
class ValidationStage(Enum):
    """Phases of validation for diagnostic logging."""

    TYPE = auto()
    RANGE = auto()
    MEMBERSHIP = auto()
    REGEX = auto()

    def __str__(self) -> str:
        """Return the lowercase name of the validation stage."""
        return self.name.lower()

__str__()

Return the lowercase name of the validation stage.

Source code in src/easydiffraction/core/validation.py
68
69
70
def __str__(self) -> str:
    """Return the lowercase name of the validation stage."""
    return self.name.lower()

ValidatorBase

Bases: ABC

Abstract base class for all validators.

Source code in src/easydiffraction/core/validation.py
 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
class ValidatorBase(ABC):
    """Abstract base class for all validators."""

    @abstractmethod
    def validated(
        self,
        value: object,
        name: str,
        default: object = None,
        current: object = None,
    ) -> object:
        """
        Return a validated value or fallback.

        Subclasses must implement this method.
        """
        raise NotImplementedError

    def _fallback(
        self,
        current: object = None,
        default: object = None,
    ) -> object:
        """Return current if set, else default."""
        return current if current is not None else default

validated(value, name, default=None, current=None) abstractmethod

Return a validated value or fallback.

Subclasses must implement this method.

Source code in src/easydiffraction/core/validation.py
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@abstractmethod
def validated(
    self,
    value: object,
    name: str,
    default: object = None,
    current: object = None,
) -> object:
    """
    Return a validated value or fallback.

    Subclasses must implement this method.
    """
    raise NotImplementedError

variable

GenericDescriptorBase

Bases: GuardedBase

Base class for all parameter-like descriptors.

A descriptor encapsulates a typed value with validation, human-readable name/description and a globally unique identifier that is stable across the session. Concrete subclasses specialize the expected data type and can extend the public API with additional behavior (e.g. units).

Source code in src/easydiffraction/core/variable.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
class GenericDescriptorBase(GuardedBase):
    """
    Base class for all parameter-like descriptors.

    A descriptor encapsulates a typed value with validation,
    human-readable name/description and a globally unique identifier
    that is stable across the session. Concrete subclasses specialize
    the expected data type and can extend the public API with additional
    behavior (e.g. units).
    """

    _BOOL_SPEC_TEMPLATE = AttributeSpec(
        data_type=DataTypes.BOOL,
        default=False,
    )

    def __init__(
        self,
        *,
        value_spec: AttributeSpec,
        name: str,
        description: str = None,
    ) -> None:
        """
        Initialize the descriptor with validation and identity.

        Parameters
        ----------
        value_spec : AttributeSpec
            Validation specification for the value.
        name : str
            Local name of the descriptor within its category.
        description : str, default=None
            Optional human-readable description.
        """
        super().__init__()

        expected_type = getattr(self, '_value_type', None)

        if expected_type:
            user_type = (
                value_spec._data_type_validator.expected_type
                if value_spec._data_type_validator is not None
                else None
            )
            if user_type and user_type is not expected_type:
                Diagnostics.type_override_error(
                    type(self).__name__,
                    expected_type,
                    user_type,
                )
            else:
                # Enforce descriptor's own type if not already defined
                value_spec._data_type_validator = TypeValidator(expected_type)

        self._value_spec = value_spec
        self._name = name
        self._description = description

        # Initial validated states
        # self._value = self._value_spec.validated(
        #    value_spec.value,
        #    name=self.unique_name,
        # )

        # Assign default directly.
        # Skip validation — defaults are trusted.
        # Callable is needed for dynamic defaults like SpaceGroup
        # it_coordinate_system_code, and similar cases.
        default = value_spec.default
        self._value = default() if callable(default) else default

    def __str__(self) -> str:
        """Return the string representation of this descriptor."""
        return f'<{self.unique_name} = {self.value!r}>'

    @property
    def name(self) -> str:
        """Local name of the descriptor (without category/datablock)."""
        return self._name

    @property
    def unique_name(self) -> str:
        """Fully qualified name: datablock, category and entry."""
        parts = [
            self._identity.datablock_entry_name,
            self._identity.category_code,
            self._identity.category_entry_name,
            self.name,
        ]
        return '.'.join(filter(None, parts))

    def _parent_of_type(self, cls: type) -> object | None:
        """Traverse parents and return the first of type cls."""
        obj = getattr(self, '_parent', None)
        visited = set()
        while obj is not None and id(obj) not in visited:
            visited.add(id(obj))
            if isinstance(obj, cls):
                return obj
            obj = getattr(obj, '_parent', None)
        return None

    def _datablock_item(self) -> object | None:
        """Return the DatablockItem ancestor, if any."""
        from easydiffraction.core.datablock import DatablockItem

        return self._parent_of_type(DatablockItem)

    @property
    def value(self) -> object:
        """Current validated value."""
        return self._value

    @value.setter
    def value(self, v: object) -> None:
        """Set a new value after validating against the spec."""
        # Do nothing if the value is unchanged
        if self._value == v:
            return

        # Validate and set the new value
        self._value = self._value_spec.validated(
            v,
            name=self.unique_name,
            current=self._value,
        )

        # Mark parent datablock as needing categories update
        # TODO: Check if it is actually in use?
        parent_datablock = self._datablock_item()
        if parent_datablock is not None:
            parent_datablock._need_categories_update = True

    def _set_value_from_minimizer(self, v: object) -> None:
        """
        Set the value from a minimizer, bypassing validation.

        Writes ``_value`` directly — no type or range checks — but still
        marks the owning :class:`DatablockItem` dirty so that
        ``_update_categories()`` knows work is needed.

        This exists because:

        1. Physical-range validators (e.g. intensity ≥ 0) would reject
        trial values the minimizer needs to explore. 2. Validation
        overhead is measurable over thousands of    objective-function
        evaluations.
        """
        self._value = v
        parent_datablock = self._datablock_item()
        if parent_datablock is not None:
            parent_datablock._need_categories_update = True

    @property
    def description(self) -> str | None:
        """Optional human-readable description."""
        return self._description

    @property
    def parameters(self) -> list[GenericDescriptorBase]:
        """
        Return a flat list of parameters contained by this object.

        For a single descriptor, it returns a one-element list with
        itself. Composite objects override this to flatten nested
        structures.
        """
        return [self]

    @property
    def as_cif(self) -> str:
        """Serialize this descriptor to a CIF-formatted string."""
        return param_to_cif(self)

    def from_cif(self, block: object, idx: int = 0) -> None:
        """Populate this parameter from a CIF block."""
        param_from_cif(self, block, idx)

__init__(*, value_spec, name, description=None)

Initialize the descriptor with validation and identity.

Parameters:

Name Type Description Default
value_spec AttributeSpec

Validation specification for the value.

required
name str

Local name of the descriptor within its category.

required
description str

Optional human-readable description.

None
Source code in src/easydiffraction/core/variable.py
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
def __init__(
    self,
    *,
    value_spec: AttributeSpec,
    name: str,
    description: str = None,
) -> None:
    """
    Initialize the descriptor with validation and identity.

    Parameters
    ----------
    value_spec : AttributeSpec
        Validation specification for the value.
    name : str
        Local name of the descriptor within its category.
    description : str, default=None
        Optional human-readable description.
    """
    super().__init__()

    expected_type = getattr(self, '_value_type', None)

    if expected_type:
        user_type = (
            value_spec._data_type_validator.expected_type
            if value_spec._data_type_validator is not None
            else None
        )
        if user_type and user_type is not expected_type:
            Diagnostics.type_override_error(
                type(self).__name__,
                expected_type,
                user_type,
            )
        else:
            # Enforce descriptor's own type if not already defined
            value_spec._data_type_validator = TypeValidator(expected_type)

    self._value_spec = value_spec
    self._name = name
    self._description = description

    # Initial validated states
    # self._value = self._value_spec.validated(
    #    value_spec.value,
    #    name=self.unique_name,
    # )

    # Assign default directly.
    # Skip validation — defaults are trusted.
    # Callable is needed for dynamic defaults like SpaceGroup
    # it_coordinate_system_code, and similar cases.
    default = value_spec.default
    self._value = default() if callable(default) else default

__str__()

Return the string representation of this descriptor.

Source code in src/easydiffraction/core/variable.py
100
101
102
def __str__(self) -> str:
    """Return the string representation of this descriptor."""
    return f'<{self.unique_name} = {self.value!r}>'

as_cif property

Serialize this descriptor to a CIF-formatted string.

description property

Optional human-readable description.

from_cif(block, idx=0)

Populate this parameter from a CIF block.

Source code in src/easydiffraction/core/variable.py
203
204
205
def from_cif(self, block: object, idx: int = 0) -> None:
    """Populate this parameter from a CIF block."""
    param_from_cif(self, block, idx)

name property

Local name of the descriptor (without category/datablock).

parameters property

Return a flat list of parameters contained by this object.

For a single descriptor, it returns a one-element list with itself. Composite objects override this to flatten nested structures.

unique_name property

Fully qualified name: datablock, category and entry.

value property writable

Current validated value.

GenericNumericDescriptor

Bases: GenericDescriptorBase

Base descriptor that constrains values to numbers.

Source code in src/easydiffraction/core/variable.py
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
class GenericNumericDescriptor(GenericDescriptorBase):
    """Base descriptor that constrains values to numbers."""

    _value_type = DataTypes.NUMERIC

    def __init__(
        self,
        *,
        units: str = '',
        **kwargs: object,
    ) -> None:
        super().__init__(**kwargs)
        self._units: str = units

    def __str__(self) -> str:
        """Return the string representation including units."""
        s: str = super().__str__()
        s = s[1:-1]  # strip <>
        if self.units:
            s += f' {self.units}'
        return f'<{s}>'

    @property
    def units(self) -> str:
        """Units associated with the numeric value, if any."""
        return self._units

__str__()

Return the string representation including units.

Source code in src/easydiffraction/core/variable.py
240
241
242
243
244
245
246
def __str__(self) -> str:
    """Return the string representation including units."""
    s: str = super().__str__()
    s = s[1:-1]  # strip <>
    if self.units:
        s += f' {self.units}'
    return f'<{s}>'

units property

Units associated with the numeric value, if any.

GenericParameter

Bases: GenericNumericDescriptor

Numeric descriptor extended with fitting-related attributes.

Adds standard attributes used by minimizers: "free" flag, uncertainty, bounds and an optional starting value. Subclasses can integrate with specific backends while preserving this interface.

Source code in src/easydiffraction/core/variable.py
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
class GenericParameter(GenericNumericDescriptor):
    """
    Numeric descriptor extended with fitting-related attributes.

    Adds standard attributes used by minimizers: "free" flag,
    uncertainty, bounds and an optional starting value. Subclasses can
    integrate with specific backends while preserving this interface.
    """

    def __init__(
        self,
        **kwargs: object,
    ) -> None:
        super().__init__(**kwargs)

        # Initial validated states
        self._free_spec = self._BOOL_SPEC_TEMPLATE
        self._free = self._free_spec.default
        self._uncertainty_spec = AttributeSpec(
            data_type=DataTypes.NUMERIC,
            validator=RangeValidator(ge=0),
            allow_none=True,
        )
        self._uncertainty = self._uncertainty_spec.default
        self._fit_min_spec = AttributeSpec(data_type=DataTypes.NUMERIC, default=-np.inf)
        self._fit_min = self._fit_min_spec.default
        self._fit_max_spec = AttributeSpec(data_type=DataTypes.NUMERIC, default=np.inf)
        self._fit_max = self._fit_max_spec.default
        self._start_value_spec = AttributeSpec(data_type=DataTypes.NUMERIC, default=0.0)
        self._start_value = self._start_value_spec.default
        self._constrained_spec = self._BOOL_SPEC_TEMPLATE
        self._constrained = self._constrained_spec.default

        self._uid: str = self._generate_uid()
        UidMapHandler.get().add_to_uid_map(self)

    def __str__(self) -> str:
        """Return string representation with uncertainty and free."""
        s = GenericDescriptorBase.__str__(self)
        s = s[1:-1]  # strip <>
        if self.uncertainty is not None:
            s += f' ± {self.uncertainty}'
        if self.units is not None:
            s += f' {self.units}'
        s += f' (free={self.free})'
        return f'<{s}>'

    @staticmethod
    def _generate_uid(length: int = 16) -> str:
        letters = string.ascii_lowercase
        return ''.join(secrets.choice(letters) for _ in range(length))

    @property
    def uid(self) -> str:
        """Stable random identifier for this descriptor."""
        return self._uid

    @property
    def _minimizer_uid(self) -> str:
        """Variant of uid that is safe for minimizer engines."""
        # return self.unique_name.replace('.', '__')
        return self.uid

    @property
    def constrained(self) -> bool:
        """Whether this parameter is part of a constraint expression."""
        return self._constrained

    def _set_value_constrained(self, v: object) -> None:
        """
        Set the value from a constraint expression.

        Validates against the spec, marks the parent datablock dirty,
        and flags the parameter as constrained. Used exclusively by
        ``ConstraintsHandler.apply()``.
        """
        self.value = v
        self._constrained = True

    @property
    def free(self) -> bool:
        """Whether this parameter is currently varied during fitting."""
        return self._free

    @free.setter
    def free(self, v: bool) -> None:
        """Set the "free" flag after validation."""
        self._free = self._free_spec.validated(
            v, name=f'{self.unique_name}.free', current=self._free
        )

    @property
    def uncertainty(self) -> float | None:
        """Estimated standard uncertainty of the fitted value."""
        return self._uncertainty

    @uncertainty.setter
    def uncertainty(self, v: float | None) -> None:
        """Set the uncertainty value (must be non-negative or None)."""
        self._uncertainty = self._uncertainty_spec.validated(
            v, name=f'{self.unique_name}.uncertainty', current=self._uncertainty
        )

    @property
    def fit_min(self) -> float:
        """Lower fitting bound."""
        return self._fit_min

    @fit_min.setter
    def fit_min(self, v: float) -> None:
        """Set the lower bound for the parameter value."""
        self._fit_min = self._fit_min_spec.validated(
            v, name=f'{self.unique_name}.fit_min', current=self._fit_min
        )

    @property
    def fit_max(self) -> float:
        """Upper fitting bound."""
        return self._fit_max

    @fit_max.setter
    def fit_max(self, v: float) -> None:
        """Set the upper bound for the parameter value."""
        self._fit_max = self._fit_max_spec.validated(
            v, name=f'{self.unique_name}.fit_max', current=self._fit_max
        )

__str__()

Return string representation with uncertainty and free.

Source code in src/easydiffraction/core/variable.py
293
294
295
296
297
298
299
300
301
302
def __str__(self) -> str:
    """Return string representation with uncertainty and free."""
    s = GenericDescriptorBase.__str__(self)
    s = s[1:-1]  # strip <>
    if self.uncertainty is not None:
        s += f' ± {self.uncertainty}'
    if self.units is not None:
        s += f' {self.units}'
    s += f' (free={self.free})'
    return f'<{s}>'

constrained property

Whether this parameter is part of a constraint expression.

fit_max property writable

Upper fitting bound.

fit_min property writable

Lower fitting bound.

free property writable

Whether this parameter is currently varied during fitting.

uid property

Stable random identifier for this descriptor.

uncertainty property writable

Estimated standard uncertainty of the fitted value.

GenericStringDescriptor

Bases: GenericDescriptorBase

Base descriptor that constrains values to strings.

Source code in src/easydiffraction/core/variable.py
211
212
213
214
215
216
217
218
219
220
class GenericStringDescriptor(GenericDescriptorBase):
    """Base descriptor that constrains values to strings."""

    _value_type = DataTypes.STRING

    def __init__(
        self,
        **kwargs: object,
    ) -> None:
        super().__init__(**kwargs)

NumericDescriptor

Bases: GenericNumericDescriptor

Numeric descriptor bound to a CIF handler.

Source code in src/easydiffraction/core/variable.py
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
class NumericDescriptor(GenericNumericDescriptor):
    """Numeric descriptor bound to a CIF handler."""

    def __init__(
        self,
        *,
        cif_handler: CifHandler,
        **kwargs: object,
    ) -> None:
        """
        Numeric descriptor bound to a CIF handler.

        Parameters
        ----------
        cif_handler : CifHandler
            Object that tracks CIF identifiers.
        **kwargs : object
            Forwarded to GenericNumericDescriptor.
        """
        super().__init__(**kwargs)
        self._cif_handler = cif_handler
        self._cif_handler.attach(self)

__init__(*, cif_handler, **kwargs)

Numeric descriptor bound to a CIF handler.

Parameters:

Name Type Description Default
cif_handler CifHandler

Object that tracks CIF identifiers.

required
**kwargs object

Forwarded to GenericNumericDescriptor.

{}
Source code in src/easydiffraction/core/variable.py
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
def __init__(
    self,
    *,
    cif_handler: CifHandler,
    **kwargs: object,
) -> None:
    """
    Numeric descriptor bound to a CIF handler.

    Parameters
    ----------
    cif_handler : CifHandler
        Object that tracks CIF identifiers.
    **kwargs : object
        Forwarded to GenericNumericDescriptor.
    """
    super().__init__(**kwargs)
    self._cif_handler = cif_handler
    self._cif_handler.attach(self)

Parameter

Bases: GenericParameter

Fittable parameter bound to a CIF handler.

Source code in src/easydiffraction/core/variable.py
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
class Parameter(GenericParameter):
    """Fittable parameter bound to a CIF handler."""

    def __init__(
        self,
        *,
        cif_handler: CifHandler,
        **kwargs: object,
    ) -> None:
        """
        Fittable parameter bound to a CIF handler.

        Parameters
        ----------
        cif_handler : CifHandler
            Object that tracks CIF identifiers.
        **kwargs : object
            Forwarded to GenericParameter.
        """
        super().__init__(**kwargs)
        self._cif_handler = cif_handler
        self._cif_handler.attach(self)

__init__(*, cif_handler, **kwargs)

Fittable parameter bound to a CIF handler.

Parameters:

Name Type Description Default
cif_handler CifHandler

Object that tracks CIF identifiers.

required
**kwargs object

Forwarded to GenericParameter.

{}
Source code in src/easydiffraction/core/variable.py
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
def __init__(
    self,
    *,
    cif_handler: CifHandler,
    **kwargs: object,
) -> None:
    """
    Fittable parameter bound to a CIF handler.

    Parameters
    ----------
    cif_handler : CifHandler
        Object that tracks CIF identifiers.
    **kwargs : object
        Forwarded to GenericParameter.
    """
    super().__init__(**kwargs)
    self._cif_handler = cif_handler
    self._cif_handler.attach(self)

StringDescriptor

Bases: GenericStringDescriptor

String descriptor bound to a CIF handler.

Source code in src/easydiffraction/core/variable.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
class StringDescriptor(GenericStringDescriptor):
    """String descriptor bound to a CIF handler."""

    def __init__(
        self,
        *,
        cif_handler: CifHandler,
        **kwargs: object,
    ) -> None:
        """
        Initialize a string descriptor bound to a CIF handler.

        Parameters
        ----------
        cif_handler : CifHandler
            Object that tracks CIF identifiers.
        **kwargs : object
            Forwarded to GenericStringDescriptor.
        """
        super().__init__(**kwargs)
        self._cif_handler = cif_handler
        self._cif_handler.attach(self)

__init__(*, cif_handler, **kwargs)

Initialize a string descriptor bound to a CIF handler.

Parameters:

Name Type Description Default
cif_handler CifHandler

Object that tracks CIF identifiers.

required
**kwargs object

Forwarded to GenericStringDescriptor.

{}
Source code in src/easydiffraction/core/variable.py
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
def __init__(
    self,
    *,
    cif_handler: CifHandler,
    **kwargs: object,
) -> None:
    """
    Initialize a string descriptor bound to a CIF handler.

    Parameters
    ----------
    cif_handler : CifHandler
        Object that tracks CIF identifiers.
    **kwargs : object
        Forwarded to GenericStringDescriptor.
    """
    super().__init__(**kwargs)
    self._cif_handler = cif_handler
    self._cif_handler.attach(self)