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

materialsproject / pymatgen / 4075885785

pending completion
4075885785

push

github

Shyue Ping Ong
Merge branch 'master' of github.com:materialsproject/pymatgen

96 of 96 new or added lines in 27 files covered. (100.0%)

81013 of 102710 relevant lines covered (78.88%)

0.79 hits per line

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

53.07
/pymatgen/analysis/chemenv/coordination_environments/voronoi.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3

4
"""
1✔
5
This module contains the object used to describe the possible bonded atoms based on a Voronoi analysis.
6
"""
7

8
from __future__ import annotations
1✔
9

10
import logging
1✔
11
import time
1✔
12

13
import numpy as np
1✔
14
from monty.json import MSONable
1✔
15
from scipy.spatial import Voronoi
1✔
16

17
from pymatgen.analysis.chemenv.utils.coordination_geometry_utils import (
1✔
18
    get_lower_and_upper_f,
19
    my_solid_angle,
20
    rectangle_surface_intersection,
21
)
22
from pymatgen.analysis.chemenv.utils.defs_utils import AdditionalConditions
1✔
23
from pymatgen.analysis.chemenv.utils.math_utils import normal_cdf_step
1✔
24
from pymatgen.core.sites import PeriodicSite
1✔
25
from pymatgen.core.structure import Structure
1✔
26

27
__author__ = "David Waroquiers"
1✔
28
__copyright__ = "Copyright 2012, The Materials Project"
1✔
29
__credits__ = "Geoffroy Hautier"
1✔
30
__version__ = "2.0"
1✔
31
__maintainer__ = "David Waroquiers"
1✔
32
__email__ = "david.waroquiers@gmail.com"
1✔
33
__date__ = "Feb 20, 2016"
1✔
34

35

36
def from_bson_voronoi_list2(bson_nb_voro_list2, structure):
1✔
37
    """
38
    Returns the voronoi_list needed for the VoronoiContainer object from a bson-encoded voronoi_list.
39

40
    Args:
41
        bson_nb_voro_list2: List of periodic sites involved in the Voronoi.
42
        structure: Structure object.
43

44
    Returns:
45
        The voronoi_list needed for the VoronoiContainer (with PeriodicSites as keys of the dictionary - not
46
        allowed in the BSON format).
47
    """
48
    voronoi_list = [None] * len(bson_nb_voro_list2)
1✔
49
    for isite, voro in enumerate(bson_nb_voro_list2):
1✔
50
        if voro is None or voro == "None":
1✔
51
            continue
×
52
        voronoi_list[isite] = []
1✔
53
        for psd, dd in voro:
1✔
54
            struct_site = structure[dd["index"]]
1✔
55
            periodic_site = PeriodicSite(
1✔
56
                struct_site._species,
57
                struct_site.frac_coords + psd[1],
58
                struct_site._lattice,
59
                properties=struct_site.properties,
60
            )
61
            dd["site"] = periodic_site
1✔
62
            voronoi_list[isite].append(dd)
1✔
63
    return voronoi_list
1✔
64

65

66
class DetailedVoronoiContainer(MSONable):
1✔
67
    """
68
    Class used to store the full Voronoi of a given structure.
69
    """
70

71
    AC = AdditionalConditions()
1✔
72
    default_voronoi_cutoff = 10.0
1✔
73
    default_normalized_distance_tolerance = 1e-5
1✔
74
    default_normalized_angle_tolerance = 1e-3
1✔
75

76
    def __init__(
1✔
77
        self,
78
        structure=None,
79
        voronoi_list2=None,
80
        voronoi_cutoff=default_voronoi_cutoff,
81
        isites=None,
82
        normalized_distance_tolerance=default_normalized_distance_tolerance,
83
        normalized_angle_tolerance=default_normalized_angle_tolerance,
84
        additional_conditions=None,
85
        valences=None,
86
        maximum_distance_factor=None,
87
        minimum_angle_factor=None,
88
    ):
89
        """
90
        Constructor for the VoronoiContainer object. Either a structure is given, in which case the Voronoi is
91
        computed, or the different components of the VoronoiContainer are given (used in the from_dict method).
92

93
        Args:
94
            structure: Structure for which the Voronoi is computed.
95
            voronoi_list2: List of voronoi polyhedrons for each site.
96
            voronoi_cutoff: cutoff used for the voronoi.
97
            isites: indices of sites for which the Voronoi has to be computed.
98
            normalized_distance_tolerance: Tolerance for two normalized distances to be considered equal.
99
            normalized_angle_tolerance:Tolerance for two normalized angles to be considered equal.
100
            additional_conditions: Additional conditions to be used.
101
            valences: Valences of all the sites in the structure (used when additional conditions require it).
102
            maximum_distance_factor: The maximum distance factor to be considered.
103
            minimum_angle_factor: The minimum angle factor to be considered.
104

105
        Raises:
106
            RuntimeError if the Voronoi cannot be constructed.
107
        """
108
        self.normalized_distance_tolerance = normalized_distance_tolerance
1✔
109
        self.normalized_angle_tolerance = normalized_angle_tolerance
1✔
110
        if additional_conditions is None:
1✔
111
            self.additional_conditions = [self.AC.NONE, self.AC.ONLY_ACB]
1✔
112
        else:
113
            self.additional_conditions = additional_conditions
1✔
114
        self.valences = valences
1✔
115
        self.maximum_distance_factor = maximum_distance_factor
1✔
116
        self.minimum_angle_factor = minimum_angle_factor
1✔
117
        if isites is None:
1✔
118
            indices = list(range(len(structure)))
1✔
119
        else:
120
            indices = isites
1✔
121
        self.structure = structure
1✔
122
        logging.debug("Setting Voronoi list")
1✔
123
        if voronoi_list2 is not None:
1✔
124
            self.voronoi_list2 = voronoi_list2
1✔
125
        else:
126
            self.setup_voronoi_list(indices=indices, voronoi_cutoff=voronoi_cutoff)
1✔
127
        logging.debug("Setting neighbors distances and angles")
