• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pyiron / structuretoolkit / 5729289971

pending completion
5729289971

Pull #47

github-actions

web-flow
Merge 63e68d88b into de8f8fd69
Pull Request #47: Update interstitial class

51 of 51 new or added lines in 1 file covered. (100.0%)

2209 of 2543 relevant lines covered (86.87%)

0.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.24
/structuretoolkit/analyse/spatial.py
1
# coding: utf-8
2
# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
3
# Distributed under the terms of "New BSD License", see the LICENSE file.
4

5
import numpy as np
1✔
6
from scipy.sparse import coo_matrix
1✔
7
from scipy.spatial import ConvexHull, Delaunay, Voronoi
1✔
8

9
from structuretoolkit.analyse.neighbors import get_neighborhood, get_neighbors
1✔
10
from structuretoolkit.common.helper import (
1✔
11
    get_average_of_unique_labels,
12
    get_extended_positions,
13
    get_vertical_length,
14
    get_wrapped_coordinates,
15
)
16
from sklearn.cluster import DBSCAN
1✔
17

18
__author__ = "Joerg Neugebauer, Sam Waseda"
1✔
19
__copyright__ = (
1✔
20
    "Copyright 2021, Max-Planck-Institut für Eisenforschung GmbH - "
21
    "Computational Materials Design (CM) Department"
22
)
23
__version__ = "1.0"
1✔
24
__maintainer__ = "Sam Waseda"
1✔
25
__email__ = "waseda@mpie.de"
1✔
26
__status__ = "production"
1✔
27
__date__ = "Sep 1, 2017"
1✔
28

29

30
def get_mean_positions(positions, cell, pbc, labels):
1✔
31
    """
32
    This function calculates the average position(-s) across periodic boundary conditions according
33
    to the labels
34

35
    Args:
36
        positions (numpy.ndarray (n, 3)): Coordinates to be averaged
37
        cell (numpy.ndarray (3, 3)): Cell dimensions
38
        pbc (numpy.ndarray (3,)): Periodic boundary conditions (in boolean)
39
        labels (numpy.ndarray (n,)): labels according to which the atoms are grouped
40

41
    Returns:
42
        (numpy.ndarray): mean positions
43
    """
44
    # Translate labels to integer enumeration (0, 1, 2, ... etc.) and get their counts
45
    _, labels, counts = np.unique(labels, return_inverse=True, return_counts=True)
1✔
46
    # Get reference point for each unique label
47
    mean_positions = positions[np.unique(labels, return_index=True)[1]]
1✔
48
    # Get displacement vectors from reference points to all other points for the same labels
49
    all_positions = positions - mean_positions[labels]
1✔
50
    # Account for pbc
51
    all_positions = np.einsum("ji,nj->ni", np.linalg.inv(cell), all_positions)
1✔
52
    all_positions[:, pbc] -= np.rint(all_positions)[:, pbc]
1✔
53
    all_positions = np.einsum("ji,nj->ni", cell, all_positions)
1✔
54
    # Add average displacement vector of each label to the reference point
55
    np.add.at(mean_positions, labels, (all_positions.T / counts[labels]).T)
1✔
56
    return mean_positions
1✔
57

58

59
def create_gridpoints(structure, n_gridpoints_per_angstrom=5):
1✔
60
    cell = get_vertical_length(structure=structure)
1✔
61
    n_points = (n_gridpoints_per_angstrom * cell).astype(int)
1✔
62
    positions = np.meshgrid(
1✔
63
        *[np.linspace(0, 1, n_points[i], endpoint=False) for i in range(3)]
64
    )
65
    positions = np.stack(positions, axis=-1).reshape(-1, 3)
1✔
66
    return np.einsum("ji,nj->ni", structure.cell, positions)
1✔
67

68

69
def remove_too_close(positions, structure, min_distance=1):
1✔
70
    neigh = get_neighborhood(structure=structure, positions=positions, num_neighbors=1)
