Skip to content

Structure

Bases: Structure

This class is a wraparound for Pymatgen's Structure class with additional properties and methods.

Source code in src/baderkit/toolkit/structure.py
 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
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
class Structure(PymatgenStructure):
    """
    This class is a wraparound for Pymatgen's Structure class with additional
    properties and methods.
    """

    def __init__(
        self,
        lattice: Lattice,
        species: list[Species],
        frac_coords: list[NDArray],
        symmetry_kwargs: dict = {},
        **kwargs,
    ):
        # clean frac coords
        for coord in frac_coords:
            coord %= 1.0
        super().__init__(lattice, species, frac_coords, **kwargs)
        # add labels to sites. This is to add backwards compatability to the
        # relabel_sites method that doesn't exist in earlier versions of pymatgen
        for site in self:
            site.properties["label"] = site.specie.symbol

        # cache for symmetry data
        self._symmetry_kwargs = symmetry_kwargs
        self._symmetry_data = None
        self._last_symmetry_save = None
        self._spacegroup_analyzer = None

    def insert(  # type: ignore
        self,
        i: int,
        species: CompositionLike,
        coords: NDArray,
        coords_are_cartesian: bool = False,
        validate_proximity: bool = False,
        properties: dict | None = None,
    ):
        """
        Insert a site to the structure.

        This is a wraparound for the PyMatGen method adding a label if it is
        not specified.

        Args:
            i (int): Index to insert site
            species (species-like): Species of inserted site
            coords (3x1 array): Coordinates of inserted site
            coords_are_cartesian (bool): Whether coordinates are cartesian.
                Defaults to False.
            validate_proximity (bool): Whether to check if inserted site is
                too close to an existing site. Defaults to False.
            properties (dict): Properties associated with the site.

        Returns:
            New structure with inserted site.
        """
        if properties is None:
            properties = {}
        if properties.get("label", None) is None:
            properties["label"] = str(species)

        super().insert(
            i,
            species=species,
            coords=coords,
            coords_are_cartesian=coords_are_cartesian,
            validate_proximity=validate_proximity,
            properties=properties,
        )

    @property
    def labels(self) -> list[str]:
        """

        Returns
        -------
        list[str]
            The list of labels for each site

        """
        return [site.label for site in self]

    @labels.setter
    def labels(self, labels: list[str]):
        assert len(labels) == len(self), "Labels must be the same length structure."

    def get_cart_from_miller(self, h: int, k: int, l: int) -> NDArray[float]:
        """
        Gets the cartesian coordinates of the vector perpendicular to the provided
        miller indices

        Parameters
        ----------
        h : int
            First miller index.
        k : int
            Second miller index.
        l : int
            Third miller index.


        Returns
        -------
        NDArray[float]
            The cartesian coordinates of the vector perpendicular to the plane
            defined by the provided miller indices.

        """
        lattice = self.lattice
        # Get three points that define the plane from miller indices. For indices
        # of zero we can just take one of the other points and add 1 along the
        # lattice direction of interest to make a parallel line
        if h != 0:
            a1 = np.array([1 / h, 0, 0])
        else:
            a1 = None
        if k != 0:
            a2 = np.array([0, 1 / k, 0])
        else:
            a2 = None
        if l != 0:
            a3 = np.array([0, 0, 1 / l])
        else:
            a3 = None

        if a1 is None:
            if a2 is not None:
                a1 = a2.copy()
            else:
                a1 = a3.copy()
            a1[0] += 1

        if a2 is None:
            if a1 is not None:
                a2 = a1.copy()
            else:
                a2 = a3.copy()
            a2[1] += 1

        if a3 is None:
            if a1 is not None:
                a3 = a1.copy()
            else:
                a3 = a2.copy()
            a3[2] += 1

        # get real space coords from fractional coords
        a1_real = lattice.get_cartesian_coords(a1)
        a2_real = lattice.get_cartesian_coords(a2)
        a3_real = lattice.get_cartesian_coords(a3)

        vector1 = a2_real - a1_real
        vector2 = a3_real - a1_real
        normal_vector = np.cross(vector1, vector2)
        return normal_vector / np.linalg.norm(normal_vector)

    def relabel_sites(self, ignore_uniq: bool = False) -> Self:
        """
        This method is an exact copy of [Pymatgen's](https://github.com/materialsproject/pymatgen/blob/v2025.5.28/src/pymatgen/core/structure.py).
        It is not available in some older version of pymatgen so I've added it
        to increase the dependency range.

        Relabel sites to ensure they are unique.

        Site labels are updated in-place, and relabeled by suffixing _1, _2, ..., _n for duplicates.
        Call Structure.copy().relabel_sites() to avoid modifying the original structure.


        Parameters
        ----------
        ignore_uniq : bool, optional
            If True, do not relabel sites that already have unique labels.
            The default is False.

        Returns
        -------
        Self
            Structure: self with relabeled sites.

        """

        grouped = defaultdict(list)
        for site in self:
            grouped[site.label].append(site)

        for label, sites in grouped.items():
            if len(sites) == 0 or (len(sites) == 1 and ignore_uniq):
                continue

            for idx, site in enumerate(sites):
                site.label = f"{label}_{idx + 1}"

        return self

    @property
    def reduced_formula(self) -> str:
        """

        Returns
        -------
        str
            The reduced formula of the structure.

        """
        # NOTE: This property seems to only be available in some versions of
        # pymatgen, but its useful enough that I'm ensuring it always
        # exists here.

        return self.composition.reduced_formula

    @property
    def symmetry_kwargs(self) -> dict:
        """

        Returns
        -------
        dict
            The keyword arguments used in the SpaceGroupAnalyzer for finding
            symmetry data.

        """
        return self._symmetry_kwargs

    @symmetry_kwargs.setter
    def symmetry_kwargs(self, value: dict):
        # set kwargs and reset symmetry
        self._symmetry_kwargs = value
        self._symmetry_data = None

    @property
    def spacegroup_analyzer(self) -> SpacegroupAnalyzer:
        """

        Returns
        -------
        SpacegroupAnalyzer
            A spacegroup analyzer for this structure

        """
        if self._spacegroup_analyzer is None or self._last_symmetry_save != tuple(self):
            self._last_symmetry_save = tuple(self)
            self._spacegroup_analyzer = SpacegroupAnalyzer(self, **self.symmetry_kwargs)
        return self._spacegroup_analyzer

    @property
    def symmetry_data(self):
        """

        Returns
        -------
        TYPE
            The pymatgen symmetry dataset for the Structure object

        """
        if self._symmetry_data is None or self._last_symmetry_save != tuple(self):
            self._symmetry_data = self.spacegroup_analyzer.get_symmetry_dataset()
        return self._symmetry_data

    @property
    def equivalent_atoms(self) -> NDArray[int]:
        """

        Returns
        -------
        NDArray[int]
            The equivalent atoms in the Structure.

        """
        return self.symmetry_data.equivalent_atoms

    @staticmethod
    def merge_frac_coords(frac_coords):
        # avoid circular import
        from baderkit.global_numba.basic import merge_frac_coords

        frac_coords = np.asarray(frac_coords, dtype=np.float64)
        if len(frac_coords) == 0:
            return None
        elif frac_coords.ndim == 1:
            return frac_coords
        elif frac_coords.ndim == 2 and frac_coords.shape[2] == 3:
            return merge_frac_coords(frac_coords)
        else:
            raise Exception("Frac coords must have Nx3 shape")

    @property
    def site_symbols(self):
        return np.array([i.specie.symbol for i in self])

    @property
    def farthest_point(self) -> tuple[float, NDArray[float]]:
        """

        Returns
        -------
        tuple(float, NDArray[float]
            The maximum distance away from any atom in the system and the corresponding
            point. Note that generally this may be one point of several in systems
            with symmetry.

        """

        dist, point = largest_empty_sphere(self.lattice.matrix, self.frac_coords)
        return dist, point

    def to(self, filename: str | Path = "", fmt: str = "", **kwargs) -> str | None:
        """
        Outputs the structure to a file or string.

        Args:
            filename (str): If provided, output will be written to a file. If
                fmt is not specified, the format is determined from the
                filename. Defaults is None, i.e. string output.
            fmt (str): Format to output to. Defaults to JSON unless filename
                is provided. If fmt is specifies, it overrides whatever the
                filename is. Options include "cif", "poscar", "cssr", "json",
                "xsf", "mcsqs", "prismatic", "yaml", "fleur-inpgen".
                Non-case sensitive.
            **kwargs: Kwargs passthru to relevant methods. E.g., This allows
                the passing of parameters like symprec to the
                CifWriter.__init__ method for generation of symmetric cifs.

        Returns:
            (str) if filename is None. None otherwise.
        """

        # this is just a wrapper for the pymatgen method because some versions
        # do not allow for Path objects
        if filename:
            filename = Path(filename)
            filename = str(filename.resolve())
        return super().to(filename=filename, fmt=fmt, **kwargs)

    ###########################################################################
    # Plotting
    ###########################################################################
    def to_plotter(self, **kwargs):
        """

        Returns
        -------
        A GridPlotter object for visualization.
        """

        from baderkit.plotting import StructurePlotter

        return StructurePlotter(self, **kwargs)