1✔
128
        t1 = time.process_time()
1✔
129
        self.setup_neighbors_distances_and_angles(indices=indices)
1✔
130
        t2 = time.process_time()
1✔
131
        logging.debug(f"Neighbors distances and angles set up in {t2 - t1:.2f} seconds")
1✔
132

133
    def setup_voronoi_list(self, indices, voronoi_cutoff):
1✔
134
        """
135
        Set up of the voronoi list of neighbors by calling qhull.
136

137
        Args:
138
            indices: indices of the sites for which the Voronoi is needed.
139
            voronoi_cutoff: Voronoi cutoff for the search of neighbors.
140

141
        Raises:
142
            RuntimeError: If an infinite vertex is found in the voronoi construction.
143
        """
144
        self.voronoi_list2 = [None] * len(self.structure)
1✔
145
        self.voronoi_list_coords = [None] * len(self.structure)
1✔
146
        logging.debug("Getting all neighbors in structure")
1✔
147
        struct_neighbors = self.structure.get_all_neighbors(voronoi_cutoff, include_index=True)
1✔
148
        size_neighbors = [(not len(neigh) > 3) for neigh in struct_neighbors]
1✔
149
        if np.any(size_neighbors):
1✔
150
            logging.debug("Please consider increasing voronoi_distance_cutoff")
×
151
        t1 = time.process_time()
1✔
152
        logging.debug("Setting up Voronoi list :")
1✔
153
        for jj, isite in enumerate(indices):
1✔
154
            logging.debug(f"  - Voronoi analysis for site #{isite:d} ({jj + 1:d}/{len(indices):d})")
1✔
155
            site = self.structure[isite]
1✔
156
            neighbors1 = [(site, 0.0, isite)]
1✔
157
            neighbors1.extend(struct_neighbors[isite])
1✔
158
            distances = [i[1] for i in sorted(neighbors1, key=lambda s: s[1])]
1✔
159
            neighbors = [i[0] for i in sorted(neighbors1, key=lambda s: s[1])]
1✔
160
            qvoronoi_input = [s.coords for s in neighbors]
1✔
161
            voro = Voronoi(points=qvoronoi_input, qhull_options="o Fv")
1✔
162
            all_vertices = voro.vertices
1✔
163

164
            results2 = []
1✔
165
            maxangle = 0.0
1✔
166
            mindist = 10000.0
1✔
167
            for iridge, ridge_points in enumerate(voro.ridge_points):
1✔
168
                if 0 in ridge_points:
1✔
169
                    ridge_vertices_indices = voro.ridge_vertices[iridge]
1✔
170
                    if -1 in ridge_vertices_indices:
1✔
171
                        raise RuntimeError(
×
172
                            "This structure is pathological, infinite vertex in the voronoi construction"
173
                        )
174

175
                    ridge_point2 = max(ridge_points)
1✔
176
                    facets = [all_vertices[i] for i in ridge_vertices_indices]
1✔
177
                    sa = my_solid_angle(site.coords, facets)
1✔
178
                    maxangle = max([sa, maxangle])
1✔
179

180
                    mindist = min([mindist, distances[ridge_point2]])
1✔
181
                    for iii, sss in enumerate(self.structure):
1✔
182
                        if neighbors[ridge_point2].is_periodic_image(sss, tolerance=1.0e-6):
1✔
183
                            myindex = iii
1✔
184
                            break
1✔
185
                    results2.append(
1✔
186
                        {
187
                            "site": neighbors[ridge_point2],
188
                            "angle": sa,
189
                            "distance": distances[ridge_point2],
190
                            "index": myindex,
191
                        }
192
                    )
193
            for dd in results2:
1✔
194
                dd["normalized_angle"] = dd["angle"] / maxangle
1✔
195
                dd["normalized_distance"] = dd["distance"] / mindist
1✔
196
            self.voronoi_list2[isite] = results2
1✔
197
            self.voronoi_list_coords[isite] = np.array([dd["site"].coords for dd in results2])
1✔
198
        t2 = time.process_time()
1✔
199
        logging.debug(f"Voronoi list set up in {t2 - t1:.2f} seconds")
1✔
200

201
    def setup_neighbors_distances_and_angles(self, indices):
1✔
202
        """
203
        Initializes the angle and distance separations.
204

205
        Args:
206
            indices: Indices of the sites for which the Voronoi is needed.
207
        """
208
        self.neighbors_distances = [None] * len(self.structure)
1✔
209
        self.neighbors_normalized_distances = [None] * len(self.structure)
1✔
210
        self.neighbors_angles = [None] * len(self.structure)
1✔
211
        self.neighbors_normalized_angles = [None] * len(self.structure)
1✔
212
        for isite in indices:
1✔
213
            results = self.voronoi_list2[isite]
1✔
214
            if results is None:
1✔
215
                continue
1✔
216
            # Initializes neighbors distances and normalized distances groups
217
            self.neighbors_distances[isite] = []
1✔
218
            self.neighbors_normalized_distances[isite] = []
1✔
219
            normalized_distances = [nb_dict["normalized_distance"] for nb_dict in results]
1✔
220
            isorted_distances = np.argsort(normalized_distances)
1✔
221
            self.neighbors_normalized_distances[isite].append(
1✔
222
                {
223
                    "min": normalized_distances[isorted_distances[0]],
224
                    "max": normalized_distances[isorted_distances[0]],
225
                }
226
            )
227
            self.neighbors_distances[isite].append(
1✔
228
                {
229
                    "min": results[isorted_distances[0]]["distance"],
230
                    "max": results[isorted_distances[0]]["distance"],
231
                }
232
            )
233
            icurrent = 0
1✔
234
            nb_indices = {int(isorted_distances[0])}
1✔
235
            dnb_indices = {int(isorted_distances[0])}
1✔
236
            for idist in iter(isorted_distances):
1✔
237
                wd = normalized_distances[idist]
1✔
238
                if self.maximum_distance_factor is not None:
1✔
239
                    if wd > self.maximum_distance_factor:
1✔
240
                        self.neighbors_normalized_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
1✔
241
                        self.neighbors_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
1✔
242
                        self.neighbors_normalized_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
1✔
243
                        self.neighbors_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
1✔
244
                        break
1✔
245
                if np.isclose(
1✔
246
                    wd,
247
                    self.neighbors_normalized_distances[isite][icurrent]["max"],
248
                    rtol=0.0,
249
                    atol=self.normalized_distance_tolerance,
250
                ):
251
                    self.neighbors_normalized_distances[isite][icurrent]["max"] = wd
1✔
252
                    self.neighbors_distances[isite][icurrent]["max"] = results[idist]["distance"]
1✔
253
                    dnb_indices.add(int(idist))
1✔
254
                else:
255
                    self.neighbors_normalized_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
1✔
256
                    self.neighbors_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
1✔
257
                    self.neighbors_normalized_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
1✔
258
                    self.neighbors_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
1✔
259
                    dnb_indices = {int(idist)}
1✔
260
                    self.neighbors_normalized_distances[isite].append({"min": wd, "max": wd})
1✔
261
                    self.neighbors_distances[isite].append(
1✔
262
                        {
263
                            "min": results[idist]["distance"],
264
                            "max": results[idist]["distance"],
265
                        }
266
                    )
267
                    icurrent += 1
1✔
268
                nb_indices.add(int(idist))
1✔
269
            else:
270
                self.neighbors_normalized_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
1✔
271
                self.neighbors_distances[isite][icurrent]["nb_indices"] = list(nb_indices)
1✔
272
                self.neighbors_normalized_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
1✔
273
                self.neighbors_distances[isite][icurrent]["dnb_indices"] = list(dnb_indices)
1✔
274
            for idist in range(len(self.neighbors_distances[isite]) - 1):
1✔
275
                dist_dict = self.neighbors_distances[isite][idist]
1✔
276
                dist_dict_next = self.neighbors_distances[isite][idist + 1]
1✔
277
                dist_dict["next"] = dist_dict_next["min"]
1✔
278
                ndist_dict = self.neighbors_normalized_distances[isite][idist]
1✔
279
                ndist_dict_next = self.neighbors_normalized_distances[isite][idist + 1]
1✔
280
                ndist_dict["next"] = ndist_dict_next["min"]
1✔
281
            if self.maximum_distance_factor is not None:
1✔
282
                dfact = self.maximum_distance_factor
1✔
283
            else:
284
                dfact = self.default_voronoi_cutoff / self.neighbors_distances[isite][0]["min"]
1✔
285
            self.neighbors_normalized_distances[isite][-1]["next"] = dfact
1✔
286
            self.neighbors_distances[isite][-1]["next"] = dfact * self.neighbors_distances[isite][0]["min"]
1✔
287
            # Initializes neighbors angles and normalized angles groups
288
            self.neighbors_angles[isite] = []
1✔
289
            self.neighbors_normalized_angles[isite] = []
1✔
290
            normalized_angles = [nb_dict["normalized_angle"] for nb_dict in results]
1✔
291
            isorted_angles = np.argsort(normalized_angles)[::-1]
1✔
292
            self.neighbors_normalized_angles[isite].append(
1✔
293
                {
294
                    "max": normalized_angles[isorted_angles[0]],
295
                    "min": normalized_angles[isorted_angles[0]],
296
                }
297
            )
298
            self.neighbors_angles[isite].append(
1✔
299
                {
300
                    "max": results[isorted_angles[0]]["angle"],
301
                    "min": results[isorted_angles[0]]["angle"],
302
                }
303
            )
304
            icurrent = 0
1✔
305
            nb_indices = {int(isorted_angles[0])}
1✔
306
            dnb_indices = {int(isorted_angles[0])}
1✔
307
            for iang in iter(isorted_angles):
1✔
308
                wa = normalized_angles[iang]
1✔
309
                if self.minimum_angle_factor is not None:
1✔
310
                    if wa < self.minimum_angle_factor:
1✔
311
                        self.neighbors_normalized_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
1✔
312
                        self.neighbors_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
1✔
313
                        self.neighbors_normalized_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
1✔
314
                        self.neighbors_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
1✔
315
                        break
1✔
316
                if np.isclose(
1✔
317
                    wa,
318
                    self.neighbors_normalized_angles[isite][icurrent]["min"],
319
                    rtol=0.0,
320
                    atol=self.normalized_angle_tolerance,
321
                ):
322
                    self.neighbors_normalized_angles[isite][icurrent]["min"] = wa
1✔
323
                    self.neighbors_angles[isite][icurrent]["min"] = results[iang]["angle"]
1✔
324
                    dnb_indices.add(int(iang))
1✔
325
                else:
326
                    self.neighbors_normalized_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
1✔
327
                    self.neighbors_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
1✔
328
                    self.neighbors_normalized_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
1✔
329
                    self.neighbors_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
1✔
330
                    dnb_indices = {int(iang)}
1✔
331
                    self.neighbors_normalized_angles[isite].append({"max": wa, "min": wa})
1✔
332
                    self.neighbors_angles[isite].append({"max": results[iang]["angle"], "min": results[iang]["angle"]})
1✔
333
                    icurrent += 1
1✔
334
                nb_indices.add(int(iang))
1✔
335
            else:
336
                self.neighbors_normalized_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
1✔
337
                self.neighbors_angles[isite][icurrent]["nb_indices"] = list(nb_indices)
1✔
338
                self.neighbors_normalized_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
1✔
339
                self.neighbors_angles[isite][icurrent]["dnb_indices"] = list(dnb_indices)
1✔
340
            for iang in range(len(self.neighbors_angles[isite]) - 1):
1✔
341
                ang_dict = self.neighbors_angles[isite][iang]
1✔
342
                ang_dict_next = self.neighbors_angles[isite][iang + 1]
1✔
343
                ang_dict["next"] = ang_dict_next["max"]
1✔
344
                nang_dict = self.neighbors_normalized_angles[isite][iang]