1✔
71
    return positions[neigh.distances.flatten() > min_distance]
1✔
72

73

74
def set_to_high_symmetry_points(positions, structure, neigh, decimals=4):
1✔
75
    for _ in range(10):
1✔
76
        neigh = neigh.get_neighborhood(positions)
1✔
77
        dx = np.mean(neigh.vecs, axis=-2)
1✔
78
        if np.allclose(dx, 0):
1✔
79
            return positions
1✔
80
        positions += dx
1✔
81
        positions = get_wrapped_coordinates(structure=structure, positions=positions)
1✔
82
        unique_indices = np.unique(
1✔
83
            np.round(positions, decimals=decimals), axis=0, return_index=True
84
        )[1]
85
        positions = positions[unique_indices]
1✔
86
    raise ValueError("High symmetry points could not be detected")
×
87

88

89
def cluster_by_steinhardt(positions, neigh, l_values, q_eps, var_ratio, min_samples):
1✔
90
    if min_samples is None:
1✔
91
        min_samples = min(len(neigh.distances), 5)
1✔
92
    neigh = neigh.get_neighborhood(positions)
1✔
93
    Q_values = np.array([neigh.get_steinhardt_parameter(ll) for ll in l_values])
1✔
94
    db = DBSCAN(q_eps, min_samples=min_samples)
1✔
95
    var = np.std(neigh.distances, axis=-1)
1✔
96
    descriptors = np.concatenate((Q_values, [var * var_ratio]), axis=0)
1✔
97
    labels = db.fit_predict(descriptors.T)
1✔
98
    var_mean = np.array(
1✔
99
        [np.mean(var[labels==ll]) for ll in np.unique(labels) if ll >= 0]
100
    )
101
    return positions[labels == np.argmin(var_mean)]
1✔
102

103

104
class Interstitials:
1✔
105
    """
106
    Class to search for interstitial sites
107

108
    This class internally does the following steps:
109

110
        0. Initialize grid points (or Voronoi vertices) which are considered as
111
            interstitial site candidates.
112
        1. Eliminate points within a distance from the nearest neighboring atoms as
113
            given by `min_distance`
114
        2. Shift interstitial candidates to the nearest symmetric points with respect to the
115
            neighboring atom sites/vertices.
116
        3. Cluster interstitial candidates to avoid point overlapping.
117
        4. Cluster interstitial candidates by their Steinhardt parameters (cf. `l_values` for
118
            the values of the spherical harmonics) and the variance of the distances and
119
            take the group with the smallest average distance variance
120

121
    The interstitial sites can be obtained via `positions`
122

123
    In complex structures (i.e. grain boundary, dislocation etc.), the default parameters
124
    should be chosen properly. In order to see other quantities, which potentially
125
    characterize interstitial sites, see the following class methods:
126

127
        - `get_variances()`
128
        - `get_distances()`
129
        - `get_steinhardt_parameters()`
130
        - `get_volumes()`
131
        - `get_areas()`
132

133
    Troubleshooting:
134

135
    Identifying interstitial sites is not a very easy task. The algorithm employed here will
136
    probably do a good job, but if it fails, it might be good to look at the following points
137

138
    - The intermediate results can be accessed via `run_workflow` by specifying the step number.
139
    - The most vulnerable point is the last step, clustering by Steinhardt parameters. Take a
140
        look at the step before and after. If the interstitial sites are present in the step
141
        before the clustering by Steinhardt parameters, you might want to change the values of
142
        `q_eps` and `var_ratio`. It might make a difference to use `l_values` as well.
143
    - It might fail to find sites if the box is very small. In that case it might make sense to
144
        set `min_samples` very low (you can take 1)
145
    """
146

147
    def __init__(
1✔
148
        self,
149
        structure,
150
        num_neighbors,
151
        n_gridpoints_per_angstrom=5,
152
        min_distance=1,
153
        use_voronoi=False,
154
        x_eps=0.1,
155
        l_values=np.arange(2, 13),
156
        q_eps=0.3,
157
        var_ratio=5,
158
        min_samples=None,
159
        neigh_args={},
160
        **args
161
    ):