equivalent_atoms property

Returns:

Type Description
NDArray[int]

The equivalent atoms in the Structure.

farthest_point property

Returns:

Type Description
tuple(float, NDArray[float]

The maximum distance away from any atom in the system and the corresponding point. Note that generally this may be one point of several in systems with symmetry.

labels property writable

Returns:

Type Description
list[str]

The list of labels for each site

reduced_formula property

Returns:

Type Description
str

The reduced formula of the structure.

spacegroup_analyzer property

Returns:

Type Description
SpacegroupAnalyzer

A spacegroup analyzer for this structure

symmetry_data property

Returns:

Type Description
TYPE

The pymatgen symmetry dataset for the Structure object

symmetry_kwargs property writable

Returns:

Type Description
dict

The keyword arguments used in the SpaceGroupAnalyzer for finding symmetry data.

get_cart_from_miller(h, k, l)

Gets the cartesian coordinates of the vector perpendicular to the provided miller indices

Parameters:

Name Type Description Default
h int

First miller index.

required
k int

Second miller index.

required
l int

Third miller index.

required

Returns:

Type Description
NDArray[float]

The cartesian coordinates of the vector perpendicular to the plane defined by the provided miller indices.

Source code in src/baderkit/toolkit/structure.py
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
def get_cart_from_miller(self, h: int, k: int, l: int) -> NDArray[float]:
    """
    Gets the cartesian coordinates of the vector perpendicular to the provided
    miller indices

    Parameters
    ----------
    h : int
        First miller index.
    k : int
        Second miller index.
    l : int
        Third miller index.


    Returns
    -------
    NDArray[float]
        The cartesian coordinates of the vector perpendicular to the plane
        defined by the provided miller indices.

    """
    lattice = self.lattice
    # Get three points that define the plane from miller indices. For indices
    # of zero we can just take one of the other points and add 1 along the
    # lattice direction of interest to make a parallel line
    if h != 0:
        a1 = np.array([1 / h, 0, 0])
    else:
        a1 = None
    if k != 0:
        a2 = np.array([0, 1 / k, 0])
    else:
        a2 = None
    if l != 0:
        a3 = np.array([0, 0, 1 / l])
    else:
        a3 = None

    if a1 is None:
        if a2 is not None:
            a1 = a2.copy()
        else:
            a1 = a3.copy()
        a1[0] += 1

    if a2 is None:
        if a1 is not None:
            a2 = a1.copy()
        else:
            a2 = a3.copy()
        a2[1] += 1

    if a3 is None:
        if a1 is not None:
            a3 = a1.copy()
        else:
            a3 = a2.copy()
        a3[2] += 1

    # get real space coords from fractional coords
    a1_real = lattice.get_cartesian_coords(a1)
    a2_real = lattice.get_cartesian_coords(a2)
    a3_real = lattice.get_cartesian_coords(a3)

    vector1 = a2_real - a1_real
    vector2 = a3_real - a1_real
    normal_vector = np.cross(vector1, vector2)
    return normal_vector / np.linalg.norm(normal_vector)

insert(i, species, coords, coords_are_cartesian=False, validate_proximity=False, properties=None)

Insert a site to the structure.

This is a wraparound for the PyMatGen method adding a label if it is not specified.

Args: i (int): Index to insert site species (species-like): Species of inserted site coords (3x1 array): Coordinates of inserted site coords_are_cartesian (bool): Whether coordinates are cartesian. Defaults to False. validate_proximity (bool): Whether to check if inserted site is too close to an existing site. Defaults to False. properties (dict): Properties associated with the site.

Returns: New structure with inserted site.

Source code in src/baderkit/toolkit/structure.py
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
def insert(  # type: ignore
    self,
    i: int,
    species: CompositionLike,
    coords: NDArray,
    coords_are_cartesian: bool = False,
    validate_proximity: bool = False,
    properties: dict | None = None,
):
    """
    Insert a site to the structure.

    This is a wraparound for the PyMatGen method adding a label if it is
    not specified.

    Args:
        i (int): Index to insert site
        species (species-like): Species of inserted site
        coords (3x1 array): Coordinates of inserted site
        coords_are_cartesian (bool): Whether coordinates are cartesian.
            Defaults to False.
        validate_proximity (bool): Whether to check if inserted site is
            too close to an existing site. Defaults to False.
        properties (dict): Properties associated with the site.

    Returns:
        New structure with inserted site.
    """
    if properties is None:
        properties = {}
    if properties.get("label", None) is None:
        properties["label"] = str(species)

    super().insert(
        i,
        species=species,
        coords=coords,
        coords_are_cartesian=coords_are_cartesian,
        validate_proximity=validate_proximity,
        properties=properties,
    )

relabel_sites(ignore_uniq=False)

This method is an exact copy of Pymatgen's. It is not available in some older version of pymatgen so I've added it to increase the dependency range.

Relabel sites to ensure they are unique.

Site labels are updated in-place, and relabeled by suffixing _1, _2, ..., _n for duplicates. Call Structure.copy().relabel_sites() to avoid modifying the original structure.

Parameters:

Name Type Description Default
ignore_uniq bool

If True, do not relabel sites that already have unique labels. The default is False.

False

Returns:

Type Description
Self

Structure: self with relabeled sites.

Source code in src/baderkit/toolkit/structure.py
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
def relabel_sites(self, ignore_uniq: bool = False) -> Self:
    """
    This method is an exact copy of [Pymatgen's](https://github.com/materialsproject/pymatgen/blob/v2025.5.28/src/pymatgen/core/structure.py).
    It is not available in some older version of pymatgen so I've added it
    to increase the dependency range.

    Relabel sites to ensure they are unique.

    Site labels are updated in-place, and relabeled by suffixing _1, _2, ..., _n for duplicates.
    Call Structure.copy().relabel_sites() to avoid modifying the original structure.


    Parameters
    ----------
    ignore_uniq : bool, optional
        If True, do not relabel sites that already have unique labels.
        The default is False.

    Returns
    -------
    Self
        Structure: self with relabeled sites.

    """

    grouped = defaultdict(list)
    for site in self:
        grouped[site.label].append(site)

    for label, sites in grouped.items():
        if len(sites) == 0 or (len(sites) == 1 and ignore_uniq):
            continue

        for idx, site in enumerate(sites):
            site.label = f"{label}_{idx + 1}"

    return self

to(filename='', fmt='', **kwargs)

Outputs the structure to a file or string.

Args: filename (str): If provided, output will be written to a file. If fmt is not specified, the format is determined from the filename. Defaults is None, i.e. string output. fmt (str): Format to output to. Defaults to JSON unless filename is provided. If fmt is specifies, it overrides whatever the filename is. Options include "cif", "poscar", "cssr", "json", "xsf", "mcsqs", "prismatic", "yaml", "fleur-inpgen". Non-case sensitive. **kwargs: Kwargs passthru to relevant methods. E.g., This allows the passing of parameters like symprec to the CifWriter.init method for generation of symmetric cifs.

Returns: (str) if filename is None. None otherwise.

Source code in src/baderkit/toolkit/structure.py
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
def to(self, filename: str | Path = "", fmt: str = "", **kwargs) -> str | None:
    """
    Outputs the structure to a file or string.

    Args:
        filename (str): If provided, output will be written to a file. If
            fmt is not specified, the format is determined from the
            filename. Defaults is None, i.e. string output.
        fmt (str): Format to output to. Defaults to JSON unless filename
            is provided. If fmt is specifies, it overrides whatever the
            filename is. Options include "cif", "poscar", "cssr", "json",
            "xsf", "mcsqs", "prismatic", "yaml", "fleur-inpgen".
            Non-case sensitive.
        **kwargs: Kwargs passthru to relevant methods. E.g., This allows
            the passing of parameters like symprec to the
            CifWriter.__init__ method for generation of symmetric cifs.

    Returns:
        (str) if filename is None. None otherwise.
    """

    # this is just a wrapper for the pymatgen method because some versions
    # do not allow for Path objects
    if filename:
        filename = Path(filename)
        filename = str(filename.resolve())
    return super().to(filename=filename, fmt=fmt, **kwargs)

to_plotter(**kwargs)

Returns:

Type Description
A GridPlotter object for visualization.
Source code in src/baderkit/toolkit/structure.py
357
358
359
360
361
362
363
364
365
366
367
def to_plotter(self, **kwargs):
    """

    Returns
    -------
    A GridPlotter object for visualization.
    """

    from baderkit.plotting import StructurePlotter

    return StructurePlotter(self, **kwargs)