1✔
345
                nang_dict_next = self.neighbors_normalized_angles[isite][iang + 1]
1✔
346
                nang_dict["next"] = nang_dict_next["max"]
1✔
347
            if self.minimum_angle_factor is not None:
1✔
348
                afact = self.minimum_angle_factor
1✔
349
            else:
350
                afact = 0.0
1✔
351
            self.neighbors_normalized_angles[isite][-1]["next"] = afact
1✔
352
            self.neighbors_angles[isite][-1]["next"] = afact * self.neighbors_angles[isite][0]["max"]
1✔
353

354
    def _precompute_additional_conditions(self, ivoronoi, voronoi, valences):
1✔
355
        additional_conditions = {ac: [] for ac in self.additional_conditions}
×
356
        for _, vals in voronoi:
×
357
            for ac in self.additional_conditions:
×
358
                additional_conditions[ac].append(
×
359
                    self.AC.check_condition(
360
                        condition=ac,
361
                        structure=self.structure,
362
                        parameters={
363
                            "valences": valences,
364
                            "neighbor_index": vals["index"],
365
                            "site_index": ivoronoi,
366
                        },
367
                    )
368
                )
369
        return additional_conditions
×
370

371
    def _precompute_distance_conditions(self, ivoronoi, voronoi):
1✔
372
        distance_conditions = []
×
373
        for idp, dp_dict in enumerate(self.neighbors_normalized_distances[ivoronoi]):
×
374
            distance_conditions.append([])
×
375
            dp = dp_dict["max"]
×
376
            for _, vals in voronoi:
×
377
                distance_conditions[idp].append(
×
378
                    vals["normalized_distance"] <= dp
379
                    or np.isclose(
380
                        vals["normalized_distance"],
381
                        dp,
382
                        rtol=0.0,
383
                        atol=self.normalized_distance_tolerance / 2.0,
384
                    )
385
                )
386
        return distance_conditions
×
387

388
    def _precompute_angle_conditions(self, ivoronoi, voronoi):
1✔
389
        angle_conditions = []
×
390
        for iap, ap_dict in enumerate(self.neighbors_normalized_angles[ivoronoi]):
×
391
            angle_conditions.append([])
×
392
            ap = ap_dict["max"]
×
393
            for _, vals in voronoi:
×
394
                angle_conditions[iap].append(
×
395
                    vals["normalized_angle"] >= ap
396
                    or np.isclose(
397
                        vals["normalized_angle"],
398
                        ap,
399
                        rtol=0.0,
400
                        atol=self.normalized_angle_tolerance / 2.0,
401
                    )
402
                )
403
        return angle_conditions
×
404

405
    # def neighbors_map(self, isite, distfactor, angfactor, additional_condition):
406
    #     if self.neighbors_normalized_distances[isite] is None:
407
    #         return None
408
    #     dist_where = np.argwhere(
409
    #         np.array([wd['min'] for wd in self.neighbors_normalized_distances[isite]]) <= distfactor)
410
    #     if len(dist_where) == 0:
411
    #         return None
412
    #     idist = dist_where[-1][0]
413
    #     ang_where = np.argwhere(np.array([wa['max'] for wa in self.neighbors_normalized_angles[isite]]) >= angfactor)
414
    #     if len(ang_where) == 0:
415
    #         return None
416
    #     iang = ang_where[0][0]
417
    #     if self.additional_conditions.count(additional_condition) != 1:
418
    #         return None
419
    #     i_additional_condition = self.additional_conditions.index(additional_condition)
420
    #     return {'i_distfactor': idist, 'i_angfactor': iang, 'i_additional_condition': i_additional_condition}
421

422
    def neighbors_surfaces(self, isite, surface_calculation_type=None, max_dist=2.0):
1✔
423
        """
424
        Get the different surfaces corresponding to the different distance-angle cutoffs for a given site.
425

426
        Args:
427
            isite: Index of the site
428
            surface_calculation_type: How to compute the surface.
429
            max_dist: The maximum distance factor to be considered.
430

431
        Returns:
432
            Surfaces for each distance-angle cutoff.
433
        """
434
        if self.voronoi_list2[isite] is None:
×
435
            return None
×
436
        bounds_and_limits = self.voronoi_parameters_bounds_and_limits(isite, surface_calculation_type, max_dist)
×
437
        distance_bounds = bounds_and_limits["distance_bounds"]
×
438
        angle_bounds = bounds_and_limits["angle_bounds"]
×
439
        surfaces = np.zeros((len(distance_bounds), len(angle_bounds)), np.float_)
×
440
        for idp in range(len(distance_bounds) - 1):
×
441
            this_dist_plateau = distance_bounds[idp + 1] - distance_bounds[idp]
×
442
            for iap in range(len(angle_bounds) - 1):
×
443
                this_ang_plateau = angle_bounds[iap + 1] - angle_bounds[iap]
×
444
                surfaces[idp][iap] = np.absolute(this_dist_plateau * this_ang_plateau)
×
445
        return surfaces
×
446

447
    def neighbors_surfaces_bounded(self, isite, surface_calculation_options=None):
1✔
448
        """
449
        Get the different surfaces (using boundaries) corresponding to the different distance-angle cutoffs
450
        for a given site.
451

452
        Args:
453
            isite: Index of the site.
454
            surface_calculation_options: Options for the boundaries.
455

456
        Returns:
457
            Surfaces for each distance-angle cutoff.
458
        """
459
        if self.voronoi_list2[isite] is None:
×
460
            return None
×
461
        if surface_calculation_options is None:
×
462
            surface_calculation_options = {
×
463
                "type": "standard_elliptic",
464
                "distance_bounds": {"lower": 1.2, "upper": 1.8},
465
                "angle_bounds": {"lower": 0.1, "upper": 0.8},
466
            }
467
        if surface_calculation_options["type"] in [
×
468
            "standard_elliptic",
469
            "standard_diamond",
470
            "standard_spline",
471
        ]:
472
            plot_type = {
×
473
                "distance_parameter": ("initial_normalized", None),
474
                "angle_parameter": ("initial_normalized", None),
475
            }
476
        else:
477
            raise ValueError(
×
478
                f'Type {surface_calculation_options["type"]!r} for the surface calculation in DetailedVoronoiContainer '
479
                "is invalid"
480
            )
481
        max_dist = surface_calculation_options["distance_bounds"]["upper"] + 0.1
×
482
        bounds_and_limits = self.voronoi_parameters_bounds_and_limits(
×
483
            isite=isite, plot_type=plot_type, max_dist=max_dist
484
        )
485

486
        distance_bounds = bounds_and_limits["distance_bounds"]
×
487
        angle_bounds = bounds_and_limits["angle_bounds"]
×
488
        lower_and_upper_functions = get_lower_and_upper_f(surface_calculation_options=surface_calculation_options)
×
489
        mindist = surface_calculation_options["distance_bounds"]["lower"]
×
490
        maxdist = surface_calculation_options["distance_bounds"]["upper"]
×
491
        minang = surface_calculation_options["angle_bounds"]["lower"]
×
492
        maxang = surface_calculation_options["angle_bounds"]["upper"]
×
493

494
        f_lower = lower_and_upper_functions["lower"]
×
495
        f_upper = lower_and_upper_functions["upper"]
×
496
        surfaces = np.zeros((len(distance_bounds), len(angle_bounds)), np.float_)
×
497
        for idp in range(len(distance_bounds) - 1):
×
498
            dp1 = distance_bounds[idp]
×
499
            dp2 = distance_bounds[idp + 1]
×
500
            if dp2 < mindist or dp1 > maxdist:
×
501
                continue
×
502
            if dp1 < mindist:
×
503
                d1 = mindist
×
504
            else:
505
                d1 = dp1
×
506
            if dp2 > maxdist:
×
507
                d2 = maxdist
×
508
            else:
509
                d2 = dp2
×
510
            for iap in range(len(angle_bounds) - 1):
×
511
                ap1 = angle_bounds[iap]
×
512
                ap2 = angle_bounds[iap + 1]
×
513
                if ap1 > ap2:
×
514
                    ap1 = angle_bounds[iap + 1]
×
515
                    ap2 = angle_bounds[iap]
×
516
                if ap2 < minang or ap1 > maxang:
×
517
                    continue
×
518
                intersection, interror = rectangle_surface_intersection(
×
519
                    rectangle=((d1, d2), (ap1, ap2)),
520
                    f_lower=f_lower,
521
                    f_upper=f_upper,
522
                    bounds_lower=[mindist, maxdist],
523
                    bounds_upper=[mindist, maxdist],
524
                    check=False,
525
                )
526
                surfaces[idp][iap] = intersection
×
527
        return surfaces
×
528

529
    @staticmethod
1✔
530
    def _get_vertices_dist_ang_indices(parameter_indices_list):
1✔
531
        pp0 = [pp[0] for pp in parameter_indices_list]
1✔
532
        pp1 = [pp[1] for pp in parameter_indices_list]
1✔
533
        min_idist = min(pp0)
1✔
534
        min_iang = min(pp1)
1✔
535
        max_idist = max(pp0)
1✔
536
        max_iang = max(pp1)
1✔
537
        i_min_angs = np.argwhere(np.array(pp1) == min_iang)
1✔
538
        i_max_dists = np.argwhere(np.array(pp0) == max_idist)
1✔
539
        pp0_at_min_iang = [pp0[ii[0]] for ii in i_min_angs]
1✔
540
        pp1_at_max_idist = [pp1[ii[0]] for ii in i_max_dists]
1✔
541
        max_idist_at_min_iang = max(pp0_at_min_iang)
1✔
542
        min_iang_at_max_idist = min(pp1_at_max_idist)
1✔
543

544
        p1 = (min_idist, min_iang)
1✔
545
        p2 = (max_idist_at_min_iang, min_iang)
1✔
546
        p3 = (max_idist_at_min_iang, min_iang_at_max_idist)
1✔
547
        p4 = (max_idist, min_iang_at_max_idist)
1✔
548
        p5 = (max_idist, max_iang)
1✔
549
        p6 = (min_idist, max_iang)
1✔
550

551
        return [p1, p2, p3, p4, p5, p6]
1✔
552

553
    def maps_and_surfaces(
1✔
554
        self,
555
        isite,
556
        surface_calculation_type=None,
557
        max_dist=2.0,
558
        additional_conditions=None,
559
    ):
560
        """
561
        Get the different surfaces and their cn_map corresponding to the different distance-angle cutoffs
562
        for a given site.
563

564
        Args:
565
            isite: Index of the site
566
            surface_calculation_type: How to compute the surface.
567
            max_dist: The maximum distance factor to be considered.
568
            additional_conditions: If additional conditions have to be considered.
569

570
        Returns:
571
            Surfaces and cn_map's for each distance-angle cutoff.
572
        """
573
        if self.voronoi_list2[isite] is None:
×
574
            return None
×
575
        if additional_conditions is None:
×
576
            additional_conditions = [self.AC.ONLY_ACB]
×
577
        surfaces = self.neighbors_surfaces(
×
578
            isite=isite,
579
            surface_calculation_type=surface_calculation_type,
580
            max_dist=max_dist,
581
        )
582
        maps_and_surfaces = []
×
583
        for cn, value in self._unique_coordinated_neighbors_parameters_indices[isite].items():  # pylint: disable=E1101
×
584
            for imap, list_parameters_indices in enumerate(value):
×
585
                thissurf = 0.0
×
586
                for idp, iap, iacb in list_parameters_indices:
×
587
                    if iacb in additional_conditions:
×
588
                        thissurf += surfaces[idp, iap]
×
589
                maps_and_surfaces.append(
×
590
                    {
591
                        "map": (cn, imap),
592
                        "surface": thissurf,
593
                        "parameters_indices": list_parameters_indices,
594
                    }
595
                )
596
        return maps_and_surfaces
×
597