162
        """
163

164
        Args:
165
            num_neighbors (int): Number of neighbors/vertices to consider for the interstitial
166
                sites. By definition, tetrahedral sites should have 4 vertices and octahedral
167
                sites 6.
168
            n_gridpoints_per_angstrom (int): Number of grid points per angstrom for the
169
                initialization of the interstitial candidates. The finer the mesh (i.e. the larger
170
                the value), the likelier it is to find the correct sites but then also it becomes
171
                computationally more expensive. Ignored if `use_voronoi` is set to `True`
172
            min_distance (float): Minimum distance from the nearest neighboring atoms to the
173
                positions for them to be considered as interstitial site candidates. Set
174
                `min_distance` to 0 if no point should be removed.
175
            use_voronoi (bool): Use Voronoi vertices for the initial interstitial candidate
176
                positions instead of grid points.
177
            x_eps (bool): eps value for the clustering of interstitial candidate positions
178
            l_values (list): list of values for the Steinhardt parameter values for the
179
                classification of the interstitial candidate points
180
            q_eps (float): eps value for the clustering of interstitial candidate points based
181
                on Steinhardt parameters and distance variances. This might play a crucial role
182
                in identifying the correct interstitial sites
183
            var_ratio (float): factor to be multiplied to the variance values in order to give
184
                a larger weight to the variances.
185
            min_samples (int/None): `min_sample` in the point clustering.
186
            neigh_args (dict): arguments to be added to `get_neighbors`
187
        """
188
        if use_voronoi:
1✔
189
            self.initial_positions = get_voronoi_vertices(structure)
×
190
        else:
191
            self.initial_positions = create_gridpoints(
1✔
192
                structure=structure, n_gridpoints_per_angstrom=n_gridpoints_per_angstrom
193
            )
194
        self._neigh = get_neighbors(
1✔
195
            structure=structure, num_neighbors=num_neighbors, **neigh_args
196
        )
197
        self.workflow = [
1✔
198
            {
199
                "f": remove_too_close,
200
                "args": {"structure": structure, "min_distance": min_distance},
201
            },
202
            {
203
                "f": set_to_high_symmetry_points,
204
                "args": {"structure": structure, "neigh": self.neigh},
205
            },
206
            {
207
                "f": lambda **args: get_cluster_positions(structure, **args),
208
                "args": {"eps": x_eps}
209
            },
210
            {
211
                "f": cluster_by_steinhardt,
212
                "args": {
213
                    "neigh": self.neigh,
214
                    "l_values": l_values,
215
                    "q_eps": q_eps,
216
                    "var_ratio": var_ratio,
217
                    "min_samples": min_samples,
218
                },
219
            },
220
        ]
221
        self._positions = None
1✔
222
        self.structure = structure
1✔
223

224
    def run_workflow(self, positions=None, steps=-1):
1✔
225
        if positions is None:
1✔
226
            positions = self.initial_positions.copy()
1✔
227
        for ii, ww in enumerate(self.workflow):
1✔
228
            positions = ww["f"](positions=positions, **ww["args"])
1✔
229
            if ii == steps:
1✔
230
                return positions
×
231
        return positions
1✔
232

233
    @property
1✔
234
    def neigh(self):
1✔
235
        """
236
        Neighborhood information of each interstitial candidate and their surrounding atoms. E.g.
237
        `class.neigh.distances[0][0]` gives the distance from the first interstitial candidate to
238
        its nearest neighboring atoms. The functionalities of `neigh` follow those of
239
        `structuretoolkit.analyse.neighbors`.
240
        """
241
        return self._neigh
1✔
242

243
    @property
1✔
244
    def positions(self):
1✔
245
        if self._positions is None:
1✔
246
            self._positions = self.run_workflow()
