Skip to content

structure

categories

atom_site_aniso

default

Anisotropic ADP category.

Defines :class:AtomSiteAniso items and :class:AtomSiteAnisoCollection used alongside :class:AtomSites to hold anisotropic displacement parameters.

AtomSiteAniso

Bases: CategoryItem

Single atom site anisotropic ADP entry.

Each entry mirrors an :class:AtomSite by label and holds six tensor components whose physical meaning (B or U) is determined by atom_site.adp_type.

Source code in src/easydiffraction/datablocks/structure/categories/atom_site_aniso/default.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 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 AtomSiteAniso(CategoryItem):
    """
    Single atom site anisotropic ADP entry.

    Each entry mirrors an :class:`AtomSite` by label and holds six
    tensor components whose physical meaning (B or U) is determined by
    ``atom_site.adp_type``.
    """

    def __init__(self) -> None:
        """Initialise with default zero-valued tensor components."""
        super().__init__()

        self._label = StringDescriptor(
            name='label',
            description='Atom-site label matching the parent atom_site entry.',
            value_spec=AttributeSpec(default=''),
            cif_handler=CifHandler(names=['_atom_site_aniso.label']),
        )

        self._adp_11 = Parameter(
            name='adp_11',
            description='Anisotropic ADP tensor component (1,1).',
            units='Ų',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0.0, le=10.0),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site_aniso.B_11',
                    '_atom_site_aniso.U_11',
                ]
            ),
        )
        self._adp_22 = Parameter(
            name='adp_22',
            description='Anisotropic ADP tensor component (2,2).',
            units='Ų',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0.0, le=10.0),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site_aniso.B_22',
                    '_atom_site_aniso.U_22',
                ]
            ),
        )
        self._adp_33 = Parameter(
            name='adp_33',
            description='Anisotropic ADP tensor component (3,3).',
            units='Ų',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0.0, le=10.0),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site_aniso.B_33',
                    '_atom_site_aniso.U_33',
                ]
            ),
        )
        self._adp_12 = Parameter(
            name='adp_12',
            description='Anisotropic ADP tensor component (1,2).',
            units='Ų',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site_aniso.B_12',
                    '_atom_site_aniso.U_12',
                ]
            ),
        )
        self._adp_13 = Parameter(
            name='adp_13',
            description='Anisotropic ADP tensor component (1,3).',
            units='Ų',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site_aniso.B_13',
                    '_atom_site_aniso.U_13',
                ]
            ),
        )
        self._adp_23 = Parameter(
            name='adp_23',
            description='Anisotropic ADP tensor component (2,3).',
            units='Ų',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site_aniso.B_23',
                    '_atom_site_aniso.U_23',
                ]
            ),
        )

        self._identity.category_code = 'atom_site_aniso'
        self._identity.category_entry_name = lambda: str(self.label.value)

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

    @property
    def label(self) -> StringDescriptor:
        """Label matching the parent atom_site entry."""
        return self._label

    @label.setter
    def label(self, value: str) -> None:
        self._label.value = value

    @property
    def adp_11(self) -> Parameter:
        """Anisotropic ADP tensor component (1,1) in Ų."""
        return self._adp_11

    @adp_11.setter
    def adp_11(self, value: float) -> None:
        self._adp_11.value = value

    @property
    def adp_22(self) -> Parameter:
        """Anisotropic ADP tensor component (2,2) in Ų."""
        return self._adp_22

    @adp_22.setter
    def adp_22(self, value: float) -> None:
        self._adp_22.value = value

    @property
    def adp_33(self) -> Parameter:
        """Anisotropic ADP tensor component (3,3) in Ų."""
        return self._adp_33

    @adp_33.setter
    def adp_33(self, value: float) -> None:
        self._adp_33.value = value

    @property
    def adp_12(self) -> Parameter:
        """Anisotropic ADP tensor component (1,2) in Ų."""
        return self._adp_12

    @adp_12.setter
    def adp_12(self, value: float) -> None:
        self._adp_12.value = value

    @property
    def adp_13(self) -> Parameter:
        """Anisotropic ADP tensor component (1,3) in Ų."""
        return self._adp_13

    @adp_13.setter
    def adp_13(self, value: float) -> None:
        self._adp_13.value = value

    @property
    def adp_23(self) -> Parameter:
        """Anisotropic ADP tensor component (2,3) in Ų."""
        return self._adp_23

    @adp_23.setter
    def adp_23(self, value: float) -> None:
        self._adp_23.value = value
__init__()

Initialise with default zero-valued tensor components.

Source code in src/easydiffraction/datablocks/structure/categories/atom_site_aniso/default.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def __init__(self) -> None:
    """Initialise with default zero-valued tensor components."""
    super().__init__()

    self._label = StringDescriptor(
        name='label',
        description='Atom-site label matching the parent atom_site entry.',
        value_spec=AttributeSpec(default=''),
        cif_handler=CifHandler(names=['_atom_site_aniso.label']),
    )

    self._adp_11 = Parameter(
        name='adp_11',
        description='Anisotropic ADP tensor component (1,1).',
        units='Ų',
        value_spec=AttributeSpec(
            default=0.0,
            validator=RangeValidator(ge=0.0, le=10.0),
        ),
        cif_handler=CifHandler(
            names=[
                '_atom_site_aniso.B_11',
                '_atom_site_aniso.U_11',
            ]
        ),
    )
    self._adp_22 = Parameter(
        name='adp_22',
        description='Anisotropic ADP tensor component (2,2).',
        units='Ų',
        value_spec=AttributeSpec(
            default=0.0,
            validator=RangeValidator(ge=0.0, le=10.0),
        ),
        cif_handler=CifHandler(
            names=[
                '_atom_site_aniso.B_22',
                '_atom_site_aniso.U_22',
            ]
        ),
    )
    self._adp_33 = Parameter(
        name='adp_33',
        description='Anisotropic ADP tensor component (3,3).',
        units='Ų',
        value_spec=AttributeSpec(
            default=0.0,
            validator=RangeValidator(ge=0.0, le=10.0),
        ),
        cif_handler=CifHandler(
            names=[
                '_atom_site_aniso.B_33',
                '_atom_site_aniso.U_33',
            ]
        ),
    )
    self._adp_12 = Parameter(
        name='adp_12',
        description='Anisotropic ADP tensor component (1,2).',
        units='Ų',
        value_spec=AttributeSpec(
            default=0.0,
            validator=RangeValidator(),
        ),
        cif_handler=CifHandler(
            names=[
                '_atom_site_aniso.B_12',
                '_atom_site_aniso.U_12',
            ]
        ),
    )
    self._adp_13 = Parameter(
        name='adp_13',
        description='Anisotropic ADP tensor component (1,3).',
        units='Ų',
        value_spec=AttributeSpec(
            default=0.0,
            validator=RangeValidator(),
        ),
        cif_handler=CifHandler(
            names=[
                '_atom_site_aniso.B_13',
                '_atom_site_aniso.U_13',
            ]
        ),
    )
    self._adp_23 = Parameter(
        name='adp_23',
        description='Anisotropic ADP tensor component (2,3).',
        units='Ų',
        value_spec=AttributeSpec(
            default=0.0,
            validator=RangeValidator(),
        ),
        cif_handler=CifHandler(
            names=[
                '_atom_site_aniso.B_23',
                '_atom_site_aniso.U_23',
            ]
        ),
    )

    self._identity.category_code = 'atom_site_aniso'
    self._identity.category_entry_name = lambda: str(self.label.value)
adp_11 property writable

Anisotropic ADP tensor component (1,1) in Ų.

adp_12 property writable

Anisotropic ADP tensor component (1,2) in Ų.

adp_13 property writable

Anisotropic ADP tensor component (1,3) in Ų.

adp_22 property writable

Anisotropic ADP tensor component (2,2) in Ų.

adp_23 property writable

Anisotropic ADP tensor component (2,3) in Ų.

adp_33 property writable

Anisotropic ADP tensor component (3,3) in Ų.

label property writable

Label matching the parent atom_site entry.

AtomSiteAnisoCollection

Bases: CategoryCollection

Collection of :class:AtomSiteAniso instances.