598
    def maps_and_surfaces_bounded(self, isite, surface_calculation_options=None, additional_conditions=None):
1✔
599
        """
600
        Get the different surfaces (using boundaries) and their cn_map corresponding to the different
601
        distance-angle cutoffs for a given site.
602

603
        Args:
604
            isite: Index of the site
605
            surface_calculation_options: Options for the boundaries.
606
            additional_conditions: If additional conditions have to be considered.
607

608
        Returns:
609
            Surfaces and cn_map's for each distance-angle cutoff.
610
        """
611
        if self.voronoi_list2[isite] is None:
×
612
            return None
×
613
        if additional_conditions is None:
×
614
            additional_conditions = [self.AC.ONLY_ACB]
×
615
        surfaces = self.neighbors_surfaces_bounded(isite=isite, surface_calculation_options=surface_calculation_options)
×
616
        maps_and_surfaces = []
×
617
        for cn, value in self._unique_coordinated_neighbors_parameters_indices[isite].items():  # pylint: disable=E1101
×
618
            for imap, list_parameters_indices in enumerate(value):
×
619
                thissurf = 0.0
×
620
                for idp, iap, iacb in list_parameters_indices:
×
621
                    if iacb in additional_conditions:
×
622
                        thissurf += surfaces[idp, iap]
×
623
                maps_and_surfaces.append(
×
624
                    {
625
                        "map": (cn, imap),
626
                        "surface": thissurf,
627
                        "parameters_indices": list_parameters_indices,
628
                    }
629
                )
630
        return maps_and_surfaces
×
631

632
    def neighbors(self, isite, distfactor, angfactor, additional_condition=None):
1✔
633
        """
634
        Get the neighbors of a given site corresponding to a given distance and angle factor.
635

636
        Args:
637
            isite: Index of the site.
638
            distfactor: Distance factor.
639
            angfactor: Angle factor.
640
            additional_condition: Additional condition to be used (currently not implemented).
641

642
        Returns:
643
            List of neighbors of the given site for the given distance and angle factors.
644
        """
645
        idist = None
1✔
646
        dfact = None
1✔
647
        for iwd, wd in enumerate(self.neighbors_normalized_distances[isite]):
1✔
648
            if distfactor >= wd["min"]:
1✔
649
                idist = iwd
1✔
650
                dfact = wd["max"]
1✔
651
            else:
652
                break
1✔
653
        iang = None
1✔
654
        afact = None
1✔
655
        for iwa, wa in enumerate(self.neighbors_normalized_angles[isite]):
1✔
656
            if angfactor <= wa["max"]:
1✔
657
                iang = iwa
1✔
658
                afact = wa["min"]
1✔
659
            else:
660
                break
×
661
        if idist is None or iang is None:
1✔
662
            raise ValueError("Distance or angle parameter not found ...")
×
663

664
        return [
1✔
665
            nb
666
            for nb in self.voronoi_list2[isite]
667
            if nb["normalized_distance"] <= dfact and nb["normalized_angle"] >= afact
668
        ]
669

670
    def voronoi_parameters_bounds_and_limits(self, isite, plot_type, max_dist):
1✔
671
        """
672
        Get the different boundaries and limits of the distance and angle factors for the given site.
673

674
        Args:
675
            isite: Index of the site.
676
            plot_type: Types of distance/angle parameters to get.
677
            max_dist: Maximum distance factor.
678

679
        Returns:
680
            Distance and angle bounds and limits.
681
        """
682
        # Initializes the distance and angle parameters
683
        if self.voronoi_list2[isite] is None:
×
684
            return None
×
685
        if plot_type is None:
×
686
            plot_type = {
×
687
                "distance_parameter": ("initial_inverse_opposite", None),
688
                "angle_parameter": ("initial_opposite", None),
689
            }
690
        dd = [dist["min"] for dist in self.neighbors_normalized_distances[isite]]
×
691
        dd[0] = 1.0
×
692
        if plot_type["distance_parameter"][0] == "initial_normalized":
×
693
            dd.append(max_dist)
×
694
            distance_bounds = np.array(dd)
×
695
            dist_limits = [1.0, max_dist]
×
696
        elif plot_type["distance_parameter"][0] == "initial_inverse_opposite":
×
697
            ddinv = [1.0 / dist for dist in dd]
×
698
            ddinv.append(0.0)
×
699
            distance_bounds = np.array([1.0 - invdist for invdist in ddinv])
×
700
            dist_limits = [0.0, 1.0]
×
701
        elif plot_type["distance_parameter"][0] == "initial_inverse3_opposite":
×
702
            ddinv = [1.0 / dist**3.0 for dist in dd]
×
703
            ddinv.append(0.0)
×
704
            distance_bounds = np.array([1.0 - invdist for invdist in ddinv])
×
705
            dist_limits = [0.0, 1.0]
×
706
        else:
707
            raise NotImplementedError(
×
708
                f"Plotting type {plot_type['distance_parameter']!r} for the distance is not implemented"
709
            )
710
        if plot_type["angle_parameter"][0] == "initial_normalized":
×
711
            aa = [0.0]
×
712
            aa.extend([ang["max"] for ang in self.neighbors_normalized_angles[isite]])
×
713
            angle_bounds = np.array(aa)
×
714
        elif plot_type["angle_parameter"][0] == "initial_opposite":
×
715
            aa = [0.0]
×
716
            aa.extend([ang["max"] for ang in self.neighbors_normalized_angles[isite]])
×
717
            aa = [1.0 - ang for ang in aa]
×
718
            angle_bounds = np.array(aa)
×
719
        else:
720
            raise NotImplementedError(
×
721
                f"Plotting type {plot_type['angle_parameter']!r} for the angle is not implemented"
722
            )
723
        ang_limits = [0.0, 1.0]
×
724
        return {
×
725
            "distance_bounds": distance_bounds,
726
            "distance_limits": dist_limits,
727
            "angle_bounds": angle_bounds,
728
            "angle_limits": ang_limits,
729
        }
730