1✔
247
            self._neigh = self.neigh.get_neighborhood(self._positions)
1✔
248
        return self._positions
1✔
249

250
    @property
1✔
251
    def hull(self):
1✔
252
        """
253
        Convex hull of each atom. It is mainly used for the volume and area calculation of each
254
        interstitial candidate. For more info, see `get_volumes` and `get_areas`.
255
        """
256
        return [ConvexHull(v) for v in self.neigh.vecs]
1✔
257

258
    def get_variances(self):
1✔
259
        """
260
        Get variance of neighboring distances. Since interstitial sites are mostly in symmetric
261
        sites, the variance values tend to be small. In the case of fcc, both tetrahedral and
262
        octahedral sites as well as tetrahedral sites in bcc should have the value of 0.
263

264
        Returns:
265
            (numpy.array (n,)) Variance values
266
        """
267
        return np.std(self.neigh.distances, axis=-1)
1✔
268

269
    def get_distances(self, function_to_apply=np.min):
1✔
270
        """
271
        Get per-position return values of a given function for the neighbors.
272

273
        Args:
274
            function_to_apply (function): Function to apply to the distance array. Default is
275
                numpy.minimum
276

277
        Returns:
278
            (numpy.array (n,)) Function values on the distance array
279
        """
280
        return function_to_apply(self.neigh.distances, axis=-1)
1✔
281

282
    def get_steinhardt_parameters(self, l):
1✔
283
        """
284
        Args:
285
            l (int/numpy.array): Order of Steinhardt parameter
286

287
        Returns:
288
            (numpy.array (n,)) Steinhardt parameter values
289
        """
290
        return self.neigh.get_steinhardt_parameter(l=l)
1✔
291

292
    def get_volumes(self):
1✔
293
        """
294
        Returns:
295
            (numpy.array (n,)): Convex hull volume of each site.
296
        """
297
        return np.array([h.volume for h in self.hull])
1✔
298

299
    def get_areas(self):
1✔
300
        """
301
        Returns:
302
            (numpy.array (n,)): Convex hull area of each site.
303
        """
304
        return np.array([h.area for h in self.hull])
1✔
305

306

307
def get_interstitials(
1✔
308
    structure,
309
    num_neighbors,
310
    n_gridpoints_per_angstrom=5,
311
    min_distance=1,
312
    use_voronoi=False,
313
    x_eps=0.1,
314
    l_values=np.arange(2, 13),
315
    q_eps=0.3,
316
    var_ratio=5,
317
    min_samples=None,
318
    neigh_args={},
319
    **args
320
):
321
    return Interstitials(
1✔
322
        structure=structure,
323
        num_neighbors=num_neighbors,
324
        n_gridpoints_per_angstrom=n_gridpoints_per_angstrom,
325
        min_distance=min_distance,
326
        use_voronoi=use_voronoi,
327
        x_eps=x_eps,
328
        l_values=l_values,
329
        q_eps=q_eps,
330
        var_ratio=var_ratio,
331
        min_samples=min_samples,
332
        neigh_args=neigh_args,
333
        **args
334
    )
335

336

337
get_interstitials.__doc__ = (
1✔
338
    Interstitials.__doc__.replace("Class", "Function") + Interstitials.__init__.__doc__
339
)
340

341

