Skip to content

sample_models

collections

atom_sites

AtomSite

Bases: Component

Represents a single atom site within the crystal structure.

Source code in src/easydiffraction/sample_models/collections/atom_sites.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
class AtomSite(Component):
    """
    Represents a single atom site within the crystal structure.
    """

    @property
    def category_key(self):
        return 'atom_sites'

    @property
    def cif_category_key(self):
        return 'atom_site'

    def __init__(
        self,
        label: str,
        type_symbol: str,
        fract_x: float,
        fract_y: float,
        fract_z: float,
        wyckoff_letter: str = None,
        occupancy: float = 1.0,
        b_iso: float = 0.0,
        adp_type: str = 'Biso',
    ):  # TODO: add support for Uiso, Uani and Bani
        super().__init__()

        self.label = Descriptor(
            value=label,
            name='label',
            cif_name='label',
        )
        self.type_symbol = Descriptor(
            value=type_symbol,
            name='type_symbol',
            cif_name='type_symbol',
        )
        self.adp_type = Descriptor(
            value=adp_type,
            name='adp_type',
            cif_name='ADP_type',
        )
        self.wyckoff_letter = Descriptor(
            value=wyckoff_letter,
            name='wyckoff_letter',
            cif_name='Wyckoff_letter',
        )
        self.fract_x = Parameter(
            value=fract_x,
            name='fract_x',
            cif_name='fract_x',
        )
        self.fract_y = Parameter(
            value=fract_y,
            name='fract_y',
            cif_name='fract_y',
        )
        self.fract_z = Parameter(
            value=fract_z,
            name='fract_z',
            cif_name='fract_z',
        )
        self.occupancy = Parameter(
            value=occupancy,
            name='occupancy',
            cif_name='occupancy',
        )
        self.b_iso = Parameter(
            value=b_iso,
            name='b_iso',
            units='Ų',
            cif_name='B_iso_or_equiv',
        )
        # Select which of the input parameters is used for the
        # as ID for the whole object
        self._entry_id = label

        # Lock further attribute additions to prevent
        # accidental modifications by users
        self._locked = True

AtomSites

Bases: Collection

Collection of AtomSite instances.

Source code in src/easydiffraction/sample_models/collections/atom_sites.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
class AtomSites(Collection):
    """
    Collection of AtomSite instances.
    """

    # TODO: Check, if we can get rid of this property
    #  We could use class name instead
    @property
    def _type(self):
        return 'category'  # datablock or category

    @property
    def _child_class(self):
        return AtomSite

components

cell

Cell

Bases: Component

Represents the unit cell parameters of a sample model.

Source code in src/easydiffraction/sample_models/components/cell.py
 8
 9
10
11
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
class Cell(Component):
    """
    Represents the unit cell parameters of a sample model.
    """

    @property
    def category_key(self) -> str:
        return 'cell'

    @property
    def cif_category_key(self) -> str:
        return 'cell'

    def __init__(
        self,
        length_a: float = 10.0,
        length_b: float = 10.0,
        length_c: float = 10.0,
        angle_alpha: float = 90.0,
        angle_beta: float = 90.0,
        angle_gamma: float = 90.0,
    ) -> None:
        super().__init__()

        self.length_a = Parameter(
            value=length_a,
            name='length_a',
            cif_name='length_a',
            units='Å',
        )
        self.length_b = Parameter(
            value=length_b,
            name='length_b',
            cif_name='length_b',
            units='Å',
        )
        self.length_c = Parameter(
            value=length_c,
            name='length_c',
            cif_name='length_c',
            units='Å',
        )
        self.angle_alpha = Parameter(
            value=angle_alpha,
            name='angle_alpha',
            cif_name='angle_alpha',
            units='deg',
        )
        self.angle_beta = Parameter(
            value=angle_beta,
            name='angle_beta',
            cif_name='angle_beta',
            units='deg',
        )
        self.angle_gamma = Parameter(
            value=angle_gamma,
            name='angle_gamma',
            cif_name='angle_gamma',
            units='deg',
        )

        # Lock further attribute additions to prevent
        # accidental modifications by users
        self._locked: bool = True

space_group

SpaceGroup

Bases: Component