731
    def is_close_to(self, other, rtol=0.0, atol=1e-8) -> bool:
1✔
732
        """
733
        Whether two DetailedVoronoiContainer objects are close to each other.
734

735
        Args:
736
            other: Another DetailedVoronoiContainer to be compared with.
737
            rtol: Relative tolerance to compare values.
738
            atol: Absolute tolerance to compare values.
739

740
        Returns:
741
            True if the two DetailedVoronoiContainer are close to each other.
742
        """
743
        isclose = (
×
744
            np.isclose(
745
                self.normalized_angle_tolerance,
746
                other.normalized_angle_tolerance,
747
                rtol=rtol,
748
                atol=atol,
749
            )
750
            and np.isclose(
751
                self.normalized_distance_tolerance,
752
                other.normalized_distance_tolerance,
753
                rtol=rtol,
754
                atol=atol,
755
            )
756
            and self.additional_conditions == other.additional_conditions
757
            and self.valences == other.valences
758
        )
759
        if not isclose:
×
760
            return isclose
×
761
        for isite, site_voronoi in enumerate(self.voronoi_list2):
×
762
            self_to_other_nbs = {}
×
763
            for inb, nb in enumerate(site_voronoi):
×
764
                if nb is None:
×
765
                    if other.voronoi_list2[isite] is None:
×
766
                        continue
×
767
                    return False
×
768
                if other.voronoi_list2[isite] is None:
×
769
                    return False
×
770
                nb_other = None
×
771
                for inb2, nb2 in enumerate(other.voronoi_list2[isite]):
×
772
                    if nb["site"] == nb2["site"]:
×
773
                        self_to_other_nbs[inb] = inb2
×
774
                        nb_other = nb2
×
775
                        break
×
776
                if nb_other is None:
×
777
                    return False
×
778
                if not np.isclose(nb["distance"], nb_other["distance"], rtol=rtol, atol=atol):
×
779
                    return False
×
780
                if not np.isclose(nb["angle"], nb_other["angle"], rtol=rtol, atol=atol):
×
781
                    return False
×
782
                if not np.isclose(
×
783
                    nb["normalized_distance"],
784
                    nb_other["normalized_distance"],
785
                    rtol=rtol,
786
                    atol=atol,
787
                ):
788
                    return False
×
789
                if not np.isclose(
×
790
                    nb["normalized_angle"],
791
                    nb_other["normalized_angle"],
792
                    rtol=rtol,
793
                    atol=atol,
794
                ):
795
                    return False
×
796
                if nb["index"] != nb_other["index"]:
×
797
                    return False
×
798
                if nb["site"] != nb_other["site"]:
×
799
                    return False
×
800
        return True
×
801

802
    def get_rdf_figure(self, isite, normalized=True, figsize=None, step_function=None):
1✔
803
        """
804
        Get the Radial Distribution Figure for a given site.
805

806
        Args:
807
            isite: Index of the site.
808
            normalized: Whether to normalize distances.
809
            figsize: Size of the figure.
810
            step_function: Type of step function to be used for the RDF.
811

812
        Returns:
813
            Matplotlib figure.
814
        """
815

816
        def dp_func(dp):
×
817
            return 1.0 - 1.0 / np.power(dp, 3.0)
×
818

819
        import matplotlib.pyplot as plt
×
820

821
        if step_function is None:
×
822
            step_function = {"type": "normal_cdf", "scale": 0.0001}
×
823

824
        # Initializes the figure
825
        if figsize is None:
×
826
            fig = plt.figure()
×
827
        else:
828
            fig = plt.figure(figsize=figsize)
×
829
        subplot = fig.add_subplot(111)
×
830
        if normalized:
×
831
            dists = self.neighbors_normalized_distances[isite]
×
832
        else:
833
            dists = self.neighbors_distances[isite]
×
834

835
        if step_function["type"] == "step_function":
×
836
            isorted = np.argsort([dd["min"] for dd in dists])
×
837
            sorted_dists = [dists[ii]["min"] for ii in isorted]
×
838
            dnb_dists = [len(dists[ii]["dnb_indices"]) for ii in isorted]
×
839
            xx = [0.0]
×
840
            yy = [0.0]
×
841
            for idist, dist in enumerate(sorted_dists):
×
842
                xx.append(dist)
×
843
                xx.append(dist)
×
844
                yy.append(yy[-1])
×
845
                yy.append(yy[-1] + dnb_dists[idist])
×
846
            xx.append(1.1 * xx[-1])
×
847
            yy.append(yy[-1])
×
848
        elif step_function["type"] == "normal_cdf":
×
849
            scale = step_function["scale"]
×
850
            mydists = [dp_func(dd["min"]) for dd in dists]
×
851
            mydcns = [len(dd["dnb_indices"]) for dd in dists]
×
852
            xx = np.linspace(0.0, 1.1 * max(mydists), num=500)
×
853
            yy = np.zeros_like(xx)
×
854
            for idist, dist in enumerate(mydists):
×
855
                yy += mydcns[idist] * normal_cdf_step(xx, mean=dist, scale=scale)
×
856
        else:
857
            raise ValueError(f"Step function of type {step_function['type']!r} is not allowed")
×
858
        subplot.plot(xx, yy)
×
859

860
        return fig
×
861

862
    def get_sadf_figure(self, isite, normalized=True, figsize=None, step_function=None):
1✔
863
        """
864
        Get the Solid Angle Distribution Figure for a given site.
865
        Args:
866
            isite: Index of the site.
867
            normalized: Whether to normalize angles.
868
            figsize: Size of the figure.
869
            step_function: Type of step function to be used for the SADF.
870

871
        Returns:
872
            Matplotlib figure.
873
        """
874

875
        def ap_func(ap):
×
876
            return np.power(ap, -0.1)
×
877

878
        import matplotlib.pyplot as plt
×
879

880
        if step_function is None:
×
881
            step_function = {"type": "step_function", "scale": 0.0001}
×
882

883
        # Initializes the figure
884
        if figsize is None:
×
885
            fig = plt.figure()
×
886
        else:
887
            fig = plt.figure(figsize=figsize)
×
888
        subplot = fig.add_subplot(111)
×
889
        if normalized:
×
890
            angs = self.neighbors_normalized_angles[isite]
×
891
        else:
892
            angs = self.neighbors_angles[isite]
×
893

894
        if step_function["type"] == "step_function":
×
895
            isorted = np.argsort([ap_func(aa["min"]) for aa in angs])
×
896
            sorted_angs = [ap_func(angs[ii]["min"]) for ii in isorted]
×
897
            dnb_angs = [len(angs[ii]["dnb_indices"]) for ii in isorted]
×
898
            xx = [0.0]
×
899
            yy = [0.0]
×
900
            for iang, ang in enumerate(sorted_angs):
×
901
                xx.append(ang)
×
902
                xx.append(ang)
×
903
                yy.append(yy[-1])
×
904
                yy.append(yy[-1] + dnb_angs[iang])
×
905
            xx.append(1.1 * xx[-1])
×
906
            yy.append(yy[-1])
×
907
        elif step_function["type"] == "normal_cdf":
×
908
            scale = step_function["scale"]
×
909
            myangs = [ap_func(aa["min"]) for aa in angs]
×
910
            mydcns = [len(dd["dnb_indices"]) for dd in angs]
×
911
            xx = np.linspace(0.0, 1.1 * max(myangs), num=500)
×
912
            yy = np.zeros_like(xx)
×
913
            for iang, ang in enumerate(myangs):
×
914
                yy += mydcns[iang] * normal_cdf_step(xx, mean=ang, scale=scale)
×
915
        else:
916
            raise ValueError(f"Step function of type {step_function['type']!r} is not allowed")
×
917
        subplot.plot(xx, yy)
×
918

919
        return fig
×
920

921
    def __eq__(self, other: object) -> bool:
1✔
922
        needed_attrs = (
1✔
923
            "normalized_angle_tolerance",
924
            "normalized_distance_tolerance",
925
            "additional_conditions",
926
            "valences",
927
            "voronoi_list2",
928
            "structure",
929
        )
930
        if not all(hasattr(other, attr) for attr in needed_attrs):
1✔
931
            return NotImplemented
×
932
        return all(getattr(self, attr) == getattr(other, attr) for attr in needed_attrs)
1✔
933

934
    def to_bson_voronoi_list2(self):
1✔
935
        """
936
        Transforms the voronoi_list into a vlist + bson_nb_voro_list, that are BSON-encodable.
937

938
        Returns:
939
            [vlist, bson_nb_voro_list], to be used in the as_dict method.
940
        """
941
        bson_nb_voro_list2 = [None] * len(self.voronoi_list2)
1✔
942
        for ivoro, voro in enumerate(self.voronoi_list2):
1✔
943
            if voro is None or voro == "None":
1✔
944
                continue
×
945
            site_voro = []
1✔
946
            # {'site': neighbors[nn[1]],
947
            #  'angle': sa,
948
            #  'distance': distances[nn[1]],
949
            #  'index': myindex}
950
            for nb_dict in voro:
1✔
951
                site = nb_dict["site"]
1✔
952
                site_dict = {key: val for key, val in nb_dict.items() if key not in ["site"]}
1✔
953
                # site_voro.append([ps.as_dict(), dd]) [float(c) for c in self.frac_coords]
954
                diff = site.frac_coords - self.structure[nb_dict["index"]].frac_coords
1✔
955
                site_voro.append([[nb_dict["index"], [float(c) for c in diff]], site_dict])
1✔
956
            bson_nb_voro_list2[ivoro] = site_voro
1✔
957
        return bson_nb_voro_list2
1✔
958

959
    def as_dict(self):
1✔
960
        """
961
        Bson-serializable dict representation of the VoronoiContainer.
962

963
        Returns:
964
            dictionary that is BSON-encodable.
965
        """
966
        bson_nb_voro_list2 = self.to_bson_voronoi_list2()
1✔
967
        return {
1✔
968
            "@module": type(self).__module__,
969
            "@class": type(self).__name__,
970
            "bson_nb_voro_list2": bson_nb_voro_list2,
971
            # "neighbors_lists": self.neighbors_lists,
972
            "structure": self.structure.as_dict(),
973
            "normalized_angle_tolerance": self.normalized_angle_tolerance,
974
            "normalized_distance_tolerance": self.normalized_distance_tolerance,
975
            "additional_conditions": self.additional_conditions,
976
            "valences": self.valences,
977
            "maximum_distance_factor": self.maximum_distance_factor,
978
            "minimum_angle_factor": self.minimum_angle_factor,
979
        }
980

981
    @classmethod
1✔
982
    def from_dict(cls, d):
1✔
983
        """
984
        Reconstructs the VoronoiContainer object from a dict representation of the VoronoiContainer created using
985
        the as_dict method.
986

987
        Args:
988
            d: dict representation of the VoronoiContainer object.
989

990
        Returns:
991
            VoronoiContainer object.
992
        """
993
        structure = Structure.from_dict(d["structure"])
1✔
994
        voronoi_list2 = from_bson_voronoi_list2(d["bson_nb_voro_list2"], structure)
1✔
995
        maximum_distance_factor = d["maximum_distance_factor"] if "maximum_distance_factor" in d else None
1✔
996
        minimum_angle_factor = d["minimum_angle_factor"] if "minimum_angle_factor" in d else None
1✔
997
        return cls(
1✔
998
            structure=structure,
999
            voronoi_list2=voronoi_list2,
1000
            # neighbors_lists=neighbors_lists,
1001
            normalized_angle_tolerance=d["normalized_angle_tolerance"],
1002
            normalized_distance_tolerance=d["normalized_distance_tolerance"],
1003
            additional_conditions=d["additional_conditions"],
1004
            valences=d["valences"],
1005
            maximum_distance_factor=maximum_distance_factor,
1006
            minimum_angle_factor=minimum_angle_factor,
1007
        )
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