Skip to content

core

objects

Collection

Bases: ABC

Base class for collections like AtomSites, LinkedPhases, SampleModels, Experiments, etc.

Source code in src/easydiffraction/core/objects.py
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
class Collection(ABC):
    """
    Base class for collections like AtomSites, LinkedPhases, SampleModels,
    Experiments, etc.
    """

    @property
    @abstractmethod
    def _child_class(self):
        return None

    def __init__(self, parent=None):
        self._parent = parent  # Parent datablock
        self._datablock_id = None  # Parent datablock name to be set by the parent
        self._items = {}

    def __getitem__(self, key: str) -> Union[Component, 'Collection']:
        return self._items[key]

    def __iter__(self) -> Iterator[Union[Component, 'Collection']]:
        return iter(self._items.values())

    @property
    def datablock_id(self):
        return self._datablock_id

    @datablock_id.setter
    def datablock_id(self, new_id):
        self._datablock_id = new_id
        for param in self.get_all_params():
            param.datablock_id = new_id

    def add(self, *args, **kwargs):
        """
        Add a new item to the collection. The item must be a subclass of
        Component.
        """
        if self._child_class is None:
            raise ValueError('Child class is not defined.')
        child_obj = self._child_class(*args, **kwargs)
        child_obj.datablock_id = self.datablock_id  # Setting the datablock_id to update its child parameters
        child_obj.entry_id = child_obj.entry_id  # Forcing the entry_id to be reset to update its child parameters
        self._items[child_obj._entry_id] = child_obj

        # Call on_item_added if it exists, i.e. defined in the derived class
        if hasattr(self, 'on_item_added'):
            self.on_item_added(child_obj)

    def get_all_params(self):
        params = []
        for item in self._items.values():
            if isinstance(item, Datablock):
                datablock = item
                for datablock_item in datablock.items():
                    if isinstance(datablock_item, Component):
                        component = datablock_item
                        for param in component.get_all_params():
                            params.append(param)
                    elif isinstance(datablock_item, Collection):
                        collection = datablock_item
                        for component in collection:
                            for param in component.get_all_params():
                                params.append(param)
            elif isinstance(item, Component):
                component = item
                for param in component.get_all_params():
                    params.append(param)
            else:
                raise TypeError(f'Expected a Component or Datablock, got {type(item)}')
        return params

    def get_fittable_params(self) -> List[Parameter]:
        all_params = self.get_all_params()
        params = []
        for param in all_params:
            if hasattr(param, 'free') and not param.constrained:
                params.append(param)
        return params

    def get_free_params(self) -> List[Parameter]:
        fittable_params = self.get_fittable_params()
        params = []
        for param in fittable_params:
            if param.free:
                params.append(param)
        return params

    def as_cif(self) -> str:
        lines = []
        if self._type == 'category':
            for idx, item in enumerate(self._items.values()):
                params = item.as_dict()
                category_key = item.cif_category_key
                # Keys
                keys = [f'_{category_key}.{param_key}' for param_key in params.keys()]
                # Values. If the value is a string and contains spaces, add quotes
                values = []
                for value in params.values():
                    value = f'{value}'
                    if ' ' in value:
                        value = f'"{value}"'
                    values.append(value)
                # Header is added only for the first item
                if idx == 0:
                    lines.append('loop_')
                    header = '\n'.join(keys)
                    lines.append(header)
                line = ' '.join(values)
                lines.append(line)
        return '\n'.join(lines)

add(*args, **kwargs)

Add a new item to the collection. The item must be a subclass of Component.

Source code in src/easydiffraction/core/objects.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
def add(self, *args, **kwargs):
    """
    Add a new item to the collection. The item must be a subclass of
    Component.
    """
    if self._child_class is None:
        raise ValueError('Child class is not defined.')
    child_obj = self._child_class(*args, **kwargs)
    child_obj.datablock_id = self.datablock_id  # Setting the datablock_id to update its child parameters
    child_obj.entry_id = child_obj.entry_id  # Forcing the entry_id to be reset to update its child parameters
    self._items[child_obj._entry_id] = child_obj

    # Call on_item_added if it exists, i.e. defined in the derived class
    if hasattr(self, 'on_item_added'):
        self.on_item_added(child_obj)

Component

Bases: ABC