Represents the space group of a sample model.

Source code in src/easydiffraction/sample_models/components/space_group.py
10
11
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
class SpaceGroup(Component):
    """
    Represents the space group of a sample model.
    """

    @property
    def category_key(self) -> str:
        return 'space_group'

    @property
    def cif_category_key(self) -> str:
        return 'space_group'

    def __init__(
        self,
        name_h_m: str = 'P 1',
        it_coordinate_system_code: Optional[int] = None,
    ) -> None:
        super().__init__()

        self.name_h_m = Descriptor(
            value=name_h_m,
            name='name_h_m',
            cif_name='name_H-M_alt',
        )
        self.it_coordinate_system_code = Descriptor(
            value=it_coordinate_system_code,
            name='it_coordinate_system_code',
            cif_name='IT_coordinate_system_code',
        )

        # Lock further attribute additions to prevent
        # accidental modifications by users
        self._locked = True

sample_model

SampleModel

Bases: Datablock

Represents an individual structural model of a sample. Wraps crystallographic information including space group, cell, and atomic sites.

Source code in src/easydiffraction/sample_models/sample_model.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
class SampleModel(Datablock):
    """
    Represents an individual structural model of a sample.
    Wraps crystallographic information including space group, cell, and
    atomic sites.
    """

    # TODO: Move cif_path and cif_str out of __init__ and into separate methods
    def __init__(self, name: str, cif_path: str = None, cif_str: str = None):
        super().__init__()
        self._name = name
        self.space_group = SpaceGroup()
        self.cell = Cell()
        self.atom_sites = AtomSites()

        if cif_path:
            self.load_from_cif_file(cif_path)
        elif cif_str:
            self.load_from_cif_string(cif_str)

    # ----------------------
    # Name (ID) of the model
    # ----------------------

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

    @name.setter
    @enforce_type
    def name(self, new_name: str):
        self._name = new_name

    # -----------
    # Space group
    # -----------

    @property
    def space_group(self):
        return self._space_group

    @space_group.setter
    @enforce_type
    def space_group(self, new_space_group: SpaceGroup):
        self._space_group = new_space_group

    # ----
    # Cell
    # ----

    @property
    def cell(self):
        return self._cell

    @cell.setter
    @enforce_type
    def cell(self, new_cell: Cell):
        self._cell = new_cell

    # ----------
    # Atom sites
    # ----------

    @property
    def atom_sites(self):
        return self._atom_sites

    @atom_sites.setter
    @enforce_type
    def atom_sites(self, new_atom_sites: AtomSites):
        self._atom_sites = new_atom_sites

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

    def _apply_cell_symmetry_constraints(self):
        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):
        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:
                # raise ValueError("Wyckoff letter is not defined for atom.")
                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):
        pass

    def apply_symmetry_constraints(self):
        self._apply_cell_symmetry_constraints()
        self._apply_atomic_coordinates_symmetry_constraints()
        self._apply_atomic_displacement_symmetry_constraints()

    # -----------------
    # Creation from CIF
    # -----------------

    def load_from_cif_file(self, cif_path: str):
        """Load model data from a CIF file."""
        # TODO: Implement CIF parsing here
        print(f'Loading SampleModel from CIF file: {cif_path}')
        # Example: self.id = extract_id_from_cif(cif_path)

    def load_from_cif_string(self, cif_str: str):
        """Load model data from a CIF string."""
        # TODO: Implement CIF parsing from a string
        print('Loading SampleModel from CIF string.')

    # -----------------
    # Convertion to CIF
    # -----------------

    def as_cif(self) -> str:
        """
        Export the sample model to CIF format.
        Returns:
            str: CIF string representation of the sample model.
        """
        # Data block header
        cif_lines = [f'data_{self.name}']

        # Space Group
        cif_lines += ['', self.space_group.as_cif()]

        # Unit Cell
        cif_lines += ['', self.cell.as_cif()]

        # Atom Sites
        cif_lines += ['', self.atom_sites.as_cif()]

        return '\n'.join(cif_lines)

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

    def show_structure(self, plane='xy', grid_size=20):
        """
        Show an ASCII projection of the structure on a 2D plane.

        Args:
            plane (str): 'xy', 'xz', or 'yz' plane to project.
            grid_size (int): Size of the ASCII grid (default is 20).
        """

        print(paragraph(f"Sample model 🧩 '{self.name}' structure view"))
        print('Not implemented yet.')

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

    def show_as_cif(self) -> None:
        cif_text: str = self.as_cif()
        paragraph_title: str = paragraph(f"Sample model 🧩 '{self.name}' as cif")
        render_cif(cif_text, paragraph_title)

