Skip to content

sample_models

categories

atom_sites

Atom site category.

Defines AtomSite items and AtomSites collection used in sample models. Only documentation was added; behavior remains unchanged.

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

        self._label: StringDescriptor = StringDescriptor(
            name='label',
            description='Unique identifier for the atom site.',
            value_spec=AttributeSpec(
                value=label,
                type_=DataTypes.STRING,
                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?
                content_validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site.label',
                ]
            ),
        )
        self._type_symbol: StringDescriptor = StringDescriptor(
            name='type_symbol',
            description='Chemical symbol of the atom at this site.',
            value_spec=AttributeSpec(
                value=type_symbol,
                type_=DataTypes.STRING,
                default='Tb',
                content_validator=MembershipValidator(allowed=self._type_symbol_allowed_values),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site.type_symbol',
                ]
            ),
        )
        self._fract_x: Parameter = Parameter(
            name='fract_x',
            description='Fractional x-coordinate of the atom site within the unit cell.',
            value_spec=AttributeSpec(
                value=fract_x,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site.fract_x',
                ]
            ),
        )
        self._fract_y: Parameter = Parameter(
            name='fract_y',
            description='Fractional y-coordinate of the atom site within the unit cell.',
            value_spec=AttributeSpec(
                value=fract_y,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site.fract_y',
                ]
            ),
        )
        self._fract_z: Parameter = Parameter(
            name='fract_z',
            description='Fractional z-coordinate of the atom site within the unit cell.',
            value_spec=AttributeSpec(
                value=fract_z,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site.fract_z',
                ]
            ),
        )
        self._wyckoff_letter: StringDescriptor = StringDescriptor(
            name='wyckoff_letter',
            description='Wyckoff letter indicating the symmetry of the '
            'atom site within the space group.',
            value_spec=AttributeSpec(
                value=wyckoff_letter,
                type_=DataTypes.STRING,
                default=self._wyckoff_letter_default_value,
                content_validator=MembershipValidator(allowed=self._wyckoff_letter_allowed_values),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site.Wyckoff_letter',
                    '_atom_site.Wyckoff_symbol',
                ]
            ),
        )
        self._occupancy: Parameter = Parameter(
            name='occupancy',
            description='Occupancy of the atom site, representing the '
            'fraction of the site occupied by the atom type.',
            value_spec=AttributeSpec(
                value=occupancy,
                type_=DataTypes.NUMERIC,
                default=1.0,
                content_validator=RangeValidator(),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site.occupancy',
                ]
            ),
        )
        self._b_iso: Parameter = Parameter(
            name='b_iso',
            description='Isotropic atomic displacement parameter (ADP) for the atom site.',
            value_spec=AttributeSpec(
                value=b_iso,
                type_=DataTypes.NUMERIC,
                default=0.0,
                content_validator=RangeValidator(),
            ),
            units='Ų',
            cif_handler=CifHandler(
                names=[
                    '_atom_site.B_iso_or_equiv',
                ]
            ),
        )
        self._adp_type: StringDescriptor = StringDescriptor(
            name='adp_type',
            description='Type of atomic displacement parameter (ADP) '
            'used (e.g., Biso, Uiso, Uani, Bani).',
            value_spec=AttributeSpec(
                value=adp_type,
                type_=DataTypes.STRING,
                default='Biso',
                content_validator=MembershipValidator(allowed=['Biso']),
            ),
            cif_handler=CifHandler(
                names=[
                    '_atom_site.adp_type',
                ]
            ),
        )

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

    @property
    def _type_symbol_allowed_values(self):
        return list({key[1] for key in DATABASE['Isotopes']})

    @property
    def _wyckoff_letter_allowed_values(self):
        # 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):
        # TODO: What to pass as default?
        return self._wyckoff_letter_allowed_values[0]

    @property
    def label(self):
        """Label descriptor for the site (unique key)."""
        return self._label

    @label.setter
    def label(self, value):
        self._label.value = value

    @property
    def type_symbol(self):
        """Chemical symbol descriptor (e.g. 'Si')."""
        return self._type_symbol

    @type_symbol.setter
    def type_symbol(self, value):
        self._type_symbol.value = value

    @property
    def adp_type(self):
        """ADP type descriptor (e.g. 'Biso')."""
        return self._adp_type

    @adp_type.setter
    def adp_type(self, value):
        self._adp_type.value = value

    @property
    def wyckoff_letter(self):
        """Wyckoff letter descriptor (space-group position)."""
        return self._wyckoff_letter

    @wyckoff_letter.setter
    def wyckoff_letter(self, value):
        self._wyckoff_letter.value = value

    @property
    def fract_x(self):
        """Fractional x coordinate descriptor."""
        return self._fract_x

    @fract_x.setter
    def fract_x(self, value):
        self._fract_x.value = value

    @property
    def fract_y(self):
        """Fractional y coordinate descriptor."""
        return self._fract_y

    @fract_y.setter
    def fract_y(self, value):
        self._fract_y.value = value

    @property
    def fract_z(self):
        """Fractional z coordinate descriptor."""
        return self._fract_z

    @fract_z.setter
    def fract_z(self, value):
        self._fract_z.value = value

    @property
    def occupancy(self):
        """Occupancy descriptor (0..1)."""
        return self._occupancy

    @occupancy.setter
    def occupancy(self, value):
        self._occupancy.value = value

    @property
    def b_iso(self):
        """Isotropic ADP descriptor in Ų."""
        return self._b_iso

    @b_iso.setter
    def b_iso(self, value):
        self._b_iso.value = value
adp_type property writable

ADP type descriptor (e.g. 'Biso').

b_iso property writable

Isotropic ADP descriptor in Ų.

fract_x property writable

Fractional x coordinate descriptor.

fract_y property writable

Fractional y coordinate descriptor.

fract_z property writable

Fractional z coordinate descriptor.

label property writable

Label descriptor for the site (unique key).

occupancy property writable

Occupancy descriptor (0..1).

type_symbol property writable

Chemical symbol descriptor (e.g. 'Si').

wyckoff_letter property writable

Wyckoff letter descriptor (space-group position).

AtomSites

Bases: CategoryCollection

Collection of AtomSite instances.

Source code in src/easydiffraction/sample_models/categories/atom_sites.py
291
292
293
294
295
class AtomSites(CategoryCollection):
    """Collection of AtomSite instances."""

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

cell

Unit cell parameters category for sample models.

Cell

Bases: CategoryItem

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

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

    def __init__(
        self,
        *,
        length_a: Optional[int | float] = None,
        length_b: Optional[int | float] = None,
        length_c: Optional[int | float] = None,
        angle_alpha: Optional[int | float] = None,
        angle_beta: Optional[int | float] = None,
        angle_gamma: Optional[int | float] = None,
    ) -> None:
        super().__init__()

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

        self._identity.category_code = 'cell'

    @property
    def length_a(self):
        """Descriptor for a-axis length in Ã…."""
        return self._length_a

    @length_a.setter
    def length_a(self, value):
        self._length_a.value = value

    @property
    def length_b(self):
        """Descriptor for b-axis length in Ã…."""
        return self._length_b

    @length_b.setter
    def length_b(self, value):
        self._length_b.value = value

    @property
    def length_c(self):
        """Descriptor for c-axis length in Ã…."""
        return self._length_c

    @length_c.setter
    def length_c(self, value):
        self._length_c.value = value

    @property
    def angle_alpha(self):
        """Descriptor for angle alpha in degrees."""
        return self._angle_alpha

    @angle_alpha.setter
    def angle_alpha(self, value):
        self._angle_alpha.value = value

    @property
    def angle_beta(self):
        """Descriptor for angle beta in degrees."""
        return self._angle_beta

    @angle_beta.setter
    def angle_beta(self, value):
        self._angle_beta.value = value

    @property
    def angle_gamma(self):
        """Descriptor for angle gamma in degrees."""
        return self._angle_gamma

    @angle_gamma.setter
    def angle_gamma(self, value):
        self._angle_gamma.value = value
angle_alpha property writable

Descriptor for angle alpha in degrees.

angle_beta property writable

Descriptor for angle beta in degrees.

angle_gamma property writable

Descriptor for angle gamma in degrees.

length_a property writable

Descriptor for a-axis length in Ã….

length_b property writable

Descriptor for b-axis length in Ã….

length_c property writable

Descriptor for c-axis length in Ã….

space_group

Space group category for crystallographic sample models.

SpaceGroup

Bases: CategoryItem

Space group with Hermann–Mauguin symbol and IT code.

Source code in src/easydiffraction/sample_models/categories/space_group.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
class SpaceGroup(CategoryItem):
    """Space group with Hermann–Mauguin symbol and IT code."""

    def __init__(
        self,
        *,
        name_h_m: str = None,
        it_coordinate_system_code: str = None,
    ) -> None:
        super().__init__()
        self._name_h_m: StringDescriptor = StringDescriptor(
            name='name_h_m',
            description='Hermann-Mauguin symbol of the space group.',
            value_spec=AttributeSpec(
                value=name_h_m,
                type_=DataTypes.STRING,
                default='P 1',
                content_validator=MembershipValidator(
                    allowed=lambda: self._name_h_m_allowed_values
                ),
            ),
            cif_handler=CifHandler(
                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 = StringDescriptor(
            name='it_coordinate_system_code',
            description='A qualifier identifying which setting in IT is used.',
            value_spec=AttributeSpec(
                value=it_coordinate_system_code,
                type_=DataTypes.STRING,
                default=lambda: self._it_coordinate_system_code_default_value,
                content_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'

    def _reset_it_coordinate_system_code(self):
        self._it_coordinate_system_code.value = self._it_coordinate_system_code_default_value

    @property
    def _name_h_m_allowed_values(self):
        return ACCESIBLE_NAME_HM_SHORT

    @property
    def _it_coordinate_system_code_allowed_values(self):
        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 if codes else ['']

    @property
    def _it_coordinate_system_code_default_value(self):
        return self._it_coordinate_system_code_allowed_values[0]

    @property
    def name_h_m(self):
        """Descriptor for Hermann–Mauguin symbol."""
        return self._name_h_m

    @name_h_m.setter
    def name_h_m(self, value):
        self._name_h_m.value = value
        self._reset_it_coordinate_system_code()

    @property
    def it_coordinate_system_code(self):
        """Descriptor for IT coordinate system code."""
        return self._it_coordinate_system_code

    @it_coordinate_system_code.setter
    def it_coordinate_system_code(self, value):
        self._it_coordinate_system_code.value = value
it_coordinate_system_code property writable

Descriptor for IT coordinate system code.

name_h_m property writable

Descriptor for Hermann–Mauguin symbol.

sample_model

base

SampleModelBase

Bases: DatablockItem

Base sample model and container for structural information.

Holds space group, unit cell and atom-site categories. The factory is responsible for creating rich instances from CIF; this base accepts just the name and exposes helpers for applying symmetry.

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

    Holds space group, unit cell and atom-site categories. The
    factory is responsible for creating rich instances from CIF;
    this base accepts just the ``name`` and exposes helpers for
    applying symmetry.
    """

    def __init__(
        self,
        *,
        name,
    ) -> None:
        super().__init__()
        self._name = name
        self._cell: Cell = Cell()
        self._space_group: SpaceGroup = SpaceGroup()
        self._atom_sites: AtomSites = AtomSites()
        self._identity.datablock_entry_name = lambda: self.name

    def __str__(self) -> str:
        """Human-readable representation of this component."""
        name = self._log_name
        items = ', '.join(
            f'{k}={v}'
            for k, v in {
                'cell': self.cell,
                'space_group': self.space_group,
                'atom_sites': self.atom_sites,
            }.items()
        )
        return f'<{name} ({items})>'

    @property
    def name(self) -> str:
        """Model name.

        Returns:
            The user-facing identifier for this model.
        """
        return self._name

    @name.setter
    def name(self, new: str) -> None:
        """Update model name."""
        self._name = new

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

    @cell.setter
    def cell(self, new: Cell) -> None:
        """Replace the unit-cell category object."""
        self._cell = new

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

    @space_group.setter
    def space_group(self, new: SpaceGroup) -> None:
        """Replace the space-group category object."""
        self._space_group = new

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

    @atom_sites.setter
    def atom_sites(self, new: AtomSites) -> None:
        """Replace the atom-sites collection."""
        self._atom_sites = new

    # --------------------
    # Symmetry constraints
    # --------------------

    def _apply_cell_symmetry_constraints(self):
        """Apply symmetry rules to unit-cell parameters in place."""
        dummy_cell = {
            'lattice_a': self.cell.length_a.value,
            'lattice_b': self.cell.length_b.value,
            'lattice_c': self.cell.length_c.value,
            'angle_alpha': self.cell.angle_alpha.value,
            'angle_beta': self.cell.angle_beta.value,
            'angle_gamma': self.cell.angle_gamma.value,
        }
        space_group_name = self.space_group.name_h_m.value
        ecr.apply_cell_symmetry_constraints(cell=dummy_cell, name_hm=space_group_name)
        self.cell.length_a.value = dummy_cell['lattice_a']
        self.cell.length_b.value = dummy_cell['lattice_b']
        self.cell.length_c.value = dummy_cell['lattice_c']
        self.cell.angle_alpha.value = dummy_cell['angle_alpha']
        self.cell.angle_beta.value = dummy_cell['angle_beta']
        self.cell.angle_gamma.value = dummy_cell['angle_gamma']

    def _apply_atomic_coordinates_symmetry_constraints(self):
        """Apply symmetry rules to fractional coordinates of atom
        sites.
        """
        space_group_name = self.space_group.name_h_m.value
        space_group_coord_code = self.space_group.it_coordinate_system_code.value
        for atom in self.atom_sites:
            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
                #  For now, we just skip applying constraints if wyckoff
                #  letter is not set. Alternatively, could raise an
                #  error or warning
                #  print(f"Warning: Wyckoff letter is not ...")
                #  raise ValueError("Wyckoff letter is not ...")
                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_atomic_displacement_symmetry_constraints(self):
        """Placeholder for ADP symmetry constraints (not
        implemented).
        """
        pass

    def apply_symmetry_constraints(self):
        """Apply all available symmetry constraints to this model."""
        self._apply_cell_symmetry_constraints()
        self._apply_atomic_coordinates_symmetry_constraints()
        self._apply_atomic_displacement_symmetry_constraints()

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

    def show_structure(self):
        """Show an ASCII projection of the structure on a 2D plane."""
        console.paragraph(f"Sample model 🧩 '{self.name}' structure view")
        console.print('Not implemented yet.')

    def show_params(self):
        """Display structural parameters (space group, cell, atom
        sites).
        """
        console.print(f'\nSampleModel ID: {self.name}')
        console.print(f'Space group: {self.space_group.name_h_m}')
        console.print(f'Cell parameters: {self.cell.as_dict}')
        console.print('Atom sites:')
        self.atom_sites.show()

    def show_as_cif(self) -> None:
        """Render the CIF text for this model in a terminal-friendly
        view.
        """
        cif_text: str = self.as_cif
        paragraph_title: str = f"Sample model 🧩 '{self.name}' as cif"
        console.paragraph(paragraph_title)
        render_cif(cif_text)
__str__()

Human-readable representation of this component.

Source code in src/easydiffraction/sample_models/sample_model/base.py
34
35
36
37
38
39
40
41
42
43
44
45
def __str__(self) -> str:
    """Human-readable representation of this component."""
    name = self._log_name
    items = ', '.join(
        f'{k}={v}'
        for k, v in {
            'cell': self.cell,
            'space_group': self.space_group,
            'atom_sites': self.atom_sites,
        }.items()
    )
    return f'<{name} ({items})>'
apply_symmetry_constraints()

Apply all available symmetry constraints to this model.

Source code in src/easydiffraction/sample_models/sample_model/base.py
151
152
153
154
155
def apply_symmetry_constraints(self):
    """Apply all available symmetry constraints to this model."""
    self._apply_cell_symmetry_constraints()
    self._apply_atomic_coordinates_symmetry_constraints()
    self._apply_atomic_displacement_symmetry_constraints()
atom_sites property writable

Atom-sites collection for this model.

cell property writable

Unit-cell category object.

name property writable

Model name.

Returns:

Type Description
str

The user-facing identifier for this model.

show_as_cif()

Render the CIF text for this model in a terminal-friendly view.

Source code in src/easydiffraction/sample_models/sample_model/base.py
176
177
178
179
180
181
182
183
def show_as_cif(self) -> None:
    """Render the CIF text for this model in a terminal-friendly
    view.
    """
    cif_text: str = self.as_cif
    paragraph_title: str = f"Sample model 🧩 '{self.name}' as cif"
    console.paragraph(paragraph_title)
    render_cif(cif_text)
show_params()

Display structural parameters (space group, cell, atom sites).

Source code in src/easydiffraction/sample_models/sample_model/base.py
166
167
168
169
170
171
172
173
174
def show_params(self):
    """Display structural parameters (space group, cell, atom
    sites).
    """
    console.print(f'\nSampleModel ID: {self.name}')
    console.print(f'Space group: {self.space_group.name_h_m}')
    console.print(f'Cell parameters: {self.cell.as_dict}')
    console.print('Atom sites:')
    self.atom_sites.show()
show_structure()

Show an ASCII projection of the structure on a 2D plane.

Source code in src/easydiffraction/sample_models/sample_model/base.py
161
162
163
164
def show_structure(self):
    """Show an ASCII projection of the structure on a 2D plane."""
    console.paragraph(f"Sample model 🧩 '{self.name}' structure view")
    console.print('Not implemented yet.')
space_group property writable

Space-group category object.

factory

Factory for creating sample models from simple inputs or CIF.

Supports three argument combinations: name, cif_path, or cif_str. Returns a minimal SampleModelBase populated from CIF when provided, or an empty model with the given name.

SampleModelFactory

Bases: FactoryBase

Create SampleModelBase instances from supported inputs.

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

    _ALLOWED_ARG_SPECS = [
        {'required': ['name'], 'optional': []},
        {'required': ['cif_path'], 'optional': []},
        {'required': ['cif_str'], 'optional': []},
    ]

    @classmethod
    def create(cls, **kwargs) -> SampleModelBase:
        """Create a model based on a validated argument combination.

        Keyword Args:
            name: Name of the sample model to create.
            cif_path: Path to a CIF file to parse.
            cif_str: Raw CIF string to parse.
            **kwargs: Extra args are ignored if None; only the above
                three keys are supported.

        Returns:
            SampleModelBase: A populated or empty model instance.
        """
        # Check for valid argument combinations
        user_args = {k for k, v in kwargs.items() if v is not None}
        cls._validate_args(user_args, cls._ALLOWED_ARG_SPECS, cls.__name__)

        if 'cif_path' in kwargs:
            return cls._create_from_cif_path(kwargs['cif_path'])
        elif 'cif_str' in kwargs:
            return cls._create_from_cif_str(kwargs['cif_str'])
        elif 'name' in kwargs:
            return SampleModelBase(name=kwargs['name'])

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

    @classmethod
    def _create_from_cif_path(
        cls,
        cif_path: str,
    ) -> SampleModelBase:
        """Create a model by reading and parsing a CIF file."""
        # Parse CIF and build model
        doc = cls._read_cif_document_from_path(cif_path)
        block = cls._pick_first_structural_block(doc)
        return cls._create_model_from_block(block)

    @classmethod
    def _create_from_cif_str(
        cls,
        cif_str: str,
    ) -> SampleModelBase:
        """Create a model by parsing a CIF string."""
        # Parse CIF string and build model
        doc = cls._read_cif_document_from_string(cif_str)
        block = cls._pick_first_structural_block(doc)
        return cls._create_model_from_block(block)

    # TODO: Move to io.cif.parse?

    # -------------
    # gemmi helpers
    # -------------

    @staticmethod
    def _read_cif_document_from_path(path: str) -> gemmi.cif.Document:
        """Read a CIF document from a file path."""
        return gemmi.cif.read_file(path)

    @staticmethod
    def _read_cif_document_from_string(text: str) -> gemmi.cif.Document:
        """Read a CIF document from a raw text string."""
        return gemmi.cif.read_string(text)

    @staticmethod
    def _has_structural_content(block: gemmi.cif.Block) -> bool:
        """Return True if the CIF block contains structural content."""
        # Basic heuristics: atom_site loop or full set of cell params
        loop = block.find_loop('_atom_site.fract_x')
        if loop is not None:
            return True
        required_cell = [
            '_cell.length_a',
            '_cell.length_b',
            '_cell.length_c',
            '_cell.angle_alpha',
            '_cell.angle_beta',
            '_cell.angle_gamma',
        ]
        return all(block.find_value(tag) for tag in required_cell)

    @classmethod
    def _pick_first_structural_block(
        cls,
        doc: gemmi.cif.Document,
    ) -> gemmi.cif.Block:
        """Pick the most likely structural block from a CIF document."""
        # Prefer blocks with atom_site loop; else first block with cell
        for block in doc:
            if cls._has_structural_content(block):
                return block
        # As a fallback, return the sole or first block
        try:
            return doc.sole_block()
        except Exception:
            return doc[0]

    @classmethod
    def _create_model_from_block(
        cls,
        block: gemmi.cif.Block,
    ) -> SampleModelBase:
        """Build a model instance from a single CIF block."""
        name = cls._extract_name_from_block(block)
        model = SampleModelBase(name=name)
        cls._set_space_group_from_cif_block(model, block)
        cls._set_cell_from_cif_block(model, block)
        cls._set_atom_sites_from_cif_block(model, block)
        return model

    @classmethod
    def _extract_name_from_block(cls, block: gemmi.cif.Block) -> str:
        """Extract a model name from the CIF block name."""
        return block.name or 'model'

    @classmethod
    def _set_space_group_from_cif_block(
        cls,
        model: SampleModelBase,
        block: gemmi.cif.Block,
    ) -> None:
        """Populate the model's space group from a CIF block."""
        model.space_group.from_cif(block)

    @classmethod
    def _set_cell_from_cif_block(
        cls,
        model: SampleModelBase,
        block: gemmi.cif.Block,
    ) -> None:
        """Populate the model's unit cell from a CIF block."""
        model.cell.from_cif(block)

    @classmethod
    def _set_atom_sites_from_cif_block(
        cls,
        model: SampleModelBase,
        block: gemmi.cif.Block,
    ) -> None:
        """Populate the model's atom sites from a CIF block."""
        model.atom_sites.from_cif(block)
create(**kwargs) classmethod

Create a model based on a validated argument combination.

Other Parameters:

Name Type Description
name

Name of the sample model to create.

cif_path

Path to a CIF file to parse.

cif_str

Raw CIF string to parse.

**kwargs

Extra args are ignored if None; only the above three keys are supported.

Returns:

Name Type Description
SampleModelBase SampleModelBase

A populated or empty model instance.

Source code in src/easydiffraction/sample_models/sample_model/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
@classmethod
def create(cls, **kwargs) -> SampleModelBase:
    """Create a model based on a validated argument combination.

    Keyword Args:
        name: Name of the sample model to create.
        cif_path: Path to a CIF file to parse.
        cif_str: Raw CIF string to parse.
        **kwargs: Extra args are ignored if None; only the above
            three keys are supported.

    Returns:
        SampleModelBase: A populated or empty model instance.
    """
    # Check for valid argument combinations
    user_args = {k for k, v in kwargs.items() if v is not None}
    cls._validate_args(user_args, cls._ALLOWED_ARG_SPECS, cls.__name__)

    if 'cif_path' in kwargs:
        return cls._create_from_cif_path(kwargs['cif_path'])
    elif 'cif_str' in kwargs:
        return cls._create_from_cif_str(kwargs['cif_str'])
    elif 'name' in kwargs:
        return SampleModelBase(name=kwargs['name'])

sample_models

SampleModels

Bases: DatablockCollection

Collection manager for multiple SampleModel instances.

Source code in src/easydiffraction/sample_models/sample_models.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class SampleModels(DatablockCollection):
    """Collection manager for multiple SampleModel instances."""

    def __init__(self) -> None:
        super().__init__(item_type=SampleModelBase)

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

    @typechecked
    def add_from_cif_path(self, cif_path: str) -> None:
        """Create and add a model from a CIF file path.

        Args:
            cif_path: Path to a CIF file.
        """
        sample_model = SampleModelFactory.create(cif_path=cif_path)
        self.add(sample_model)

    @typechecked
    def add_from_cif_str(self, cif_str: str) -> None:
        """Create and add a model from CIF content (string).

        Args:
            cif_str: CIF file content.
        """
        sample_model = SampleModelFactory.create(cif_str=cif_str)
        self.add(sample_model)

    @typechecked
    def add_minimal(self, name: str) -> None:
        """Create and add a minimal model (defaults, no atoms).

        Args:
            name: Identifier to assign to the new model.
        """
        sample_model = SampleModelFactory.create(name=name)
        self.add(sample_model)

    @typechecked
    def remove(self, name: str) -> None:
        """Remove a sample model by its ID.

        Args:
            name: ID of the model to remove.
        """
        if name in self:
            del self[name]

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

    def show_names(self) -> None:
        """List all model names in the collection."""
        console.paragraph('Defined sample models' + ' 🧩')
        console.print(self.names)

    def show_params(self) -> None:
        """Show parameters of all sample models in the collection."""
        for model in self.values():
            model.show_params()

add_from_cif_path(cif_path)

Create and add a model from a CIF file path.

Parameters:

Name Type Description Default
cif_path str

Path to a CIF file.

required
Source code in src/easydiffraction/sample_models/sample_models.py
22
23
24
25
26
27
28
29
30
@typechecked
def add_from_cif_path(self, cif_path: str) -> None:
    """Create and add a model from a CIF file path.

    Args:
        cif_path: Path to a CIF file.
    """
    sample_model = SampleModelFactory.create(cif_path=cif_path)
    self.add(sample_model)

add_from_cif_str(cif_str)

Create and add a model from CIF content (string).

Parameters:

Name Type Description Default
cif_str str

CIF file content.

required
Source code in src/easydiffraction/sample_models/sample_models.py
32
33
34
35
36
37
38
39
40
@typechecked
def add_from_cif_str(self, cif_str: str) -> None:
    """Create and add a model from CIF content (string).

    Args:
        cif_str: CIF file content.
    """
    sample_model = SampleModelFactory.create(cif_str=cif_str)
    self.add(sample_model)

add_minimal(name)

Create and add a minimal model (defaults, no atoms).

Parameters:

Name Type Description Default
name str

Identifier to assign to the new model.

required
Source code in src/easydiffraction/sample_models/sample_models.py
42
43
44
45
46
47
48
49
50
@typechecked
def add_minimal(self, name: str) -> None:
    """Create and add a minimal model (defaults, no atoms).

    Args:
        name: Identifier to assign to the new model.
    """
    sample_model = SampleModelFactory.create(name=name)
    self.add(sample_model)

remove(name)

Remove a sample model by its ID.

Parameters:

Name Type Description Default
name str

ID of the model to remove.

required
Source code in src/easydiffraction/sample_models/sample_models.py
52
53
54
55
56
57
58
59
60
@typechecked
def remove(self, name: str) -> None:
    """Remove a sample model by its ID.

    Args:
        name: ID of the model to remove.
    """
    if name in self:
        del self[name]

show_names()

List all model names in the collection.

Source code in src/easydiffraction/sample_models/sample_models.py
66
67
68
69
def show_names(self) -> None:
    """List all model names in the collection."""
    console.paragraph('Defined sample models' + ' 🧩')
    console.print(self.names)

show_params()

Show parameters of all sample models in the collection.

Source code in src/easydiffraction/sample_models/sample_models.py
71
72
73
74
def show_params(self) -> None:
    """Show parameters of all sample models in the collection."""
    for model in self.values():
        model.show_params()