Base class for standard components, like Cell, Peak, etc.

Source code in src/easydiffraction/core/objects.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
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
class Component(ABC):
    """
    Base class for standard components, like Cell, Peak, etc.
    """

    @property
    @abstractmethod
    def category_key(self):
        """
        Must be implemented in subclasses to return the ED category name.
        Can differ from cif_category_key.
        """
        pass

    @property
    @abstractmethod
    def cif_category_key(self):
        """
        Must be implemented in subclasses to return the CIF category name.
        """
        pass

    def __init__(self):
        self._locked = False  # If adding new attributes is locked

        self._datablock_id = None  # Parent datablock name to be set by the parent
        self._entry_id = None  # Parent collection entry id to be set by the parent

        # TODO: Currently, it is not used. Planned to be used for displaying
        #  the parameters in the specific order.
        self._ordered_attrs: List[str] = []

    def __getattr__(self, name: str) -> Any:
        """
        If the attribute is a Parameter or Descriptor, return its value by default
        """
        attr = self.__dict__.get(name, None)
        if isinstance(attr, (Descriptor, Parameter)):
            return attr.value
        raise AttributeError(f'{name} not found in {self}')

    def __setattr__(self, name: str, value: Any) -> None:
        """
        If an object is locked for adding new attributes, raise an error.
        If the attribute 'name' does not exist, add it.
        If the attribute 'name' exists and is a Parameter or Descriptor, set its value.
        """
        if hasattr(self, '_locked') and self._locked:
            if not hasattr(self, name):
                print(error(f"Cannot add new parameter '{name}'"))
                return

        # Try to get the attribute from the instance's dictionary
        attr = self.__dict__.get(name, None)

        # If the attribute is not set, and it is a Parameter or Descriptor,
        # set its category_key and cif_category_key to the current category_key
        # and cif_category_key and add it to the component.
        # Also add its name to the list of ordered attributes
        if attr is None:
            if isinstance(value, (Descriptor, Parameter)):
                value.category_key = self.category_key
                value.cif_category_key = self.cif_category_key
                self._ordered_attrs.append(name)
            super().__setattr__(name, value)
        # If the attribute is already set and is a Parameter or Descriptor,
        # update its value. Else, allow normal reassignment
        else:
            if isinstance(attr, (Descriptor, Parameter)):
                attr.value = value
            else:
                super().__setattr__(name, value)

    @property
    def datablock_id(self):
        return self._datablock_id

    @datablock_id.setter
    def datablock_id(self, new_id):
        self._datablock_id = new_id
        # For each parameter in this component, also update its datablock_id
        for param in self.get_all_params():
            param.datablock_id = new_id

    @property
    def entry_id(self):
        return self._entry_id

    @entry_id.setter
    def entry_id(self, new_id):
        self._entry_id = new_id
        # For each parameter in the component, set the entry_id
        for param in self.get_all_params():
            param.collection_entry_id = new_id

    def get_all_params(self):
        attr_objs = []
        for attr_name in dir(self):
            attr_obj = getattr(self, attr_name)
            if isinstance(attr_obj, (Descriptor, Parameter)):
                attr_objs.append(attr_obj)
        return attr_objs

    def as_dict(self) -> Dict[str, Any]:
        d = {}

        for attr_name in dir(self):
            if attr_name.startswith('_'):
                continue

            attr_obj = getattr(self, attr_name)
            if not isinstance(attr_obj, (Descriptor, Parameter)):
                continue

            key = attr_obj.cif_name
            value = attr_obj.value
            d[key] = value

        return d

    def as_cif(self) -> str:
        if not self.cif_category_key:
            raise ValueError('cif_category_key must be defined in the derived class.')

        lines = []

        for attr_name in dir(self):
            if attr_name.startswith('_'):
                continue

            attr_obj = getattr(self, attr_name)
            if not isinstance(attr_obj, (Descriptor, Parameter)):
                continue

            key = f'_{self.cif_category_key}.{attr_obj.cif_name}'
            value = attr_obj.value

            if value is None:
                continue

            if isinstance(value, str) and ' ' in value:
                value = f'"{value}"'

            line = f'{key}  {value}'
            lines.append(line)

        return '\n'.join(lines)

__getattr__(name)

If the attribute is a Parameter or Descriptor, return its value by default

Source code in src/easydiffraction/core/objects.py
239
240
241
242
243
244
245
246
def __getattr__(self, name: str) -> Any:
    """
    If the attribute is a Parameter or Descriptor, return its value by default
    """
    attr = self.__dict__.get(name, None)
    if isinstance(attr, (Descriptor, Parameter)):
        return attr.value
    raise AttributeError(f'{name} not found in {self}')

__setattr__(name, value)

If an object is locked for adding new attributes, raise an error. If the attribute 'name' does not exist, add it. If the attribute 'name' exists and is a Parameter or Descriptor, set its value.

Source code in src/easydiffraction/core/objects.py
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
def __setattr__(self, name: str, value: Any) -> None:
    """
    If an object is locked for adding new attributes, raise an error.
    If the attribute 'name' does not exist, add it.
    If the attribute 'name' exists and is a Parameter or Descriptor, set its value.
    """
    if hasattr(self, '_locked') and self._locked:
        if not hasattr(self, name):
            print(error(f"Cannot add new parameter '{name}'"))
            return

    # Try to get the attribute from the instance's dictionary
    attr = self.__dict__.get(name, None)

    # If the attribute is not set, and it is a Parameter or Descriptor,
    # set its category_key and cif_category_key to the current category_key
    # and cif_category_key and add it to the component.
    # Also add its name to the list of ordered attributes
    if attr is None:
        if isinstance(value, (Descriptor, Parameter)):
            value.category_key = self.category_key
            value.cif_category_key = self.cif_category_key
            self._ordered_attrs.append(name)
        super().__setattr__(name, value)
    # If the attribute is already set and is a Parameter or Descriptor,
    # update its value. Else, allow normal reassignment
    else:
        if isinstance(attr, (Descriptor, Parameter)):
            attr.value = value
        else:
            super().__setattr__(name, value)

category_key abstractmethod property

Must be implemented in subclasses to return the ED category name. Can differ from cif_category_key.

cif_category_key abstractmethod property

Must be implemented in subclasses to return the CIF category name.

Datablock

Bases: ABC

Base class for Sample Model and Experiment data blocks.

Source code in src/easydiffraction/core/objects.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
class Datablock(ABC):
    """
    Base class for Sample Model and Experiment data blocks.
    """

    # TODO: Consider unifying with class Component?

    def __init__(self):
        self._name = None

    def __setattr__(self, name, value):
        # TODO: compare with class Component
        # If the value is a Component or Collection:
        # - set its datablock_id to the current datablock name
        # - add it to the datablock
        if isinstance(value, (Component, Collection)):
            value.datablock_id = self._name
        super().__setattr__(name, value)

    def items(self):
        """
        Returns a list of both components and collections in the
        data block.
        """
        attr_objs = []
        for attr_name in dir(self):
            if attr_name.startswith('_'):
                continue
            attr_obj = getattr(self, attr_name)
            if isinstance(attr_obj, (Component, Collection)):
                attr_objs.append(attr_obj)
        return attr_objs

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, new_name):
        self._name = new_name
        # For each component/collection in this datablock,
        # also update its datablock_id
        for item in self.items():
            item.datablock_id = new_name

items()

Returns a list of both components and collections in the data block.

Source code in src/easydiffraction/core/objects.py
487
488
489
490
491
492
493
494
495
496
497
498
499
def items(self):
    """
    Returns a list of both components and collections in the
    data block.
    """
    attr_objs = []
    for attr_name in dir(self):
        if attr_name.startswith('_'):
            continue
        attr_obj = getattr(self, attr_name)
        if isinstance(attr_obj, (Component, Collection)):
            attr_objs.append(attr_obj)
    return attr_objs

Descriptor

Base class for descriptors (non-refinable attributes).

Source code in src/easydiffraction/core/objects.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
class Descriptor:
    """
    Base class for descriptors (non-refinable attributes).
    """

    def __init__(
        self,
        value: Any,  # Value of the parameter
        name: str,  # ED parameter name (to access it in the code)
        cif_name: str,  # CIF parameter name (to show it in the CIF)
        pretty_name: Optional[str] = None,  # Pretty name (to show it in the table)
        datablock_id: Optional[str] = None,  # Parent datablock name
        category_key: Optional[str] = None,  # ED parent category name
        cif_category_key: Optional[str] = None,  # CIF parent category name
        collection_entry_id: Optional[str] = None,  # Parent collection entry id
        units: Optional[str] = None,  # Units of the parameter
        description: Optional[str] = None,  # Description of the parameter
        editable: bool = True,  # If false, the parameter can never be edited. It is calculated automatically
    ) -> None:
        self._value = value
        self.name: str = name
        self.cif_name: str = cif_name
        self.pretty_name: Optional[str] = pretty_name
        self._datablock_id: Optional[str] = datablock_id
        self.category_key: Optional[str] = category_key
        self.cif_category_key: Optional[str] = cif_category_key
        self._collection_entry_id: Optional[str] = collection_entry_id
        self.units: Optional[str] = units
        self._description: Optional[str] = description
        self._editable: bool = editable

        self._human_uid = self._generate_human_readable_unique_id()

        UidMapHandler.get().add_to_uid_map(self)

    def __str__(self):
        # Base value string
        value_str = f'{self.__class__.__name__}: {self.uid} = {self.value}'

        # Append ± uncertainty if it exists and is nonzero
        if hasattr(self, 'uncertainty') and getattr(self, 'uncertainty') != 0.0:
            value_str += f' ± {self.uncertainty}'

        # Append units if available
        if self.units:
            value_str += f' {self.units}'

        return value_str

    def __repr__(self):
        return self.__str__()

    def _generate_random_unique_id(self) -> str:
        # Derived class Parameter will use this unique id for the
        # minimization process to identify the parameter. It will also be
        # used to create the alias for the parameter in the constraint
        # expression.
        length = 16
        letters = [secrets.choice(string.ascii_lowercase) for _ in range(length)]
        uid = ''.join(letters)
        return uid

    def _generate_human_readable_unique_id(self):
        # Instead of generating a random string, we can use the
        # name of the parameter and the block name to create a unique id.
        #  E.g.:
        #  - "block-id.category-name.parameter-name": "lbco.cell.length_a"
        #  - "block-id.category-name.entry-id.parameter-name": "lbco.atom_site.Ba.fract_x"
        # For the analysis, we can use the same format, but without the
        # datablock id. E.g.:
        #  - "category-name.entry-id.parameter-name": "alias.occ_Ba.label"
        # This need to be called after the parameter is created and all its
        # attributes are set.
        if self.datablock_id:
            uid = f'{self.datablock_id}.{self.cif_category_key}'
        else:
            uid = f'{self.cif_category_key}'
        if self.collection_entry_id:
            uid += f'.{self.collection_entry_id}'
        uid += f'.{self.cif_name}'
        return uid

    @property
    def datablock_id(self):
        return self._datablock_id

    @datablock_id.setter
    def datablock_id(self, new_id):
        self._datablock_id = new_id
        # Update the unique id, when datablock_id attribute is of
        # the parameter is changed
        self.uid = self._generate_human_readable_unique_id()

    @property
    def collection_entry_id(self):
        return self._collection_entry_id

    @collection_entry_id.setter
    def collection_entry_id(self, new_id):
        self._collection_entry_id = new_id
        # Update the unique id, when datablock_id attribute is of
        # the parameter is changed
        self.uid = self._generate_human_readable_unique_id()

    @property
    def uid(self):
        return self._human_uid

    @uid.setter
    def uid(self, new_uid):
        # Update the unique id in the global uid map
        old_uid = self._human_uid
        self._human_uid = new_uid
        UidMapHandler.get().replace_uid(old_uid, new_uid)

    @property
    def minimizer_uid(self):
        return self.uid.replace('.', '__')

    @property
    def value(self) -> Any:
        return self._value

    @value.setter
    def value(self, new_value: Any) -> None:
        if self._editable:
            self._value = new_value
        else:
            print(warning(f"The parameter '{self.cif_name}' it is calculated automatically and cannot be changed manually."))

    @property
    def description(self) -> Optional[str]:
        return self._description

    @property
    def editable(self) -> bool:
        return self._editable

Parameter

Bases: Descriptor

A parameter with a value, uncertainty, units, and CIF representation.

Source code in src/easydiffraction/core/objects.py
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
class Parameter(Descriptor):
    """
    A parameter with a value, uncertainty, units, and CIF representation.
    """

    def __init__(
        self,
        value: Any,
        name: str,
        cif_name: str,
        pretty_name: Optional[str] = None,
        datablock_id: Optional[str] = None,  # Parent datablock name
        category_key: Optional[str] = None,
        cif_category_key: Optional[str] = None,
        collection_entry_id: Optional[str] = None,
        units: Optional[str] = None,
        description: Optional[str] = None,
        editable: bool = True,
        uncertainty: float = 0.0,
        free: bool = False,
        constrained: bool = False,
        min_value: Optional[float] = None,
        max_value: Optional[float] = None,
    ) -> None:
        super().__init__(
            value,
            name,
            cif_name,
            pretty_name,
            datablock_id,
            category_key,
            cif_category_key,
            collection_entry_id,
            units,
            description,
            editable,
        )
        self.uncertainty: float = uncertainty  # Standard uncertainty or estimated standard deviation
        self.free: bool = free  # If the parameter is free to be fitted during the optimization
        self.constrained: bool = constrained  # If symmetry constrains the parameter during the optimization
        self.min: Optional[float] = min_value  # Minimum physical value of the parameter
        self.max: Optional[float] = max_value  # Maximum physical value of the parameter
        self.start_value: Optional[Any] = None  # Starting value for optimization

singletons

BaseSingleton

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/singletons.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class BaseSingleton:
    """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:
        """Returns the shared instance, creating it if needed."""
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

get() classmethod

Returns the shared instance, creating it if needed.

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

ConstraintsHandler

Bases: BaseSingleton

Manages user-defined 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/singletons.py
 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
class ConstraintsHandler(BaseSingleton):
    """Manages user-defined 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):
        """
        Sets 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 = aliases._items

    def set_constraints(self, constraints):
        """
        Sets 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:
        """
        Converts raw expression input into a normalized internal list of
        (lhs_alias, rhs_expr) pairs, stripping whitespace and skipping invalid entries.
        """
        self._parsed_constraints = []

        for expr_obj in self._constraints.values():
            lhs_alias = expr_obj.lhs_alias.value
            rhs_expr = expr_obj.rhs_expr.value

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

    def apply(self) -> None:
        """Evaluates 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.value = rhs_value
                param.constrained = True

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

apply()

Evaluates 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/singletons.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def apply(self) -> None:
    """Evaluates 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.value = rhs_value
            param.constrained = True

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

set_aliases(aliases)

Sets 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/singletons.py
82
83
84
85
86
87
88
def set_aliases(self, aliases):
    """
    Sets 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 = aliases._items

set_constraints(constraints)

Sets 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/singletons.py
90
91
92
93
94
95
96
97
def set_constraints(self, constraints):
    """
    Sets 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()

UidMapHandler

Bases: BaseSingleton

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

Source code in src/easydiffraction/core/singletons.py
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
class UidMapHandler(BaseSingleton):
    """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]:
        """Returns the current UID-to-Parameter map."""
        return self._uid_map

    def add_to_uid_map(self, parameter):
        """Adds a single Parameter object to the UID map."""
        self._uid_map[parameter.uid] = parameter

    def replace_uid(self, old_uid, new_uid):
        """Replaces 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 in self._uid_map:
            self._uid_map[new_uid] = self._uid_map.pop(old_uid)
        else:
            raise KeyError(f"UID '{old_uid}' not found in the UID map.")

add_to_uid_map(parameter)

Adds a single Parameter object to the UID map.

Source code in src/easydiffraction/core/singletons.py
44
45
46
def add_to_uid_map(self, parameter):
    """Adds a single Parameter object to the UID map."""
    self._uid_map[parameter.uid] = parameter

get_uid_map()

Returns the current UID-to-Parameter map.

Source code in src/easydiffraction/core/singletons.py
40
41
42
def get_uid_map(self) -> Dict[str, Any]:
    """Returns the current UID-to-Parameter map."""
    return self._uid_map

replace_uid(old_uid, new_uid)

Replaces 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/singletons.py
48
49
50
51
52
53
54
55
56
57
def replace_uid(self, old_uid, new_uid):
    """Replaces 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 in self._uid_map:
        self._uid_map[new_uid] = self._uid_map.pop(old_uid)
    else:
        raise KeyError(f"UID '{old_uid}' not found in the UID map.")