as_cif()

Export the sample model to CIF format. Returns: str: CIF string representation of the sample model.

Source code in src/easydiffraction/sample_models/sample_model.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def as_cif(self) -> str:
    """
    Export the sample model to CIF format.
    Returns:
        str: CIF string representation of the sample model.
    """
    # Data block header
    cif_lines = [f'data_{self.name}']

    # Space Group
    cif_lines += ['', self.space_group.as_cif()]

    # Unit Cell
    cif_lines += ['', self.cell.as_cif()]

    # Atom Sites
    cif_lines += ['', self.atom_sites.as_cif()]

    return '\n'.join(cif_lines)

load_from_cif_file(cif_path)

Load model data from a CIF file.

Source code in src/easydiffraction/sample_models/sample_model.py
143
144
145
146
def load_from_cif_file(self, cif_path: str):
    """Load model data from a CIF file."""
    # TODO: Implement CIF parsing here
    print(f'Loading SampleModel from CIF file: {cif_path}')

load_from_cif_string(cif_str)

Load model data from a CIF string.

Source code in src/easydiffraction/sample_models/sample_model.py
149
150
151
152
def load_from_cif_string(self, cif_str: str):
    """Load model data from a CIF string."""
    # TODO: Implement CIF parsing from a string
    print('Loading SampleModel from CIF string.')

show_params()

Display structural parameters (space group, unit cell, atomic sites).

Source code in src/easydiffraction/sample_models/sample_model.py
194
195
196
197
198
199
200
def show_params(self):
    """Display structural parameters (space group, unit cell, atomic sites)."""
    print(f'\nSampleModel ID: {self.name}')
    print(f'Space group: {self.space_group.name_h_m}')
    print(f'Cell parameters: {self.cell.as_dict()}')
    print('Atom sites:')
    self.atom_sites.show()

show_structure(plane='xy', grid_size=20)

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

Parameters:

Name Type Description Default
plane str

'xy', 'xz', or 'yz' plane to project.

'xy'
grid_size int

Size of the ASCII grid (default is 20).

20
Source code in src/easydiffraction/sample_models/sample_model.py
182
183
184
185
186
187
188
189
190
191
192
def show_structure(self, plane='xy', grid_size=20):
    """
    Show an ASCII projection of the structure on a 2D plane.

    Args:
        plane (str): 'xy', 'xz', or 'yz' plane to project.
        grid_size (int): Size of the ASCII grid (default is 20).
    """

    print(paragraph(f"Sample model 🧩 '{self.name}' structure view"))
    print('Not implemented yet.')

sample_models

SampleModels

Bases: Collection

Collection manager for multiple SampleModel instances.