342
def get_layers(
1✔
343
    structure,
344
    distance_threshold=0.01,
345
    id_list=None,
346
    wrap_atoms=True,
347
    planes=None,
348
    cluster_method=None,
349
):
350
    """
351
    Get an array of layer numbers.
352

353
    Args:
354
        distance_threshold (float): Distance below which two points are
355
            considered to belong to the same layer. For detailed
356
            description: sklearn.cluster.AgglomerativeClustering
357
        id_list (list/numpy.ndarray): List of atoms for which the layers
358
            should be considered.
359
        wrap_atoms (bool): Whether to consider periodic boundary conditions according to the box definition or not.
360
            If set to `False`, atoms lying on opposite box boundaries are considered to belong to different layers,
361
            regardless of whether the box itself has the periodic boundary condition in this direction or not.
362
            If `planes` is not `None` and `wrap_atoms` is `True`, this tag has the same effect as calling
363
            `get_layers()` after calling `center_coordinates_in_unit_cell()`
364
        planes (list/numpy.ndarray): Planes along which the layers are calculated. Planes are
365
            given in vectors, i.e. [1, 0, 0] gives the layers along the x-axis. Default planes
366
            are orthogonal unit vectors: [[1, 0, 0], [0, 1, 0], [0, 0, 1]]. If you have a
367
            tilted box and want to calculate the layers along the directions of the cell
368
            vectors, use `planes=np.linalg.inv(structure.cell).T`. Whatever values are
369
            inserted, they are internally normalized, so whether [1, 0, 0] is entered or
370
            [2, 0, 0], the results will be the same.
371
        cluster_method (scikit-learn cluster algorithm): if given overrides the clustering method used, must be an
372
            instance of a cluster algorithm from scikit-learn (or compatible interface)
373

374
    Returns: Array of layer numbers (same shape as structure.positions)
375

376
    Example I - how to get the number of layers in each direction:
377

378
    >>> structure = Project('.').create_structure('Fe', 'bcc', 2.83).repeat(5)
379
    >>> print('Numbers of layers:', np.max(structure.analyse.get_layers(), axis=0)+1)
380

381
    Example II - get layers of only one species:
382

383
    >>> print('Iron layers:', structure.analyse.get_layers(
384
    ...       id_list=structure.select_index('Fe')))
385

386
    The clustering algorithm can be changed with the cluster_method argument
387

388
    >>> from sklearn.cluster import DBSCAN
389
    >>> layers = structure.analyse.get_layers(cluster_method=DBSCAN())
390
    """
391
    if distance_threshold <= 0:
1✔
392
        raise ValueError("distance_threshold must be a positive float")
1✔
393
    if id_list is not None and len(id_list) == 0:
1✔
394
        raise ValueError("id_list must contain at least one id")
1✔
395
    if wrap_atoms and planes is None:
1✔
396
        positions, indices = get_extended_positions(
1✔
397
            structure=structure, width=distance_threshold, return_indices=True
398
        )
399
        if id_list is not None:
1✔
400
            id_list = np.arange(len(structure))[np.array(id_list)]
1✔
401
            id_list = np.any(id_list[:, np.newaxis] == indices[np.newaxis, :], axis=0)
1✔
402
            positions = positions[id_list]
1✔
403
            indices = indices[id_list]
1✔
404
    else:
405
        positions = structure.positions
1✔
406
        if id_list is not None:
1✔
407
            positions = positions[id_list]
1✔
408
        if wrap_atoms:
1✔
409
            positions = get_wrapped_coordinates(
1✔
410
                structure=structure, positions=positions
411
            )
412
    if planes is not None:
1✔
413
        mat = np.asarray(planes).reshape(-1, 3)
1✔
414
        positions = np.einsum(
1✔
415
            "ij,i,nj->ni", mat, 1 / np.linalg.norm(mat, axis=-1), positions
416
        )
417
    if cluster_method is None:
1✔
418
        from sklearn.cluster import AgglomerativeClustering
1✔
419

420
        cluster_method = AgglomerativeClustering(
1✔
421
            linkage="complete",
422
            n_clusters=None,
423
            distance_threshold=distance_threshold,
424
        )
425
    layers = []
1✔
426
    for ii, x in enumerate(positions.T):
1✔
427
        cluster = cluster_method.fit(x.reshape(-1, 1))
1✔
428
        first_occurrences = np.unique(cluster.labels_, return_index=True)[1]
1✔
429
        permutation = x[first_occurrences].argsort().argsort()
1✔
430
        labels = permutation[cluster.labels_]
1✔
431
        if wrap_atoms and planes is None and structure.pbc[ii]:
1✔
432
            mean_positions = get_average_of_unique_labels(labels, positions)
1✔
433
            scaled_positions = np.einsum(
1✔
434
                "ji,nj->ni", np.linalg.inv(structure.cell), mean_positions
435
            )
436
            unique_inside_box = np.all(
1✔
437
                np.absolute(scaled_positions - 0.5 + 1.0e-8) < 0.5, axis=-1
438
            )
439
            arr_inside_box = np.any(
1✔
440
                labels[:, None] == np.unique(labels)[unique_inside_box][None, :],
441
                axis=-1,
442
            )
443
            first_occurences = np.unique(indices[arr_inside_box], return_index=True)[1]
1✔
444
            labels = labels[arr_inside_box]
1✔
445
            labels -= np.min(labels)
1✔
446
            labels = labels[first_occurences]
1✔
447
        layers.append(labels)
1✔
448
    if planes is not None and len(np.asarray(planes).shape) == 1:
1✔
449
        return np.asarray(layers).flatten()
1✔
450
    return np.vstack(layers).T
1✔
451

452

453
def get_voronoi_vertices(
1✔
454
    structure, epsilon=2.5e-4, distance_threshold=0, width_buffer=10
455
):
456
    """
457
    Get voronoi vertices of the box.
458

459
    Args:
460
        epsilon (float): displacement to add to avoid wrapping of atoms at borders
461
        distance_threshold (float): distance below which two vertices are considered as one.
462
            Agglomerative clustering algorithm (sklearn) is employed. Final positions are given
463
            as the average positions of clusters.
464
        width_buffer (float): width of the layer to be added to account for pbc.
465

466
    Returns:
467
        numpy.ndarray: 3d-array of vertices
468

469
    This function detect octahedral and tetrahedral sites in fcc; in bcc it detects tetrahedral
470
    sites. In defects (e.g. vacancy, dislocation, grain boundary etc.), it gives a list of
471
    positions interstitial atoms might want to occupy. In order for this to be more successful,
472
    it might make sense to look at the distance between the voronoi vertices and their nearest
473
    neighboring atoms via:
474

475
    >>> voronoi_vertices = structure_of_your_choice.analyse.get_voronoi_vertices()
476
    >>> neigh = structure_of_your_choice.get_neighborhood(voronoi_vertices)
477
    >>> print(neigh.distances.min(axis=-1))
478

479
    """
480
    voro = Voronoi(
1✔
481
        get_extended_positions(structure=structure, width=width_buffer) + epsilon
482
    )
483
    xx = voro.vertices
1✔
484
    if distance_threshold > 0:
1✔
485
        from sklearn.cluster import AgglomerativeClustering
1✔
486

487
        cluster = AgglomerativeClustering(
1✔
488
            linkage="single", distance_threshold=distance_threshold, n_clusters=None
489
        )
490
        cluster.fit(xx)
1✔
491
        xx = get_average_of_unique_labels(cluster.labels_, xx)
1✔
492
    xx = xx[
1✔
493
        np.linalg.norm(
494
            xx - get_wrapped_coordinates(structure=structure, positions=xx, epsilon=0),
495
            axis=-1,
496
        )
497
        < epsilon
498
    ]
499
    return xx - epsilon
1✔
500

501

502
def _get_neighbors(
1✔
503
    structure,
504
    position_interpreter,
505
    data_field: str,
506
    width_buffer: float = 10,
507
) -> np.ndarray:
508
    positions, indices = get_extended_positions(
1✔
509
        structure=structure, width=width_buffer, return_indices=True
510
    )
511
    interpretation = position_interpreter(positions)
1✔
512
    data = getattr(interpretation, data_field)
1✔
513
    x = positions[data]
1✔
514
    return indices[
1✔
515
        data[
516
            np.isclose(get_wrapped_coordinates(structure=structure, positions=x), x)
517
            .all(axis=-1)
518
            .any(axis=-1)
519
        ]
520
    ]
521

522