Source code in src/easydiffraction/datablocks/structure/categories/atom_site_aniso/default.py
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
@AtomSiteAnisoFactory.register
class AtomSiteAnisoCollection(CategoryCollection):
    """Collection of :class:`AtomSiteAniso` instances."""

    type_info = TypeInfo(
        tag='default',
        description='Anisotropic ADP collection',
    )

    def __init__(self) -> None:
        """Initialise an empty aniso-ADP collection."""
        super().__init__(item_type=AtomSiteAniso)

    def _skip_cif_serialization(self) -> bool:
        """
        Return ``True`` when no atoms use an anisotropic ADP type.

        Returns
        -------
        bool
            ``True`` if CIF output should be suppressed.
        """
        structure = getattr(self, '_parent', None)
        if structure is None:
            return True
        atom_sites = getattr(structure, '_atom_sites', None)
        if atom_sites is None:
            return True
        from easydiffraction.datablocks.structure.categories.atom_sites.enums import (  # noqa: PLC0415
            AdpTypeEnum,
        )

        aniso_types = {AdpTypeEnum.BANI.value, AdpTypeEnum.UANI.value}
        return not any(atom.adp_type.value in aniso_types for atom in atom_sites)

    def _iso_labels(self) -> set[str]:
        """Return labels of atoms with isotropic ADP type."""
        structure = getattr(self, '_parent', None)
        if structure is None:
            return set()
        atom_sites = getattr(structure, '_atom_sites', None)
        if atom_sites is None:
            return set()
        from easydiffraction.datablocks.structure.categories.atom_sites.enums import (  # noqa: PLC0415
            AdpTypeEnum,
        )

        iso_types = {AdpTypeEnum.BISO.value, AdpTypeEnum.UISO.value}
        return {atom.label.value for atom in atom_sites if atom.adp_type.value in iso_types}

    def _format_cif_row(self, item: object) -> list[str] | None:
        """
        Return ``?`` markers for isotropic atoms, ``None`` otherwise.

        Used by :func:`category_collection_to_cif` as a per-row hook.
        Atoms whose ADP type is isotropic get ``?`` for the six tensor
        components so they are not mistaken for genuine zeros.

        Parameters
        ----------
        item : object
            An :class:`AtomSiteAniso` instance.

        Returns
        -------
        list[str] | None
            Formatted row when overridden, ``None`` for default output.
        """
        if item.label.value not in self._iso_labels():
            return None
        from easydiffraction.io.cif.serialize import format_param_value  # noqa: PLC0415
        from easydiffraction.io.cif.serialize import format_value  # noqa: PLC0415

        row = [format_param_value(item._label)]
        row.extend([format_value(None)] * 6)
        return row
__init__()

Initialise an empty aniso-ADP collection.

Source code in src/easydiffraction/datablocks/structure/categories/atom_site_aniso/default.py
217
218
219
def __init__(self) -> None:
    """Initialise an empty aniso-ADP collection."""
    super().__init__(item_type=AtomSiteAniso)

factory

Atom-site-aniso factory — delegates entirely to FactoryBase.

AtomSiteAnisoFactory

Bases: FactoryBase

Create atom-site-aniso collections by tag.

Source code in src/easydiffraction/datablocks/structure/categories/atom_site_aniso/factory.py
12
13
14
15
16
17
class AtomSiteAnisoFactory(FactoryBase):
    """Create atom-site-aniso collections by tag."""

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

atom_sites

default

Atom site category.

Defines :class:AtomSite items and :class:AtomSites collection used in crystallographic structures.

AtomSite

Bases: CategoryItem

Single atom site with fractional coordinates and ADP.

Attributes are represented by descriptors to support validation and CIF serialization.

Source code in src/easydiffraction/datablocks/structure/categories/atom_sites/default.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
class AtomSite(CategoryItem):
    """
    Single atom site with fractional coordinates and ADP.

    Attributes are represented by descriptors to support validation and
    CIF serialization.
    """

    def __init__(self) -> None:
        """Initialise the atom site with default descriptor values."""
        super().__init__()

        self._label = StringDescriptor(
            name='label',
            description='Unique identifier for the atom site.',
            value_spec=AttributeSpec(
                default='Si',
                # TODO: the following pattern is valid for dict key
                #  (keywords are not checked). CIF label is less strict.
                #  Do we need conversion between CIF and internal label?
                validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'),
            ),
            cif_handler=CifHandler(names=['_atom_site.label']),
        )
        self._type_symbol = StringDescriptor(
            name='type_symbol',
            description='Chemical symbol of the atom at this site.',
            value_spec=AttributeSpec(
                default='Tb',
                validator=MembershipValidator(allowed=self._type_symbol_allowed_values),
            ),
            cif_handler=CifHandler(names=['_atom_site.type_symbol']),
        )
        self._fract_x = Parameter(
            name='fract_x',
            description='Fractional x-coordinate of the atom site within the unit cell.',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_atom_site.fract_x']),
        )
        self._fract_y = Parameter(
            name='fract_y',
            description='Fractional y-coordinate of the atom site within the unit cell.',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_atom_site.fract_y']),
        )
        self._fract_z = Parameter(
            name='fract_z',
            description='Fractional z-coordinate of the atom site within the unit cell.',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(),
            ),
            cif_handler=CifHandler(names=['_atom_site.fract_z']),
        )
        self._wyckoff_letter = StringDescriptor(
            name='wyckoff_letter',
            description='Wyckoff letter indicating the symmetry of the '
            'atom site within the space group.',
            value_spec=AttributeSpec(
                default=self._wyckoff_letter_default_value,
                validator=MembershipValidator(allowed=self._wyckoff_letter_allowed_values),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site.Wyckoff_letter',
                    '_atom_site.Wyckoff_symbol',
                ]
            ),
        )
        self._occupancy = Parameter(
            name='occupancy',
            description='Occupancy of the atom site, representing the '
            'fraction of the site occupied by the atom type.',
            value_spec=AttributeSpec(
                default=1.0,
                validator=RangeValidator(ge=0.0, le=1.0),
            ),
            cif_handler=CifHandler(names=['_atom_site.occupancy']),
        )
        self._adp_iso = Parameter(
            name='adp_iso',
            description='Isotropic atomic displacement parameter (ADP) for the atom site.',
            units='Ų',
            value_spec=AttributeSpec(
                default=0.0,
                validator=RangeValidator(ge=0.0, le=10.0),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site.B_iso_or_equiv',
                    '_atom_site.U_iso_or_equiv',
                ]
            ),
        )
        self._adp_type = StringDescriptor(
            name='adp_type',
            description='Type of atomic displacement parameter (ADP) '
            'used (e.g., Biso, Uiso, Uani, Bani).',
            value_spec=AttributeSpec(
                default=AdpTypeEnum.default(),
                validator=MembershipValidator(allowed=[m.value for m in AdpTypeEnum]),
            ),
            cif_handler=CifHandler(names=['_atom_site.adp_type']),
        )

        self._identity.category_code = 'atom_site'
        self._identity.category_entry_name = lambda: str(self.label.value)

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

    @property
    def _type_symbol_allowed_values(self) -> list[str]:
        """
        Return chemical symbols accepted by *cryspy*.

        Returns
        -------
        list[str]
            Unique element/isotope symbols from the database.
        """
        return list({key[1] for key in DATABASE['Isotopes']})

    @property
    def _wyckoff_letter_allowed_values(self) -> list[str]:
        """
        Return allowed Wyckoff-letter symbols.

        Returns
        -------
        list[str]
            Currently a hard-coded placeholder list.
        """
        # TODO: Need to now current space group. How to access it? Via
        #  parent Cell? Then letters =
        #  list(SPACE_GROUPS[62, 'cab']['Wyckoff_positions'].keys())
        #  Temporarily return hardcoded list:
        return ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']

    @property
    def _wyckoff_letter_default_value(self) -> str:
        """
        Return the default Wyckoff letter.

        Returns
        -------
        str
            First element of the allowed values list.
        """
        # TODO: What to pass as default?
        return self._wyckoff_letter_allowed_values[0]

    def _convert_adp_values(self, old_type: str, new_type: str) -> None:
        """
        Convert ADP values when the type changes.

        Handles B ↔ U conversion using B = 8π²U and iso ↔ ani seeding.

        Parameters
        ----------
        old_type : str
            Previous ADP type value.
        new_type : str
            New ADP type value.
        """
        old_enum = AdpTypeEnum(old_type)
        new_enum = AdpTypeEnum(new_type)
        factor = 8.0 * math.pi**2
        old_is_b = old_enum in {AdpTypeEnum.BISO, AdpTypeEnum.BANI}
        new_is_u = new_enum in {AdpTypeEnum.UISO, AdpTypeEnum.UANI}
        old_is_iso = old_enum in {AdpTypeEnum.BISO, AdpTypeEnum.UISO}
        new_is_iso = new_enum in {AdpTypeEnum.BISO, AdpTypeEnum.UISO}

        # Ani → Iso: collapse tensor to scalar first (in old units)
        if not old_is_iso and new_is_iso:
            self._collapse_aniso_to_iso()

        # B ↔ U conversion for iso value
        if old_is_b and new_is_u:
            self._adp_iso.value /= factor
        elif not old_is_b and not new_is_u:
            self._adp_iso.value *= factor

        # Iso → Ani: seed diagonal from (already converted) adp_iso
        if old_is_iso and not new_is_iso:
            self._seed_aniso_from_iso()
        elif not old_is_iso and not new_is_iso:
            # Ani → Ani (e.g. Bani→Uani): apply B↔U to aniso values
            aniso = self._get_aniso_entry()
            if aniso is not None:
                self._convert_aniso_values(
                    aniso,
                    old_is_b=old_is_b,
                    new_is_u=new_is_u,
                    factor=factor,
                )

    def _seed_aniso_from_iso(self) -> None:
        """Seed aniso diagonal from current adp_iso value."""
        aniso = self._get_aniso_entry()
        if aniso is None:
            # Entry not yet created; force sync on parent
            structure = getattr(self._parent, '_parent', None)
            if structure is not None and hasattr(structure, '_sync_atom_site_aniso'):
                structure._sync_atom_site_aniso()
                aniso = self._get_aniso_entry()
        if aniso is None:
            return
        iso_val = self._adp_iso.value
        aniso.adp_11 = iso_val
        aniso.adp_22 = iso_val
        aniso.adp_33 = iso_val
        aniso.adp_12 = 0.0
        aniso.adp_13 = 0.0
        aniso.adp_23 = 0.0

    def _collapse_aniso_to_iso(self) -> None:
        """
        Set adp_iso to the mean of the aniso diagonal.

        Writes directly to ``_value`` to bypass range validation,
        because intermediate minimizer steps can produce negative
        anisotropic components whose mean falls outside the nominal
        ``[0, 100]`` range.
        """
        aniso = self._get_aniso_entry()
        if aniso is None:
            return
        self._adp_iso._value = (aniso.adp_11.value + aniso.adp_22.value + aniso.adp_33.value) / 3.0

    def _get_aniso_entry(self) -> object | None:
        """Return the matching AtomSiteAniso entry, or None."""
        # _parent is absent before the atom is added to a collection
        if '_parent' not in self.__dict__:
            return None
        structure = getattr(self._parent, '_parent', None)
        if structure is None:
            return None
        aniso_coll = getattr(structure, '_atom_site_aniso', None)
        if aniso_coll is None:
            return None
        lbl = self._label.value
        if lbl in aniso_coll:
            return aniso_coll[lbl]
        return None

    @staticmethod
    def _convert_aniso_values(
        aniso: object,
        *,
        old_is_b: bool,
        new_is_u: bool,
        factor: float,
    ) -> None:
        """Apply B↔U conversion to all six aniso tensor components."""
        if old_is_b and new_is_u:
            for attr in ('adp_11', 'adp_22', 'adp_33', 'adp_12', 'adp_13', 'adp_23'):
                p = getattr(aniso, attr)
                p.value /= factor
        elif not old_is_b and not new_is_u:
            for attr in ('adp_11', 'adp_22', 'adp_33', 'adp_12', 'adp_13', 'adp_23'):
                p = getattr(aniso, attr)
                p.value *= factor

    def _reorder_adp_cif_names(self, new_type: str) -> None:
        """
        Reorder CIF names on adp_iso and aniso params for serialisation.

        Parameters
        ----------
        new_type : str
            The new ADP type value.
        """
        is_u = AdpTypeEnum(new_type) in {AdpTypeEnum.UISO, AdpTypeEnum.UANI}
        if is_u:
            self._adp_iso._cif_handler._names = [
                '_atom_site.U_iso_or_equiv',
                '_atom_site.B_iso_or_equiv',
            ]
        else:
            self._adp_iso._cif_handler._names = [
                '_atom_site.B_iso_or_equiv',
                '_atom_site.U_iso_or_equiv',
            ]

        # Reorder aniso CIF names
        aniso = self._get_aniso_entry()
        if aniso is None:
            return
        for suffix in ('11', '22', '33', '12', '13', '23'):
            param = getattr(aniso, f'_adp_{suffix}')
            if is_u:
                param._cif_handler._names = [
                    f'_atom_site_aniso.U_{suffix}',
                    f'_atom_site_aniso.B_{suffix}',
                ]
            else:
                param._cif_handler._names = [
                    f'_atom_site_aniso.B_{suffix}',
                    f'_atom_site_aniso.U_{suffix}',
                ]

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

    @property
    def label(self) -> StringDescriptor:
        """
        Unique identifier for the atom site.

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

    @label.setter
    def label(self, value: str) -> None:
        self._label.value = value

    @property
    def type_symbol(self) -> StringDescriptor:
        """
        Chemical symbol of the atom at this site.

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

    @type_symbol.setter
    def type_symbol(self, value: str) -> None:
        self._type_symbol.value = value

    @property
    def adp_type(self) -> StringDescriptor:
        """
        ADP type used (e.g., Biso, Uiso, Uani, Bani).

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

    @adp_type.setter
    def adp_type(self, value: str) -> None:
        old_type = self._adp_type.value
        self._adp_type.value = value
        new_type = self._adp_type.value
        if old_type != new_type:
            self._convert_adp_values(old_type, new_type)
            self._reorder_adp_cif_names(new_type)
            parent = getattr(self, '_parent', None)
            if parent is not None:
                parent._propagate_adp_convention(self)

    @property
    def wyckoff_letter(self) -> StringDescriptor:
        """
        Wyckoff letter for the atom site symmetry position.

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

    @wyckoff_letter.setter
    def wyckoff_letter(self, value: str) -> None:
        self._wyckoff_letter.value = value

    @property
    def fract_x(self) -> Parameter:
        """
        Fractional x-coordinate of the atom site within the unit cell.

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

    @fract_x.setter
    def fract_x(self, value: float) -> None:
        self._fract_x.value = value

    @property
    def fract_y(self) -> Parameter:
        """
        Fractional y-coordinate of the atom site within the unit cell.

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

    @fract_y.setter
    def fract_y(self, value: float) -> None:
        self._fract_y.value = value

    @property
    def fract_z(self) -> Parameter:
        """
        Fractional z-coordinate of the atom site within the unit cell.

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

    @fract_z.setter
    def fract_z(self, value: float) -> None:
        self._fract_z.value = value

    @property
    def occupancy(self) -> Parameter:
        """
        Occupancy fraction of the atom type at this site.

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

    @occupancy.setter
    def occupancy(self, value: float) -> None:
        self._occupancy.value = value

    @property
    def adp_iso(self) -> Parameter:
        """
        Isotropic ADP for the atom site (Ų).

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

    @adp_iso.setter
    def adp_iso(self, value: float) -> None:
        self._adp_iso.value = value

    @property
    def adp_iso_as_b(self) -> float:
        """
        Return the isotropic ADP as a B-factor value.

        When ``adp_type`` is ``Uiso`` or ``Uani`` the stored U value is
        converted to B via B = 8π²U.  Otherwise the stored value is
        returned unchanged.

        Returns
        -------
        float
            Equivalent B_iso value.
        """
        if AdpTypeEnum(self._adp_type.value) in {AdpTypeEnum.UISO, AdpTypeEnum.UANI}:
            return self._adp_iso.value * 8.0 * math.pi**2
        return self._adp_iso.value
__init__()

Initialise the atom site with default descriptor values.

Source code in src/easydiffraction/datablocks/structure/categories/atom_sites/default.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
 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
def __init__(self) -> None:
    """Initialise the atom site with default descriptor values."""
    super().__init__()

    self._label = StringDescriptor(
        name='label',
        description='Unique identifier for the atom site.',
        value_spec=AttributeSpec(
            default='Si',
            # TODO: the following pattern is valid for dict key
            #  (keywords are not checked). CIF label is less strict.
            #  Do we need conversion between CIF and internal label?
            validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'),
        ),
        cif_handler=CifHandler(names=['_atom_site.label']),
    )
    self._type_symbol = StringDescriptor(
        name='type_symbol',
        description='Chemical symbol of the atom at this site.',
        value_spec=AttributeSpec(
            default='Tb',
            validator=MembershipValidator(allowed=self._type_symbol_allowed_values),
        ),
        cif_handler=CifHandler(names=['_atom_site.type_symbol']),
    )
    self._fract_x = Parameter(
        name='fract_x',
        description='Fractional x-coordinate of the atom site within the unit cell.',
        value_spec=AttributeSpec(
            default=0.0,
            validator=RangeValidator(),
        ),
        cif_handler=CifHandler(names=['_atom_site.fract_x']),
    )
    self._fract_y = Parameter(
        name='fract_y',
        description='Fractional y-coordinate of the atom site within the unit cell.',
        value_spec=AttributeSpec(
            default=0.0,
            validator=RangeValidator(),
        ),
        cif_handler=CifHandler(names=['_atom_site.fract_y']),
    )
    self._fract_z = Parameter(
        name='fract_z',
        description='Fractional z-coordinate of the atom site within the unit cell.',
        value_spec=AttributeSpec(
            default=0.0,
            validator=RangeValidator(),
        ),
        cif_handler=CifHandler(names=['_atom_site.fract_z']),
    )
    self._wyckoff_letter = StringDescriptor(
        name='wyckoff_letter',
        description='Wyckoff letter indicating the symmetry of the '
        'atom site within the space group.',
        value_spec=AttributeSpec(
            default=self._wyckoff_letter_default_value,
            validator=MembershipValidator(allowed=self._wyckoff_letter_allowed_values),
        ),
        cif_handler=CifHandler(
            names=[
                '_atom_site.Wyckoff_letter',
                '_atom_site.Wyckoff_symbol',
            ]
        ),
    )
    self._occupancy = Parameter(
        name='occupancy',
        description='Occupancy of the atom site, representing the '
        'fraction of the site occupied by the atom type.',
        value_spec=AttributeSpec(
            default=1.0,
            validator=RangeValidator(ge=0.0, le=1.0),
        ),
        cif_handler=CifHandler(names=['_atom_site.occupancy']),
    )
    self._adp_iso = Parameter(
        name='adp_iso',
        description='Isotropic atomic displacement parameter (ADP) for the atom site.',
        units='Ų',
        value_spec=AttributeSpec(
            default=0.0,
            validator=RangeValidator(ge=0.0, le=10.0),
        ),
        cif_handler=CifHandler(
            names=[
                '_atom_site.B_iso_or_equiv',
                '_atom_site.U_iso_or_equiv',
            ]
        ),
    )
    self._adp_type = StringDescriptor(
        name='adp_type',
        description='Type of atomic displacement parameter (ADP) '
        'used (e.g., Biso, Uiso, Uani, Bani).',
        value_spec=AttributeSpec(
            default=AdpTypeEnum.default(),
            validator=MembershipValidator(allowed=[m.value for m in AdpTypeEnum]),
        ),
        cif_handler=CifHandler(names=['_atom_site.adp_type']),
    )

    self._identity.category_code = 'atom_site'
    self._identity.category_entry_name = lambda: str(self.label.value)
adp_iso property writable

Isotropic ADP for the atom site (Ų).

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

adp_iso_as_b property

Return the isotropic ADP as a B-factor value.

When adp_type is Uiso or Uani the stored U value is converted to B via B = 8π²U. Otherwise the stored value is returned unchanged.

Returns:

Type Description
float

Equivalent B_iso value.

adp_type property writable

ADP type used (e.g., Biso, Uiso, Uani, Bani).

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

fract_x property writable

Fractional x-coordinate of the atom site within the unit cell.

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

fract_y property writable

Fractional y-coordinate of the atom site within the unit cell.

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

fract_z property writable

Fractional z-coordinate of the atom site within the unit cell.

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

label property writable

Unique identifier for the atom site.

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

occupancy property writable

Occupancy fraction of the atom type at this site.

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

type_symbol property writable

Chemical symbol of the atom at this site.

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

wyckoff_letter property writable

Wyckoff letter for the atom site symmetry position.

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

AtomSites

Bases: CategoryCollection

Collection of :class:AtomSite instances.

Source code in src/easydiffraction/datablocks/structure/categories/atom_sites/default.py
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
@AtomSitesFactory.register
class AtomSites(CategoryCollection):
    """Collection of :class:`AtomSite` instances."""

    type_info = TypeInfo(
        tag='default',
        description='Atom sites collection',
    )

    def __init__(self) -> None:
        """Initialise an empty atom-sites collection."""
        super().__init__(item_type=AtomSite)

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

    def _propagate_adp_convention(self, source: AtomSite) -> None:
        """
        Align all atoms to the B/U convention of *source*.

        When an atom switches between B and U convention, all siblings
        are converted to the same convention so that CIF loop headers
        remain consistent.

        Parameters
        ----------
        source : AtomSite
            The atom whose convention just changed.
        """
        new_enum = AdpTypeEnum(source._adp_type.value)
        target_is_u = new_enum in {AdpTypeEnum.UISO, AdpTypeEnum.UANI}

        for atom in self._items:
            if atom is source:
                continue
            sib_enum = AdpTypeEnum(atom._adp_type.value)
            sib_is_u = sib_enum in {AdpTypeEnum.UISO, AdpTypeEnum.UANI}
            if sib_is_u == target_is_u:
                continue
            sib_is_iso = sib_enum in {AdpTypeEnum.BISO, AdpTypeEnum.UISO}
            if target_is_u:
                target = AdpTypeEnum.UISO if sib_is_iso else AdpTypeEnum.UANI
            else:
                target = AdpTypeEnum.BISO if sib_is_iso else AdpTypeEnum.BANI
            old_sib = atom._adp_type.value
            atom._adp_type._value = target.value
            atom._convert_adp_values(old_sib, target.value)
            atom._reorder_adp_cif_names(target.value)

    def _apply_atomic_coordinates_symmetry_constraints(self) -> None:
        """
        Apply symmetry rules to fractional coordinates of every site.

        Uses the parent structure's space-group symbol, IT coordinate
        system code and each atom's Wyckoff letter.  Atoms without a
        Wyckoff letter are silently skipped.
        """
        structure = self._parent
        space_group_name = structure.space_group.name_h_m.value
        space_group_coord_code = structure.space_group.it_coordinate_system_code.value
        for atom in self._items:
            dummy_atom = {
                'fract_x': atom.fract_x.value,
                'fract_y': atom.fract_y.value,
                'fract_z': atom.fract_z.value,
            }
            wl = atom.wyckoff_letter.value
            if not wl:
                # TODO: Decide how to handle this case
                continue
            ecr.apply_atom_site_symmetry_constraints(
                atom_site=dummy_atom,
                name_hm=space_group_name,
                coord_code=space_group_coord_code,
                wyckoff_letter=wl,
            )
            atom.fract_x.value = dummy_atom['fract_x']
            atom.fract_y.value = dummy_atom['fract_y']
            atom.fract_z.value = dummy_atom['fract_z']

    def _apply_adp_symmetry_constraints(self) -> None:
        """
        Apply symmetry rules to anisotropic ADP tensor components.

        For each atom with an anisotropic ADP type and a Wyckoff letter,
        enforces the tensor constraints dictated by the site symmetry.
        Also sets ``free = False`` on tensor components that are fixed
        by symmetry and on ``adp_iso`` for all anisotropic atoms.
        """
        structure = self._parent
        aniso_types = {AdpTypeEnum.BANI.value, AdpTypeEnum.UANI.value}
        space_group_name = structure.space_group.name_h_m.value
        space_group_coord_code = structure.space_group.it_coordinate_system_code.value
        aniso_collection = structure.atom_site_aniso

        for atom in self._items:
            if atom.adp_type.value not in aniso_types:
                continue
            # Isotropic ADP is not refinable for aniso atoms
            atom._adp_iso.free = False
            wl = atom.wyckoff_letter.value
            if not wl:
                continue
            lbl = atom.label.value
            if lbl not in aniso_collection:
                continue
            aniso_entry = aniso_collection[lbl]
            dummy = {
                'adp_11': aniso_entry.adp_11.value,
                'adp_22': aniso_entry.adp_22.value,
                'adp_33': aniso_entry.adp_33.value,
                'adp_12': aniso_entry.adp_12.value,
                'adp_13': aniso_entry.adp_13.value,
                'adp_23': aniso_entry.adp_23.value,
            }
            site_fract = (
                atom.fract_x.value,
                atom.fract_y.value,
                atom.fract_z.value,
            )
            dummy, ref_i = ecr.apply_atom_site_aniso_symmetry_constraints(
                atom_site_aniso=dummy,
                name_hm=space_group_name,
                coord_code=space_group_coord_code,
                _wyckoff_letter=wl,
                site_fract=site_fract,
            )
            adp_keys = ('adp_11', 'adp_22', 'adp_33', 'adp_12', 'adp_13', 'adp_23')
            for key, is_free in zip(adp_keys, ref_i, strict=False):
                param = getattr(aniso_entry, key)
                param.value = dummy[key]
                if not is_free:
                    param.free = False

    def _sync_iso_from_aniso(self) -> None:
        """
        Update ``adp_iso`` from the anisotropic tensor for aniso atoms.

        For every atom whose ADP type is anisotropic (Bani / Uani), sets
        ``adp_iso`` to the mean of the three diagonal tensor components
        so that the isotropic value stays consistent with the current
        tensor state.
        """
        aniso_types = {AdpTypeEnum.BANI.value, AdpTypeEnum.UANI.value}
        for atom in self._items:
            if atom.adp_type.value in aniso_types:
                atom._collapse_aniso_to_iso()

    def _update(
        self,
        *,
        called_by_minimizer: bool = False,
    ) -> None:
        """
        Recalculate atom sites after a change.

        Parameters
        ----------
        called_by_minimizer : bool, default=False
            Whether the update was triggered by the fitting minimizer.
            Currently unused.
        """
        del called_by_minimizer

        self._apply_atomic_coordinates_symmetry_constraints()
        self._apply_adp_symmetry_constraints()
        self._sync_iso_from_aniso()
__init__()

Initialise an empty atom-sites collection.

Source code in src/easydiffraction/datablocks/structure/categories/atom_sites/default.py
510
511
512
def __init__(self) -> None:
    """Initialise an empty atom-sites collection."""
    super().__init__(item_type=AtomSite)

enums

Enumeration for ADP type values.

AdpTypeEnum

Bases: StrEnum

Atomic displacement parameter type.

Source code in src/easydiffraction/datablocks/structure/categories/atom_sites/enums.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class AdpTypeEnum(StrEnum):
    """Atomic displacement parameter type."""

    BISO = 'Biso'
    UISO = 'Uiso'
    BANI = 'Bani'
    UANI = 'Uani'

    @classmethod
    def default(cls) -> AdpTypeEnum:
        """Return the default ADP type (BISO)."""
        return cls.BISO

    def description(self) -> str:
        """Return a human-readable description of this ADP type."""
        descriptions = {
            AdpTypeEnum.BISO: 'Isotropic B-factor (Debye-Waller)',
            AdpTypeEnum.UISO: 'Isotropic mean-square displacement',
            AdpTypeEnum.BANI: 'Anisotropic B-factor tensor',
            AdpTypeEnum.UANI: 'Anisotropic mean-square displacement tensor',
        }
        return descriptions[self]
default() classmethod

Return the default ADP type (BISO).

Source code in src/easydiffraction/datablocks/structure/categories/atom_sites/enums.py
18
19
20
21
@classmethod
def default(cls) -> AdpTypeEnum:
    """Return the default ADP type (BISO)."""
    return cls.BISO
description()

Return a human-readable description of this ADP type.

Source code in src/easydiffraction/datablocks/structure/categories/atom_sites/enums.py
23
24
25
26
27
28
29
30
31
def description(self) -> str:
    """Return a human-readable description of this ADP type."""
    descriptions = {
        AdpTypeEnum.BISO: 'Isotropic B-factor (Debye-Waller)',
        AdpTypeEnum.UISO: 'Isotropic mean-square displacement',
        AdpTypeEnum.BANI: 'Anisotropic B-factor tensor',
        AdpTypeEnum.UANI: 'Anisotropic mean-square displacement tensor',
    }
    return descriptions[self]

factory

Atom-sites factory — delegates entirely to FactoryBase.

AtomSitesFactory

Bases: FactoryBase

Create atom-sites collections by tag.

Source code in src/easydiffraction/datablocks/structure/categories/atom_sites/factory.py
12
13
14
15
16
17
class AtomSitesFactory(FactoryBase):
    """Create atom-sites collections by tag."""

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

cell

default

Unit cell parameters category for structures.

Cell

Bases: CategoryItem

Unit cell with lengths a, b, c and angles alpha, beta, gamma.

All six lattice parameters are exposed as :class:Parameter descriptors supporting validation, fitting and CIF serialization.

Source code in src/easydiffraction/datablocks/structure/categories/cell/default.py
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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
@CellFactory.register
class Cell(CategoryItem):
    """
    Unit cell with lengths a, b, c and angles alpha, beta, gamma.

    All six lattice parameters are exposed as :class:`Parameter`
    descriptors supporting validation, fitting and CIF serialization.
    """

    type_info = TypeInfo(
        tag='default',
        description='Unit cell parameters',
    )

    def __init__(self) -> None:
        """Initialise the unit cell with default parameter values."""
        super().__init__()

        self._length_a = Parameter(
            name='length_a',
            description='Length of the a axis of the unit cell',
            units='Å',
            value_spec=AttributeSpec(
                default=10.0,
                validator=RangeValidator(ge=0, le=30),
            ),
            cif_handler=CifHandler(names=['_cell.length_a']),
        )
        self._length_b = Parameter(
            name='length_b',
            description='Length of the b axis of the unit cell',
            units='Å',
            value_spec=AttributeSpec(
                default=10.0,
                validator=RangeValidator(ge=0, le=30),
            ),
            cif_handler=CifHandler(names=['_cell.length_b']),
        )
        self._length_c = Parameter(
            name='length_c',
            description='Length of the c axis of the unit cell',
            units='Å',
            value_spec=AttributeSpec(
                default=10.0,
                validator=RangeValidator(ge=0, le=30),
            ),
            cif_handler=CifHandler(names=['_cell.length_c']),
        )
        self._angle_alpha = Parameter(
            name='angle_alpha',
            description='Angle between edges b and c',
            units='deg',
            value_spec=AttributeSpec(
                default=90.0,
                validator=RangeValidator(ge=0, le=180),
            ),
            cif_handler=CifHandler(names=['_cell.angle_alpha']),
        )
        self._angle_beta = Parameter(
            name='angle_beta',
            description='Angle between edges a and c',
            units='deg',
            value_spec=AttributeSpec(
                default=90.0,
                validator=RangeValidator(ge=0, le=180),
            ),
            cif_handler=CifHandler(names=['_cell.angle_beta']),
        )
        self._angle_gamma = Parameter(
            name='angle_gamma',
            description='Angle between edges a and b',
            units='deg',
            value_spec=AttributeSpec(
                default=90.0,
                validator=RangeValidator(ge=0, le=180),
            ),
            cif_handler=CifHandler(names=['_cell.angle_gamma']),
        )

        self._identity.category_code = 'cell'

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

    def _apply_cell_symmetry_constraints(self) -> None:
        """
        Apply symmetry constraints to cell parameters in place.

        Uses the parent structure's space-group symbol to determine
        which lattice parameters are dependent and sets them
        accordingly.
        """
        dummy_cell = {
            'lattice_a': self.length_a.value,
            'lattice_b': self.length_b.value,
            'lattice_c': self.length_c.value,
            'angle_alpha': self.angle_alpha.value,
            'angle_beta': self.angle_beta.value,
            'angle_gamma': self.angle_gamma.value,
        }
        space_group_name = self._parent.space_group.name_h_m.value

        ecr.apply_cell_symmetry_constraints(
            cell=dummy_cell,
            name_hm=space_group_name,
        )

        self.length_a.value = dummy_cell['lattice_a']
        self.length_b.value = dummy_cell['lattice_b']
        self.length_c.value = dummy_cell['lattice_c']
        self.angle_alpha.value = dummy_cell['angle_alpha']
        self.angle_beta.value = dummy_cell['angle_beta']
        self.angle_gamma.value = dummy_cell['angle_gamma']

    def _update(
        self,
        *,
        called_by_minimizer: bool = False,
    ) -> None:
        """
        Recalculate cell parameters after a change.

        Parameters
        ----------
        called_by_minimizer : bool, default=False
            Whether the update was triggered by the fitting minimizer.
            Currently unused.
        """
        del called_by_minimizer  # TODO: ???

        self._apply_cell_symmetry_constraints()

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

    @property
    def length_a(self) -> Parameter:
        """
        Length of the a axis of the unit cell (Å).

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

    @length_a.setter
    def length_a(self, value: float) -> None:
        self._length_a.value = value

    @property
    def length_b(self) -> Parameter:
        """
        Length of the b axis of the unit cell (Å).

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

    @length_b.setter
    def length_b(self, value: float) -> None:
        self._length_b.value = value

    @property
    def length_c(self) -> Parameter:
        """
        Length of the c axis of the unit cell (Å).

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

    @length_c.setter
    def length_c(self, value: float) -> None:
        self._length_c.value = value

    @property
    def angle_alpha(self) -> Parameter:
        """
        Angle between edges b and c (deg).

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

    @angle_alpha.setter
    def angle_alpha(self, value: float) -> None:
        self._angle_alpha.value = value

    @property
    def angle_beta(self) -> Parameter:
        """
        Angle between edges a and c (deg).

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

    @angle_beta.setter
    def angle_beta(self, value: float) -> None:
        self._angle_beta.value = value

    @property
    def angle_gamma(self) -> Parameter:
        """
        Angle between edges a and b (deg).

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

    @angle_gamma.setter
    def angle_gamma(self, value: float) -> None:
        self._angle_gamma.value = value
__init__()

Initialise the unit cell with default parameter values.

Source code in src/easydiffraction/datablocks/structure/categories/cell/default.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def __init__(self) -> None:
    """Initialise the unit cell with default parameter values."""
    super().__init__()

    self._length_a = Parameter(
        name='length_a',
        description='Length of the a axis of the unit cell',
        units='Å',
        value_spec=AttributeSpec(
            default=10.0,
            validator=RangeValidator(ge=0, le=30),
        ),
        cif_handler=CifHandler(names=['_cell.length_a']),
    )
    self._length_b = Parameter(
        name='length_b',
        description='Length of the b axis of the unit cell',
        units='Å',
        value_spec=AttributeSpec(
            default=10.0,
            validator=RangeValidator(ge=0, le=30),
        ),
        cif_handler=CifHandler(names=['_cell.length_b']),
    )
    self._length_c = Parameter(
        name='length_c',
        description='Length of the c axis of the unit cell',
        units='Å',
        value_spec=AttributeSpec(
            default=10.0,
            validator=RangeValidator(ge=0, le=30),
        ),
        cif_handler=CifHandler(names=['_cell.length_c']),
    )
    self._angle_alpha = Parameter(
        name='angle_alpha',
        description='Angle between edges b and c',
        units='deg',
        value_spec=AttributeSpec(
            default=90.0,
            validator=RangeValidator(ge=0, le=180),
        ),
        cif_handler=CifHandler(names=['_cell.angle_alpha']),
    )
    self._angle_beta = Parameter(
        name='angle_beta',
        description='Angle between edges a and c',
        units='deg',
        value_spec=AttributeSpec(
            default=90.0,
            validator=RangeValidator(ge=0, le=180),
        ),
        cif_handler=CifHandler(names=['_cell.angle_beta']),
    )
    self._angle_gamma = Parameter(
        name='angle_gamma',
        description='Angle between edges a and b',
        units='deg',
        value_spec=AttributeSpec(
            default=90.0,
            validator=RangeValidator(ge=0, le=180),
        ),
        cif_handler=CifHandler(names=['_cell.angle_gamma']),
    )

    self._identity.category_code = 'cell'
angle_alpha property writable

Angle between edges b and c (deg).

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

angle_beta property writable

Angle between edges a and c (deg).

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

angle_gamma property writable

Angle between edges a and b (deg).

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

length_a property writable

Length of the a axis of the unit cell (Å).

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

length_b property writable

Length of the b axis of the unit cell (Å).

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

length_c property writable

Length of the c axis of the unit cell (Å).

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

factory

Cell factory — delegates entirely to FactoryBase.

CellFactory

Bases: FactoryBase

Create unit-cell categories by tag.

Source code in src/easydiffraction/datablocks/structure/categories/cell/factory.py
12
13
14
15
16
17
class CellFactory(FactoryBase):
    """Create unit-cell categories by tag."""

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

space_group

default

Space group category for crystallographic structures.

SpaceGroup

Bases: CategoryItem

Space group with H-M symbol and IT coordinate system code.

Holds the space-group symbol (name_h_m) and the International Tables coordinate-system qualifier (it_coordinate_system_code). Changing the symbol automatically resets the coordinate-system code to the first allowed value for the new group.

Source code in src/easydiffraction/datablocks/structure/categories/space_group/default.py
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 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
@SpaceGroupFactory.register
class SpaceGroup(CategoryItem):
    """
    Space group with H-M symbol and IT coordinate system code.

    Holds the space-group symbol (``name_h_m``) and the International
    Tables coordinate-system qualifier (``it_coordinate_system_code``).
    Changing the symbol automatically resets the coordinate-system code
    to the first allowed value for the new group.
    """

    type_info = TypeInfo(
        tag='default',
        description='Space group symmetry',
    )

    def __init__(self) -> None:
        """Initialise the space group with default values."""
        super().__init__()

        self._name_h_m = StringDescriptor(
            name='name_h_m',
            description='Hermann-Mauguin symbol of the space group.',
            value_spec=AttributeSpec(
                default='P 1',
                validator=MembershipValidator(
                    allowed=lambda: self._name_h_m_allowed_values,
                ),
            ),
            cif_handler=CifHandler(
                # TODO: Keep only version with "." and automate ...
                names=[
                    '_space_group.name_H-M_alt',
                    '_space_group_name_H-M_alt',
                    '_symmetry.space_group_name_H-M',
                    '_symmetry_space_group_name_H-M',
                ]
            ),
        )
        self._it_coordinate_system_code = StringDescriptor(
            name='it_coordinate_system_code',
            description='A qualifier identifying which setting in IT is used.',
            value_spec=AttributeSpec(
                default=lambda: self._it_coordinate_system_code_default_value,
                validator=MembershipValidator(
                    allowed=lambda: self._it_coordinate_system_code_allowed_values
                ),
            ),
            cif_handler=CifHandler(
                names=[
                    '_space_group.IT_coordinate_system_code',
                    '_space_group_IT_coordinate_system_code',
                    '_symmetry.IT_coordinate_system_code',
                    '_symmetry_IT_coordinate_system_code',
                ]
            ),
        )

        self._identity.category_code = 'space_group'

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

    def _reset_it_coordinate_system_code(self) -> None:
        """Reset IT coordinate system code to default for this group."""
        self._it_coordinate_system_code.value = self._it_coordinate_system_code_default_value

    @property
    def _name_h_m_allowed_values(self) -> list[str]:
        """
        Return the list of recognised Hermann-Mauguin short symbols.

        Returns
        -------
        list[str]
            All short H-M symbols known to *cryspy*.
        """
        return ACCESIBLE_NAME_HM_SHORT

    @property
    def _it_coordinate_system_code_allowed_values(self) -> list[str]:
        """
        Return allowed IT coordinate system codes for the current group.

        Returns
        -------
        list[str]
            Coordinate-system codes, or ``['']`` when none are defined.
        """
        name = self.name_h_m.value
        it_number = get_it_number_by_name_hm_short(name)
        codes = get_it_coordinate_system_codes_by_it_number(it_number)
        codes = [str(code) for code in codes]
        return codes or ['']

    @property
    def _it_coordinate_system_code_default_value(self) -> str:
        """
        Return the default IT coordinate system code.

        Returns
        -------
        str
            First element of the allowed codes list.
        """
        return self._it_coordinate_system_code_allowed_values[0]

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

    @property
    def name_h_m(self) -> StringDescriptor:
        """
        Hermann-Mauguin symbol of the space group.

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

    @name_h_m.setter
    def name_h_m(self, value: str) -> None:
        self._name_h_m.value = value
        self._reset_it_coordinate_system_code()

    @property
    def it_coordinate_system_code(self) -> StringDescriptor:
        """
        A qualifier identifying which setting in IT is used.

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

    @it_coordinate_system_code.setter
    def it_coordinate_system_code(self, value: str) -> None:
        self._it_coordinate_system_code.value = value
__init__()

Initialise the space group with default values.

Source code in src/easydiffraction/datablocks/structure/categories/space_group/default.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def __init__(self) -> None:
    """Initialise the space group with default values."""
    super().__init__()

    self._name_h_m = StringDescriptor(
        name='name_h_m',
        description='Hermann-Mauguin symbol of the space group.',
        value_spec=AttributeSpec(
            default='P 1',
            validator=MembershipValidator(
                allowed=lambda: self._name_h_m_allowed_values,
            ),
        ),
        cif_handler=CifHandler(
            # TODO: Keep only version with "." and automate ...
            names=[
                '_space_group.name_H-M_alt',
                '_space_group_name_H-M_alt',
                '_symmetry.space_group_name_H-M',
                '_symmetry_space_group_name_H-M',
            ]
        ),
    )
    self._it_coordinate_system_code = StringDescriptor(
        name='it_coordinate_system_code',
        description='A qualifier identifying which setting in IT is used.',
        value_spec=AttributeSpec(
            default=lambda: self._it_coordinate_system_code_default_value,
            validator=MembershipValidator(
                allowed=lambda: self._it_coordinate_system_code_allowed_values
            ),
        ),
        cif_handler=CifHandler(
            names=[
                '_space_group.IT_coordinate_system_code',
                '_space_group_IT_coordinate_system_code',
                '_symmetry.IT_coordinate_system_code',
                '_symmetry_IT_coordinate_system_code',
            ]
        ),
    )

    self._identity.category_code = 'space_group'
it_coordinate_system_code property writable

A qualifier identifying which setting in IT is used.

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

name_h_m property writable

Hermann-Mauguin symbol of the space group.

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

factory

Space-group factory — delegates entirely to FactoryBase.

SpaceGroupFactory

Bases: FactoryBase

Create space-group categories by tag.

Source code in src/easydiffraction/datablocks/structure/categories/space_group/factory.py
12
13
14
15
16
17
class SpaceGroupFactory(FactoryBase):
    """Create space-group categories by tag."""

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

collection

Collection of structure data blocks.

Structures

Bases: DatablockCollection

Ordered collection of :class:Structure instances.

Provides convenience add_from_* methods that mirror the :class:StructureFactory classmethods plus a bare :meth:add for inserting pre-built structures.

Source code in src/easydiffraction/datablocks/structure/collection.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
class Structures(DatablockCollection):
    """
    Ordered collection of :class:`Structure` instances.

    Provides convenience ``add_from_*`` methods that mirror the
    :class:`StructureFactory` classmethods plus a bare :meth:`add` for
    inserting pre-built structures.
    """

    def __init__(self) -> None:
        """Initialise an empty structures collection."""
        super().__init__(item_type=Structure)

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

    # TODO: Make abstract in DatablockCollection?
    @typechecked
    def create(
        self,
        *,
        name: str,
    ) -> None:
        """
        Create a minimal structure and add it to the collection.

        Parameters
        ----------
        name : str
            Identifier for the new structure.
        """
        structure = StructureFactory.from_scratch(name=name)
        self.add(structure)

    # TODO: Move to DatablockCollection?
    @typechecked
    def add_from_cif_str(
        self,
        cif_str: str,
    ) -> None:
        """
        Create a structure from CIF content and add it.

        Parameters
        ----------
        cif_str : str
            CIF file content as a string.
        """
        structure = StructureFactory.from_cif_str(cif_str)
        self.add(structure)

    # TODO: Move to DatablockCollection?
    @typechecked
    def add_from_cif_path(
        self,
        cif_path: str,
    ) -> None:
        """
        Create a structure from a CIF file and add it.

        Parameters
        ----------
        cif_path : str
            Filesystem path to a CIF file.
        """
        structure = StructureFactory.from_cif_path(cif_path)
        self.add(structure)

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

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

__init__()

Initialise an empty structures collection.

Source code in src/easydiffraction/datablocks/structure/collection.py
22
23
24
def __init__(self) -> None:
    """Initialise an empty structures collection."""
    super().__init__(item_type=Structure)

add_from_cif_path(cif_path)

Create a structure from a CIF file and add it.

Parameters:

Name Type Description Default
cif_path str

Filesystem path to a CIF file.

required
Source code in src/easydiffraction/datablocks/structure/collection.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
@typechecked
def add_from_cif_path(
    self,
    cif_path: str,
) -> None:
    """
    Create a structure from a CIF file and add it.

    Parameters
    ----------
    cif_path : str
        Filesystem path to a CIF file.
    """
    structure = StructureFactory.from_cif_path(cif_path)
    self.add(structure)

add_from_cif_str(cif_str)

Create a structure from CIF content and add it.

Parameters:

Name Type Description Default
cif_str str

CIF file content as a string.

required
Source code in src/easydiffraction/datablocks/structure/collection.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@typechecked
def add_from_cif_str(
    self,
    cif_str: str,
) -> None:
    """
    Create a structure from CIF content and add it.

    Parameters
    ----------
    cif_str : str
        CIF file content as a string.
    """
    structure = StructureFactory.from_cif_str(cif_str)
    self.add(structure)

create(*, name)

Create a minimal structure and add it to the collection.

Parameters:

Name Type Description Default
name str

Identifier for the new structure.

required
Source code in src/easydiffraction/datablocks/structure/collection.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@typechecked
def create(
    self,
    *,
    name: str,
) -> None:
    """
    Create a minimal structure and add it to the collection.

    Parameters
    ----------
    name : str
        Identifier for the new structure.
    """
    structure = StructureFactory.from_scratch(name=name)
    self.add(structure)

show_names()

List all structure names in the collection.

Source code in src/easydiffraction/datablocks/structure/collection.py
83
84
85
86
def show_names(self) -> None:
    """List all structure names in the collection."""
    console.paragraph('Defined structures' + ' 🧩')
    console.print(self.names)

show_params()

Show parameters of all structures in the collection.

Source code in src/easydiffraction/datablocks/structure/collection.py
89
90
91
92
def show_params(self) -> None:
    """Show parameters of all structures in the collection."""
    for structure in self.values():
        structure.show_params()

item

base

Structure datablock item.

Structure

Bases: DatablockItem

Structure datablock item.

Source code in src/easydiffraction/datablocks/structure/item/base.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
class Structure(DatablockItem):
    """Structure datablock item."""

    def __init__(
        self,
        *,
        name: str,
    ) -> None:
        super().__init__()
        self._name = name
        self._cell_type: str = CellFactory.default_tag()
        self._cell = CellFactory.create(self._cell_type)
        self._space_group_type: str = SpaceGroupFactory.default_tag()
        self._space_group = SpaceGroupFactory.create(self._space_group_type)
        self._atom_sites_type: str = AtomSitesFactory.default_tag()
        self._atom_sites = AtomSitesFactory.create(self._atom_sites_type)
        self._atom_site_aniso_type: str = AtomSiteAnisoFactory.default_tag()
        self._atom_site_aniso = AtomSiteAnisoFactory.create(self._atom_site_aniso_type)
        self._identity.datablock_entry_name = lambda: self.name

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

    @property
    def name(self) -> str:
        """
        Name identifier for this structure.

        Returns
        -------
        str
            The structure's name.
        """
        return self._name

    @name.setter
    @typechecked
    def name(self, new: str) -> None:
        """
        Set the name identifier for this structure.

        Parameters
        ----------
        new : str
            New name string.
        """
        self._name = new

    # ------------------------------------------------------------------
    #  Cell (read-only, single type)
    # ------------------------------------------------------------------

    @property
    def cell(self) -> Cell:
        """Unit-cell category for this structure."""
        return self._cell

    @cell.setter
    @typechecked
    def cell(self, new: Cell) -> None:
        """
        Replace the unit-cell category for this structure.

        Parameters
        ----------
        new : Cell
            New unit-cell instance.
        """
        self._cell = new

    # ------------------------------------------------------------------
    #  Space group (read-only, single type)
    # ------------------------------------------------------------------

    @property
    def space_group(self) -> SpaceGroup:
        """Space-group category for this structure."""
        return self._space_group

    @space_group.setter
    @typechecked
    def space_group(self, new: SpaceGroup) -> None:
        """
        Replace the space-group category for this structure.

        Parameters
        ----------
        new : SpaceGroup
            New space-group instance.
        """
        self._space_group = new

    # ------------------------------------------------------------------
    #  Atom sites (read-only, single type)
    # ------------------------------------------------------------------

    @property
    def atom_sites(self) -> AtomSites:
        """Atom-sites collection for this structure."""
        return self._atom_sites

    @atom_sites.setter
    @typechecked
    def atom_sites(self, new: AtomSites) -> None:
        """
        Replace the atom-sites collection for this structure.

        Parameters
        ----------
        new : AtomSites
            New atom-sites collection.
        """
        self._atom_sites = new

    # ------------------------------------------------------------------
    #  Atom site aniso (read-only, single type)
    # ------------------------------------------------------------------

    @property
    def atom_site_aniso(self) -> AtomSiteAnisoCollection:
        """Anisotropic-ADP collection for this structure."""
        return self._atom_site_aniso

    @atom_site_aniso.setter
    @typechecked
    def atom_site_aniso(self, new: AtomSiteAnisoCollection) -> None:
        """
        Replace the anisotropic-ADP collection for this structure.

        Parameters
        ----------
        new : AtomSiteAnisoCollection
            New aniso collection.
        """
        self._atom_site_aniso = new

    # ------------------------------------------------------------------
    # Private methods
    # ------------------------------------------------------------------

    def _sync_atom_site_aniso(self) -> None:
        """
        Reconcile ``atom_site_aniso`` with ``atom_sites``.

        Ensures every atom in ``atom_sites`` has a matching entry in
        ``atom_site_aniso`` and removes stale entries whose label no
        longer appears in ``atom_sites``.  Reorders CIF names on aniso
        parameters to match each atom's ``adp_type``.
        """
        existing_labels = {a.label.value for a in self._atom_sites}
        aniso_labels = {a.label.value for a in self._atom_site_aniso}

        # Add missing entries
        for atom in self._atom_sites:
            lbl = atom.label.value
            if lbl not in aniso_labels:
                entry = AtomSiteAniso()
                entry.label = lbl
                self._atom_site_aniso.add(entry)

        # Remove stale entries
        stale = [
            a.label.value for a in self._atom_site_aniso if a.label.value not in existing_labels
        ]
        for lbl in stale:
            self._atom_site_aniso.remove(lbl)

        # Reorder CIF names to match each atom's adp_type
        for atom in self._atom_sites:
            atom._reorder_adp_cif_names(atom.adp_type.value)

    def _update_categories(
        self,
        *,
        called_by_minimizer: bool = False,
    ) -> None:
        """Update categories with atom_site_aniso sync."""
        if not called_by_minimizer and not self._need_categories_update:
            return

        self._sync_atom_site_aniso()

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

        self._need_categories_update = False

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

    def show(self) -> None:
        """Display an ASCII projection of the structure in 2D."""
        console.paragraph(f"Structure 🧩 '{self.name}'")
        console.print('Not implemented yet.')

    def show_as_cif(self) -> None:
        """Render the CIF text for this structure in the terminal."""
        console.paragraph(f"Structure 🧩 '{self.name}' as cif")
        render_cif(self._cif_for_display())
atom_site_aniso property writable

Anisotropic-ADP collection for this structure.

atom_sites property writable

Atom-sites collection for this structure.

cell property writable

Unit-cell category for this structure.

name property writable

Name identifier for this structure.

Returns:

Type Description
str

The structure's name.

show()

Display an ASCII projection of the structure in 2D.

Source code in src/easydiffraction/datablocks/structure/item/base.py
215
216
217
218
def show(self) -> None:
    """Display an ASCII projection of the structure in 2D."""
    console.paragraph(f"Structure 🧩 '{self.name}'")
    console.print('Not implemented yet.')
show_as_cif()

Render the CIF text for this structure in the terminal.

Source code in src/easydiffraction/datablocks/structure/item/base.py
220
221
222
223
def show_as_cif(self) -> None:
    """Render the CIF text for this structure in the terminal."""
    console.paragraph(f"Structure 🧩 '{self.name}' as cif")
    render_cif(self._cif_for_display())
space_group property writable

Space-group category for this structure.

factory

Factory for creating structure instances from various inputs.

Provides individual class methods for each creation pathway: from_scratch, from_cif_path, or from_cif_str.

StructureFactory

Create :class:Structure instances from supported inputs.

Source code in src/easydiffraction/datablocks/structure/item/factory.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
class StructureFactory:
    """Create :class:`Structure` instances from supported inputs."""

    def __init__(self) -> None:
        log.error(
            'Structure objects must be created using class methods such as '
            '`StructureFactory.from_cif_str(...)`, etc.'
        )

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

    @classmethod
    # TODO: @typechecked fails to find gemmi?
    def _from_gemmi_block(
        cls,
        block: gemmi.cif.Block,
    ) -> Structure:
        """
        Build a structure from a single *gemmi* CIF block.

        Parameters
        ----------
        block : gemmi.cif.Block
            Parsed CIF data block.

        Returns
        -------
        Structure
            A fully populated structure instance.
        """
        name = name_from_block(block)
        structure = Structure(name=name)
        for category in structure.categories:
            category.from_cif(block)
        return structure

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

    @classmethod
    @typechecked
    def from_scratch(
        cls,
        *,
        name: str,
    ) -> Structure:
        """
        Create a minimal default structure.

        Parameters
        ----------
        name : str
            Identifier for the new structure.

        Returns
        -------
        Structure
            An empty structure with default categories.
        """
        return Structure(name=name)

    # TODO: add minimal default configuration for missing parameters
    @classmethod
    @typechecked
    def from_cif_str(
        cls,
        cif_str: str,
    ) -> Structure:
        """
        Create a structure by parsing a CIF string.

        Parameters
        ----------
        cif_str : str
            Raw CIF content.

        Returns
        -------
        Structure
            A populated structure instance.
        """
        doc = document_from_string(cif_str)
        block = pick_sole_block(doc)
        return cls._from_gemmi_block(block)

    # TODO: Read content and call self.from_cif_str
    @classmethod
    @typechecked
    def from_cif_path(
        cls,
        cif_path: str,
    ) -> Structure:
        """
        Create a structure by reading and parsing a CIF file.

        Parameters
        ----------
        cif_path : str
            Filesystem path to a CIF file.

        Returns
        -------
        Structure
            A populated structure instance.
        """
        doc = document_from_path(cif_path)
        block = pick_sole_block(doc)
        return cls._from_gemmi_block(block)
from_cif_path(cif_path) classmethod

Create a structure by reading and parsing a CIF file.

Parameters:

Name Type Description Default
cif_path str

Filesystem path to a CIF file.

required

Returns:

Type Description
Structure

A populated structure instance.

Source code in src/easydiffraction/datablocks/structure/item/factory.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
@classmethod
@typechecked
def from_cif_path(
    cls,
    cif_path: str,
) -> Structure:
    """
    Create a structure by reading and parsing a CIF file.

    Parameters
    ----------
    cif_path : str
        Filesystem path to a CIF file.

    Returns
    -------
    Structure
        A populated structure instance.
    """
    doc = document_from_path(cif_path)
    block = pick_sole_block(doc)
    return cls._from_gemmi_block(block)
from_cif_str(cif_str) classmethod

Create a structure by parsing a CIF string.

Parameters:

Name Type Description Default
cif_str str

Raw CIF content.

required

Returns:

Type Description
Structure

A populated structure instance.

Source code in src/easydiffraction/datablocks/structure/item/factory.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
@classmethod
@typechecked
def from_cif_str(
    cls,
    cif_str: str,
) -> Structure:
    """
    Create a structure by parsing a CIF string.

    Parameters
    ----------
    cif_str : str
        Raw CIF content.

    Returns
    -------
    Structure
        A populated structure instance.
    """
    doc = document_from_string(cif_str)
    block = pick_sole_block(doc)
    return cls._from_gemmi_block(block)
from_scratch(*, name) classmethod

Create a minimal default structure.

Parameters:

Name Type Description Default
name str

Identifier for the new structure.

required

Returns:

Type Description
Structure

An empty structure with default categories.

Source code in src/easydiffraction/datablocks/structure/item/factory.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@classmethod
@typechecked
def from_scratch(
    cls,
    *,
    name: str,
) -> Structure:
    """
    Create a minimal default structure.

    Parameters
    ----------
    name : str
        Identifier for the new structure.

    Returns
    -------
    Structure
        An empty structure with default categories.
    """
    return Structure(name=name)