Source code in src/easydiffraction/sample_models/sample_models.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
class SampleModels(Collection):
    """
    Collection manager for multiple SampleModel instances.
    """

    @property
    def _child_class(self):
        return SampleModel

    def __init__(self) -> None:
        super().__init__()  # Initialize Collection
        self._models = self._items  # Alias for legacy support

    def add(
        self,
        model: Optional[SampleModel] = None,
        name: Optional[str] = None,
        cif_path: Optional[str] = None,
        cif_str: Optional[str] = None,
    ) -> None:
        """
        Add a new sample model to the collection.
        Dispatches based on input type: pre-built model or parameters for new creation.

        Args:
            model: An existing SampleModel instance.
            name: Name for a new model if created from scratch.
            cif_path: Path to a CIF file to create a model from.
            cif_str: CIF content as string to create a model from.
        """
        if model:
            self._add_prebuilt_sample_model(model)
        else:
            self._create_and_add_sample_model(name, cif_path, cif_str)

    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._models:
            del self._models[name]

    def get_ids(self) -> List[str]:
        """
        Return a list of all model IDs in the collection.

        Returns:
            List of model IDs.
        """
        return list(self._models.keys())

    @property
    def ids(self) -> List[str]:
        """Property accessor for model IDs."""
        return self.get_ids()

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

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

    def as_cif(self) -> str:
        """
        Export all sample models to CIF format.

        Returns:
            CIF string representation of all sample models.
        """
        return '\n'.join([model.as_cif() for model in self._models.values()])

    @enforce_type
    def _add_prebuilt_sample_model(self, sample_model: SampleModel) -> None:
        """
        Add a pre-built SampleModel instance.

        Args:
            model: The SampleModel instance to add.

        Raises:
            TypeError: If model is not a SampleModel instance.
        """
        self._models[sample_model.name] = sample_model

    def _create_and_add_sample_model(
        self,
        name: Optional[str] = None,
        cif_path: Optional[str] = None,
        cif_str: Optional[str] = None,
    ) -> None:
        """
        Create a SampleModel instance and add it to the collection.

        Args:
            name: Name for the new model.
            cif_path: Path to a CIF file.
            cif_str: CIF content as string.

        Raises:
            ValueError: If neither name, cif_path, nor cif_str is provided.
        """
        if cif_path:
            model = SampleModel(cif_path=cif_path)
        elif cif_str:
            model = SampleModel(cif_str=cif_str)
        elif name:
            model = SampleModel(name=name)
        else:
            raise ValueError('You must provide a name, cif_path, or cif_str.')

        self._models[model.name] = model

add(model=None, name=None, cif_path=None, cif_str=None)

Add a new sample model to the collection. Dispatches based on input type: pre-built model or parameters for new creation.

Parameters:

Name Type Description Default
model Optional[SampleModel]

An existing SampleModel instance.

None
name Optional[str]

Name for a new model if created from scratch.

None
cif_path Optional[str]

Path to a CIF file to create a model from.

None
cif_str Optional[str]

CIF content as string to create a model from.

None
Source code in src/easydiffraction/sample_models/sample_models.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def add(
    self,
    model: Optional[SampleModel] = None,
    name: Optional[str] = None,
    cif_path: Optional[str] = None,
    cif_str: Optional[str] = None,
) -> None:
    """
    Add a new sample model to the collection.
    Dispatches based on input type: pre-built model or parameters for new creation.

    Args:
        model: An existing SampleModel instance.
        name: Name for a new model if created from scratch.
        cif_path: Path to a CIF file to create a model from.
        cif_str: CIF content as string to create a model from.
    """
    if model:
        self._add_prebuilt_sample_model(model)
    else:
        self._create_and_add_sample_model(name, cif_path, cif_str)

as_cif()

Export all sample models to CIF format.

Returns:

Type Description
str

CIF string representation of all sample models.

Source code in src/easydiffraction/sample_models/sample_models.py
82
83
84
85
86
87
88
89
def as_cif(self) -> str:
    """
    Export all sample models to CIF format.

    Returns:
        CIF string representation of all sample models.
    """
    return '\n'.join([model.as_cif() for model in self._models.values()])

get_ids()

Return a list of all model IDs in the collection.

Returns:

Type Description
List[str]

List of model IDs.

Source code in src/easydiffraction/sample_models/sample_models.py
58
59
60
61
62
63
64
65
def get_ids(self) -> List[str]:
    """
    Return a list of all model IDs in the collection.

    Returns:
        List of model IDs.
    """
    return list(self._models.keys())

ids property

Property accessor for model IDs.

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
48
49
50
51
52
53
54
55
56
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._models:
        del self._models[name]

show_names()

List all model IDs in the collection.

Source code in src/easydiffraction/sample_models/sample_models.py
72
73
74
75
def show_names(self) -> None:
    """List all model IDs in the collection."""
    print(paragraph('Defined sample models' + ' 🧩'))
    print(self.get_ids())

show_params()

Show parameters of all sample models in the collection.

Source code in src/easydiffraction/sample_models/sample_models.py
77
78
79
80
def show_params(self) -> None:
    """Show parameters of all sample models in the collection."""
    for model in self._models.values():
        model.show_params()