523
def get_voronoi_neighbors(structure, width_buffer: float = 10) -> np.ndarray:
1✔
524
    """
525
    Get pairs of atom indices sharing the same Voronoi vertices/areas.
526

527
    Args:
528
        width_buffer (float): Width of the layer to be added to account for pbc.
529

530
    Returns:
531
        pairs (ndarray): Pair indices
532
    """
533
    return _get_neighbors(
1✔
534
        structure=structure,
535
        position_interpreter=Voronoi,
536
        data_field="ridge_points",
537
        width_buffer=width_buffer,
538
    )
539

540

541
def get_delaunay_neighbors(structure, width_buffer: float = 10.0) -> np.ndarray:
1✔
542
    """
543
    Get indices of atoms sharing the same Delaunay tetrahedrons (commonly known as Delaunay
544
    triangles), i.e. indices of neighboring atoms, which form a tetrahedron, in which no other
545
    atom exists.
546

547
    Args:
548
        width_buffer (float): Width of the layer to be added to account for pbc.
549

550
    Returns:
551
        pairs (ndarray): Delaunay neighbor indices
552
    """
553
    return _get_neighbors(
1✔
554
        structure=structure,
555
        position_interpreter=Delaunay,
556
        data_field="simplices",
557
        width_buffer=width_buffer,
558
    )
559

560

561
def get_cluster_positions(
1✔
562
    structure, positions=None, eps=1, buffer_width=None, return_labels=False
563
):
564
    """
565
    Cluster positions according to the distances. Clustering algorithm uses DBSCAN:
566

567
    https://scikit-learn.org/stable/modules/generated/sklearn.cluster.DBSCAN.html
568

569
    Example I:
570

571
    ```
572
    analyse = Analyze(some_ase_structure)
573
    positions = analyse.cluster_points(eps=2)
574
    ```
575

576
    This example should return the atom positions, if no two atoms lie within a distance of 2.
577
    If there are at least two atoms which lie within a distance of 2, their entries will be
578
    replaced by their mean position.
579

580
    Example II:
581

582
    ```
583
    analyse = Analyze(some_ase_structure)
584
    print(analyse.cluster_positions([3*[0.], 3*[1.]], eps=3))
585
    ```
586

587
    This returns `[0.5, 0.5, 0.5]` (if the cell is large enough)
588

589
    Args:
590
        positions (numpy.ndarray): Positions to consider. Default: atom positions
591
        eps (float): The maximum distance between two samples for one to be considered as in
592
            the neighborhood of the other.
593
        buffer_width (float): Buffer width to consider across the periodic boundary
594
            conditions. If too small, it is possible that atoms that are meant to belong
595
            together across PBC are missed. Default: Same as eps
596
        return_labels (bool): Whether to return the labels given according to the grouping
597
            together with the mean positions
598

599
    Returns:
600
        positions (numpy.ndarray): Mean positions
601
        label (numpy.ndarray): Labels of the positions (returned when `return_labels = True`)
602
    """
603

604
    positions = structure.positions if positions is None else np.array(positions)
1✔
605
    if buffer_width is None:
1✔
606
        buffer_width = eps
1✔
607
    extended_positions, indices = get_extended_positions(
1✔
608
        structure=structure,
609
        width=buffer_width,
610
        return_indices=True,
611
        positions=positions,
612
    )
613
    labels = DBSCAN(eps=eps, min_samples=1).fit_predict(extended_positions)
1✔
614
    coo = coo_matrix((labels, (np.arange(len(labels)), indices)))
1✔
615
    labels = coo.max(axis=0).toarray().flatten()
1✔
616
    # make labels look nicer
617
    labels = np.unique(labels, return_inverse=True)[1]
1✔
618
    mean_positions = get_mean_positions(
1✔
619
        positions, structure.cell, structure.pbc, labels
620
    )
621
    if return_labels:
1✔
622
        return mean_positions, labels
1✔
623
    return mean_positions
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc