• 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

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

4
"""
1✔
5
This module contains the main object used to identify the coordination environments in a given structure.
6
If you use this module, please cite the following:
7
David Waroquiers, Xavier Gonze, Gian-Marco Rignanese, Cathrin Welker-Nieuwoudt, Frank Rosowski,
8
Michael Goebel, Stephan Schenk, Peter Degelmann, Rute Andre, Robert Glaum, and Geoffroy Hautier,
9
"Statistical analysis of coordination environments in oxides",
10
Chem. Mater., 2017, 29 (19), pp 8346-8360,
11
DOI: 10.1021/acs.chemmater.7b02766
12
D. Waroquiers, J. George, M. Horton, S. Schenk, K. A. Persson, G.-M. Rignanese, X. Gonze, G. Hautier
13
"ChemEnv: a fast and robust coordination environment identification tool",
14
Acta Cryst. B 2020, 76, pp 683-695,
15
DOI: 10.1107/S2052520620007994
16
"""
17

18
from __future__ import annotations
1✔
19

20
import itertools
1✔
21
import logging
1✔
22
import time
1✔
23
from random import shuffle
1✔
24

25
import numpy as np
1✔
26
from numpy.linalg import norm, svd
1✔
27

28
from pymatgen.analysis.bond_valence import BVAnalyzer
1✔
29
from pymatgen.analysis.chemenv.coordination_environments.chemenv_strategies import (
1✔
30
    MultiWeightsChemenvStrategy,
31
)
32
from pymatgen.analysis.chemenv.coordination_environments.coordination_geometries import (
1✔
33
    EXPLICIT_PERMUTATIONS,
34
    SEPARATION_PLANE,
35
    AllCoordinationGeometries,
36
)
37
from pymatgen.analysis.chemenv.coordination_environments.structure_environments import (
1✔
38
    ChemicalEnvironments,
39
    LightStructureEnvironments,
40
    StructureEnvironments,
41
)
42
from pymatgen.analysis.chemenv.coordination_environments.voronoi import (
1✔
43
    DetailedVoronoiContainer,
44
)
45
from pymatgen.analysis.chemenv.utils.coordination_geometry_utils import (
1✔
46
    Plane,
47
    collinear,
48
    separation_in_list,
49
    sort_separation,
50
    sort_separation_tuple,
51
)
52
from pymatgen.analysis.chemenv.utils.defs_utils import chemenv_citations
1✔
53
from pymatgen.core.lattice import Lattice
1✔
54
from pymatgen.core.periodic_table import Species
1✔
55
from pymatgen.core.structure import Structure
1✔
56
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
1✔
57

58
__author__ = "David Waroquiers"
1✔
59
__copyright__ = "Copyright 2012, The Materials Project"
1✔
60
__credits__ = "Geoffroy Hautier"
1✔
61
__version__ = "2.0"
1✔
62
__maintainer__ = "David Waroquiers"
1✔
63
__email__ = "david.waroquiers@gmail.com"
1✔
64
__date__ = "Feb 20, 2016"
1✔
65

66
debug = False
1✔
67
DIST_TOLERANCES = [0.02, 0.05, 0.1, 0.2, 0.3]
1✔
68

69

70
class AbstractGeometry:
1✔
71
    """
72
    Class used to describe a geometry (perfect or distorted)
73
    """
74

75
    def __init__(
1✔
76
        self,
77
        central_site=None,
78
        bare_coords=None,
79
        centering_type="standard",
80
        include_central_site_in_centroid=False,
81
        optimization=None,
82
    ):
83
        """
84
        Constructor for the abstract geometry
85
        :param central_site: Coordinates of the central site
86
        :param bare_coords: Coordinates of the neighbors of the central site
87
        :param centering_type: How to center the abstract geometry
88
        :param include_central_site_in_centroid: When the centering is on the centroid, the central site is included
89
            if this parameter is set to True.
90
        :raise: ValueError if the parameters are not consistent
91
        """
92
        bcoords = np.array(bare_coords)
1✔
93
        self.bare_centre = np.array(central_site)
1✔
94
        self.bare_points_without_centre = bcoords
1✔
95
        self.bare_points_with_centre = np.array(central_site)
1✔
96
        self.bare_points_with_centre = np.concatenate(([self.bare_points_with_centre], bcoords))
1✔
97
        self.centroid_without_centre = np.mean(self.bare_points_without_centre, axis=0)
1✔
98
        self.centroid_with_centre = np.mean(self.bare_points_with_centre, axis=0)
1✔
99
        self._points_wcs_csc = self.bare_points_with_centre - self.bare_centre
1✔
100
        self._points_wocs_csc = self.bare_points_without_centre - self.bare_centre
1✔
101
        self._points_wcs_ctwcc = self.bare_points_with_centre - self.centroid_with_centre
1✔
102
        self._points_wocs_ctwcc = self.bare_points_without_centre - self.centroid_with_centre
1✔
103
        self._points_wcs_ctwocc = self.bare_points_with_centre - self.centroid_without_centre
1✔
104
        self._points_wocs_ctwocc = self.bare_points_without_centre - self.centroid_without_centre
1✔
105

106
        self.centering_type = centering_type
1✔
107
        self.include_central_site_in_centroid = include_central_site_in_centroid
1✔
108
        self.bare_central_site = np.array(central_site)
1✔
109
        if centering_type == "standard":
1✔
110
            if len(bare_coords) < 5:
1✔
111
                if include_central_site_in_centroid:
1✔
112
                    raise ValueError(
×
113
                        "The center is the central site, no calculation of the centroid, "
114
                        "variable include_central_site_in_centroid should be set to False"
115
                    )
116
                if central_site is None:
1✔
117
                    raise ValueError("Centering_type is central_site, the central site should be given")
×
118
                self.centre = np.array(central_site)
1✔
119
            else:
120
                total = np.sum(bcoords, axis=0)
1✔
121
                if include_central_site_in_centroid:
1✔
122
                    if central_site is None:
×
123
                        raise ValueError("The centroid includes the central site but no central site is given")
×
124
                    total += self.bare_centre
×
125
                    self.centre = total / (np.float_(len(bare_coords)) + 1.0)
×
126
                else:
127
                    self.centre = total / np.float_(len(bare_coords))
1✔
128
        elif centering_type == "central_site":
1✔
129
            if include_central_site_in_centroid:
1✔
130
                raise ValueError(
1✔
131
                    "The center is the central site, no calculation of the centroid, "
132
                    "variable include_central_site_in_centroid should be set to False"
133
                )
134
            if central_site is None:
1✔
135
                raise ValueError("Centering_type is central_site, the central site should be given")
×
136
            self.centre = np.array(central_site)
1✔
137
        elif centering_type == "centroid":
1✔
138
            total = np.sum(bcoords, axis=0)
1✔
139
            if include_central_site_in_centroid:
1✔
140
                if central_site is None:
1✔
141
                    raise ValueError("The centroid includes the central site but no central site is given")
×
142
                total += self.bare_centre
1✔
143
                self.centre = total / (np.float_(len(bare_coords)) + 1.0)
1✔
144
            else:
145
                self.centre = total / np.float_(len(bare_coords))
1✔
146
        self._bare_coords = self.bare_points_without_centre
1✔
147
        self._coords = self._bare_coords - self.centre
1✔
148
        self.central_site = self.bare_central_site - self.centre
1✔
149
        self.coords = self._coords
1✔
150
        self.bare_coords = self._bare_coords
1✔
151

152
    def __str__(self):
1✔
153
        """
154
        String representation of the AbstractGeometry
155
        :return: String representation of the AbstractGeometry
156
        """
157
        outs = [f"\nAbstract Geometry with {len(self.coords)} points :"]
×
158
        for pp in self.coords:
×
159
            outs.append(f"  {pp}")
×
160
        if self.centering_type == "standard":
×
161
            if self.include_central_site_in_centroid:
×
162
                outs.append(
×
163
                    "Points are referenced to the central site for coordination numbers < 5"
164
                    " and to the centroid (calculated with the central site) for coordination"
165
                    f" numbers >= 5 : {self.centre}\n"
166
                )
167
            else:
168
                outs.append(
×
169
                    "Points are referenced to the central site for coordination numbers < 5"
170
                    " and to the centroid (calculated without the central site) for coordination"
171
                    f" numbers >= 5 : {self.centre}\n"
172
                )
173
        elif self.centering_type == "central_site":
×
174
            outs.append(f"Points are referenced to the central site : {self.centre}\n")
×
175
        elif self.centering_type == "centroid":
×
176
            if self.include_central_site_in_centroid:
×
177
                outs.append(
×
178
                    f"Points are referenced to the centroid (calculated with the central site) :\n  {self.centre}\n"
179
                )
180
            else:
181
                outs.append(
×
182
                    "Points are referenced to the centroid (calculated without the central site)"
183
                    f" :\n  {self.centre}\n"
184
                )
185
        return "\n".join(outs)
×
186

187
    @classmethod
1✔
188
    def from_cg(cls, cg, centering_type="standard", include_central_site_in_centroid=False):
1✔
189
        """
190
        :param cg:
191
        :param centering_type:
192
        :param include_central_site_in_centroid:
193
        :return:
194
        """
195
        central_site = cg.get_central_site()
1✔
196
        bare_coords = [np.array(pt, np.float_) for pt in cg.points]
1✔
197
        return cls(
1✔
198
            central_site=central_site,
199
            bare_coords=bare_coords,
200
            centering_type=centering_type,
201
            include_central_site_in_centroid=include_central_site_in_centroid,
202
        )
203

204
    def points_wcs_csc(self, permutation=None):
1✔
205
        """
206
        :param permutation:
207
        :return:
208
        """
209
        if permutation is None:
1✔
210
            return self._points_wcs_csc
1✔
211
        return np.concatenate((self._points_wcs_csc[0:1], self._points_wocs_csc.take(permutation, axis=0)))
1✔
212

213
    def points_wocs_csc(self, permutation=None):
1✔
214
        """
215
        :param permutation:
216
        :return:
217
        """
218
        if permutation is None:
1✔
219
            return self._points_wocs_csc
1✔
220
        return self._points_wocs_csc.take(permutation, axis=0)
1✔
221

222
    def points_wcs_ctwcc(self, permutation=None):
1✔
223
        """
224
        :param permutation:
225
        :return:
226
        """
227
        if permutation is None:
1✔
228
            return self._points_wcs_ctwcc
1✔
229
        return np.concatenate(
1✔
230
            (
231
                self._points_wcs_ctwcc[0:1],
232
                self._points_wocs_ctwcc.take(permutation, axis=0),
233
            )
234
        )
235

236
    def points_wocs_ctwcc(self, permutation=None):
1✔
237
        """
238
        :param permutation:
239
        :return:
240
        """
241
        if permutation is None:
1✔
242
            return self._points_wocs_ctwcc
1✔
243
        return self._points_wocs_ctwcc.take(permutation, axis=0)
1✔
244

245
    def points_wcs_ctwocc(self, permutation=None):
1✔
246
        """
247
        :param permutation:
248
        :return:
249
        """
250
        if permutation is None:
1✔
251
            return self._points_wcs_ctwocc
1✔
252
        return np.concatenate(
1✔
253
            (
254
                self._points_wcs_ctwocc[0:1],
255
                self._points_wocs_ctwocc.take(permutation, axis=0),
256
            )
257
        )
258

259
    def points_wocs_ctwocc(self, permutation=None):
1✔
260
        """
261
        :param permutation:
262
        :return:
263
        """
264
        if permutation is None:
1✔
265
            return self._points_wocs_ctwocc
1✔
266
        return self._points_wocs_ctwocc.take(permutation, axis=0)
1✔
267

268
    @property
1✔
269
    def cn(self):
1✔
270
        """
271
        :return: Coordination number
272
        """
273
        return len(self.coords)
1✔
274

275
    @property
1✔
276
    def coordination_number(self):
1✔
277
        """
278
        :return: Coordination number
279
        """
280
        return len(self.coords)
×
281

282

283
def symmetry_measure(points_distorted, points_perfect):
1✔
284
    """
285
    Computes the continuous symmetry measure of the (distorted) set of points "points_distorted" with respect to the
286
    (perfect) set of points "points_perfect".
287
    :param points_distorted: List of points describing a given (distorted) polyhedron for which the symmetry measure
288
        has to be computed with respect to the model polyhedron described by the list of points
289
        "points_perfect".
290
    :param points_perfect: List of "perfect" points describing a given model polyhedron.
291
    :return: The continuous symmetry measure of the distorted polyhedron with respect to the perfect polyhedron
292
    """
293
    # When there is only one point, the symmetry measure is 0.0 by definition
294
    if len(points_distorted) == 1:
1✔
295
        return {
1✔
296
            "symmetry_measure": 0.0,
297
            "scaling_factor": None,
298
            "rotation_matrix": None,
299
        }
300
    # Find the rotation matrix that aligns the distorted points to the perfect points in a least-square sense.
301
    rot = find_rotation(points_distorted=points_distorted, points_perfect=points_perfect)
1✔
302
    # Find the scaling factor between the distorted points and the perfect points in a least-square sense.
303
    scaling_factor, rotated_coords, points_perfect = find_scaling_factor(
1✔
304
        points_distorted=points_distorted, points_perfect=points_perfect, rot=rot
305
    )
306
    # Compute the continuous symmetry measure [see Eq. 1 in Pinsky et al., Inorganic Chemistry 37, 5575 (1998)]
307
    rotated_coords = scaling_factor * rotated_coords
1✔
308
    diff = points_perfect - rotated_coords
1✔
309
    num = np.tensordot(diff, diff)
1✔
310
    denom = np.tensordot(points_perfect, points_perfect)
1✔
311
    return {
1✔
312
        "symmetry_measure": num / denom * 100.0,
313
        "scaling_factor": scaling_factor,
314
        "rotation_matrix": rot,
315
    }
316

317

318
def find_rotation(points_distorted, points_perfect):
1✔
319
    """
320
    This finds the rotation matrix that aligns the (distorted) set of points "points_distorted" with respect to the
321
    (perfect) set of points "points_perfect" in a least-square sense.
322
    :param points_distorted: List of points describing a given (distorted) polyhedron for which the rotation that
323
        aligns these points in a least-square sense to the set of perfect points "points_perfect"
324
    :param points_perfect: List of "perfect" points describing a given model polyhedron.
325
    :return: The rotation matrix
326
    """
327
    H = np.matmul(points_distorted.T, points_perfect)
1✔
328
    [U, S, Vt] = svd(H)
1✔
329
    rot = np.matmul(Vt.T, U.T)
1✔
330
    return rot
1✔
331

332

333
def find_scaling_factor(points_distorted, points_perfect, rot):
1✔
334
    """
335
    This finds the scaling factor between the (distorted) set of points "points_distorted" and the
336
    (perfect) set of points "points_perfect" in a least-square sense.
337
    :param points_distorted: List of points describing a given (distorted) polyhedron for which the scaling factor has
338
                             to be obtained.
339
    :param points_perfect: List of "perfect" points describing a given model polyhedron.
340
    :param rot: The rotation matrix
341
    :return: The scaling factor between the two structures and the rotated set of (distorted) points.
342
    """
343
    rotated_coords = np.matmul(rot, points_distorted.T).T
1✔
344
    num = np.tensordot(rotated_coords, points_perfect)
1✔
345
    denom = np.tensordot(rotated_coords, rotated_coords)
1✔
346
    return num / denom, rotated_coords, points_perfect
1✔
347

348

349
class LocalGeometryFinder:
1✔
350
    """
351
    Main class used to find the local environments in a structure
352
    """
353

354
    DEFAULT_BVA_DISTANCE_SCALE_FACTOR = 1.0
1✔
355
    BVA_DISTANCE_SCALE_FACTORS = {
1✔
356
        "experimental": 1.0,
357
        "GGA_relaxed": 1.015,
358
        "LDA_relaxed": 0.995,
359
    }
360
    DEFAULT_SPG_ANALYZER_OPTIONS = {"symprec": 1e-3, "angle_tolerance": 5}
1✔
361
    STRUCTURE_REFINEMENT_NONE = "none"
1✔
362
    STRUCTURE_REFINEMENT_REFINED = "refined"
1✔
363
    STRUCTURE_REFINEMENT_SYMMETRIZED = "symmetrized"
1✔
364

365
    DEFAULT_STRATEGY = MultiWeightsChemenvStrategy.stats_article_weights_parameters()
1✔
366

367
    PRESETS = {
1✔
368
        "DEFAULT": {
369
            "maximum_distance_factor": 2.0,
370
            "minimum_angle_factor": 0.05,
371
            "voronoi_normalized_distance_tolerance": 0.05,
372
            "voronoi_normalized_angle_tolerance": 0.03,
373
            "optimization": 2,
374
        }
375
    }
376

377
    def __init__(
1✔
378
        self,
379
        permutations_safe_override: bool = False,
380
        plane_ordering_override: bool = True,
381
        plane_safe_permutations: bool = False,
382
        only_symbols=None,
383
        print_citation: bool = False,
384
    ):
385
        """
386
        Args:
387
            permutations_safe_override: If set to True, all permutations are tested (very time-consuming for large
388
            coordination numbers!)
389
            plane_ordering_override: If set to False, the ordering of the points in the plane is disabled
390
            plane_safe_permutations: Whether to use safe permutations.
391
            only_symbols: Whether to restrict the list of environments to be identified.
392
            print_citation: If True, the ChemEnv citation will be printed
393
        """
394
        self.allcg = AllCoordinationGeometries(
1✔
395
            permutations_safe_override=permutations_safe_override,
396
            only_symbols=only_symbols,
397
        )
398
        self.permutations_safe_override = permutations_safe_override
1✔
399
        self.plane_ordering_override = plane_ordering_override
1✔
400
        self.plane_safe_permutations = plane_safe_permutations
1✔
401
        self.setup_parameters(
1✔
402
            centering_type="centroid",
403
            include_central_site_in_centroid=True,
404
            bva_distance_scale_factor=None,
405
            structure_refinement=self.STRUCTURE_REFINEMENT_NONE,
406
        )
407
        if print_citation:
1✔
408
            print(chemenv_citations())
1✔
409

410
    def setup_parameters(
1✔
411
        self,
412
        centering_type="standard",
413
        include_central_site_in_centroid=False,
414
        bva_distance_scale_factor=None,
415
        structure_refinement=STRUCTURE_REFINEMENT_REFINED,
416
        spg_analyzer_options=None,
417
    ):
418
        """
419
        Setup of the parameters for the coordination geometry finder. A reference point for the geometries has to be
420
        chosen. This can be the centroid of the structure (including or excluding the atom for which the coordination
421
        geometry is looked for) or the atom itself. In the 'standard' centering_type, the reference point is the central
422
        atom for coordination numbers 1, 2, 3 and 4 and the centroid for coordination numbers > 4.
423
        :param centering_type: Type of the reference point (centering) 'standard', 'centroid' or 'central_site'
424
        :param include_central_site_in_centroid: In case centering_type is 'centroid', the central site is included if
425
            this value is set to True.
426
        :param bva_distance_scale_factor: Scaling factor for the bond valence analyzer (this might be different whether
427
            the structure is an experimental one, an LDA or a GGA relaxed one, or any other relaxation scheme (where
428
            under- or over-estimation of bond lengths is known).
429
        :param structure_refinement: Refinement of the structure. Can be "none", "refined" or "symmetrized".
430
        :param spg_analyzer_options: Options for the SpaceGroupAnalyzer (dictionary specifying "symprec"
431
            and "angle_tolerance". See pymatgen's SpaceGroupAnalyzer for more information.
432
        """
433
        self.centering_type = centering_type
1✔
434
        self.include_central_site_in_centroid = include_central_site_in_centroid
1✔
435
        if bva_distance_scale_factor is not None:
1✔
436
            self.bva_distance_scale_factor = bva_distance_scale_factor
×
437
        else:
438
            self.bva_distance_scale_factor = self.DEFAULT_BVA_DISTANCE_SCALE_FACTOR
1✔
439
        self.structure_refinement = structure_refinement
1✔
440
        if spg_analyzer_options is None:
1✔
441
            self.spg_analyzer_options = self.DEFAULT_SPG_ANALYZER_OPTIONS
1✔
442
        else:
443
            self.spg_analyzer_options = spg_analyzer_options
×
444

445
    def setup_parameter(self, parameter, value):
1✔
446
        """
447
        Setup of one specific parameter to the given value. The other parameters are unchanged. See setup_parameters
448
        method for the list of possible parameters
449
        :param parameter: Parameter to setup/update
450
        :param value: Value of the parameter
451
        """
452
        self.__dict__[parameter] = value
×
453

454
    def setup_structure(self, structure):
1✔
455
        """
456
        Sets up the structure for which the coordination geometries have to be identified. The structure is analyzed
457
        with the space group analyzer and a refined structure is used
458
        :param structure: A pymatgen Structure
459
        """
460
        self.initial_structure = structure.copy()
1✔
461
        if self.structure_refinement == self.STRUCTURE_REFINEMENT_NONE:
1✔
462
            self.structure = structure.copy()
1✔
463
            self.spg_analyzer = None
1✔
464
            self.symmetrized_structure = None
1✔
465
        else:
466
            self.spg_analyzer = SpacegroupAnalyzer(
1✔
467
                self.initial_structure,
468
                symprec=self.spg_analyzer_options["symprec"],
469
                angle_tolerance=self.spg_analyzer_options["angle_tolerance"],
470
            )
471
            if self.structure_refinement == self.STRUCTURE_REFINEMENT_REFINED:
1✔
472
                self.structure = self.spg_analyzer.get_refined_structure()
1✔
473
                self.symmetrized_structure = None
1✔
474
            elif self.structure_refinement == self.STRUCTURE_REFINEMENT_SYMMETRIZED:
×
475
                self.structure = self.spg_analyzer.get_refined_structure()
×
476
                self.spg_analyzer_refined = SpacegroupAnalyzer(
×
477
                    self.structure,
478
                    symprec=self.spg_analyzer_options["symprec"],
479
                    angle_tolerance=self.spg_analyzer_options["angle_tolerance"],
480
                )
481
                self.symmetrized_structure = self.spg_analyzer_refined.get_symmetrized_structure()
×
482

483
    def get_structure(self):
1✔
484
        """
485
        Returns the pymatgen Structure that has been setup for the identification of geometries (the initial one
486
        might have been refined/symmetrized using the SpaceGroupAnalyzer).
487
        :return: The pymatgen Structure that has been setup for the identification of geometries (the initial one
488
        might have been refined/symmetrized using the SpaceGroupAnalyzer).
489
        """
490
        return self.structure
×
491

492
    def set_structure(self, lattice: Lattice, species, coords, coords_are_cartesian):
1✔
493
        """
494
        Sets up the pymatgen structure for which the coordination geometries have to be identified starting from the
495
        lattice, the species and the coordinates
496
        :param lattice: The lattice of the structure
497
        :param species: The species on the sites
498
        :param coords: The coordinates of the sites
499
        :param coords_are_cartesian: If set to True, the coordinates are given in Cartesian coordinates
500
        """
501
        self.setup_structure(Structure(lattice, species, coords, coords_are_cartesian))
1✔
502

503
    def compute_coordination_environments(
1✔
504
        self,
505
        structure,
506
        indices=None,
507
        only_cations=True,
508
        strategy=DEFAULT_STRATEGY,
509
        valences="bond-valence-analysis",
510
        initial_structure_environments=None,
511
    ):
512
        """
513
        :param structure:
514
        :param indices:
515
        :param only_cations:
516
        :param strategy:
517
        :param valences:
518
        :param initial_structure_environments:
519
        :return:
520
        """
521
        self.setup_structure(structure=structure)
1✔
522
        if valences == "bond-valence-analysis":
1✔
523
            bva = BVAnalyzer()
1✔
524
            try:
1✔
525
                vals = bva.get_valences(structure=structure)
1✔
526
            except ValueError:
×
527
                vals = "undefined"
×
528
        else:
529
            if valences == "undefined":
×
530
                vals = valences
×
531
            else:
532
                len_vals, len_sites = len(valences), len(structure)
×
533
                if len_vals != len_sites:
×
534
                    raise ValueError(
×
535
                        f"Valences ({len_vals}) do not match the number of sites in the structure ({len_sites})"
536
                    )
537
                vals = valences
×
538
        # TODO: add something to compute only the neighbors sets needed for the strategy.
539
        se = self.compute_structure_environments(
1✔
540
            only_cations=only_cations,
541
            only_indices=indices,
542
            valences=vals,
543
            initial_structure_environments=initial_structure_environments,
544
        )
545
        lse = LightStructureEnvironments.from_structure_environments(strategy=strategy, structure_environments=se)
1✔
546
        return lse.coordination_environments
1✔
547

548
    def compute_structure_environments(
1✔
549
        self,
550
        excluded_atoms=None,
551
        only_atoms=None,
552
        only_cations=True,
553
        only_indices=None,
554
        maximum_distance_factor=PRESETS["DEFAULT"]["maximum_distance_factor"],
555
        minimum_angle_factor=PRESETS["DEFAULT"]["minimum_angle_factor"],
556
        max_cn=None,
557
        min_cn=None,
558
        only_symbols=None,
559
        valences="undefined",
560
        additional_conditions=None,
561
        info=None,
562
        timelimit=None,
563
        initial_structure_environments=None,
564
        get_from_hints=False,
565
        voronoi_normalized_distance_tolerance=PRESETS["DEFAULT"]["voronoi_normalized_distance_tolerance"],
566
        voronoi_normalized_angle_tolerance=PRESETS["DEFAULT"]["voronoi_normalized_angle_tolerance"],
567
        voronoi_distance_cutoff=None,
568
        recompute=None,
569
        optimization=PRESETS["DEFAULT"]["optimization"],
570
    ):
571
        """
572
        Computes and returns the StructureEnvironments object containing all the information about the coordination
573
        environments in the structure
574
        :param excluded_atoms: Atoms for which the coordination geometries does not have to be identified
575
        :param only_atoms: If not set to None, atoms for which the coordination geometries have to be identified
576
        :param only_cations: If set to True, will only compute environments for cations
577
        :param only_indices: If not set to None, will only compute environments the atoms of the given indices
578
        :param maximum_distance_factor: If not set to None, neighbors beyond
579
            maximum_distance_factor*closest_neighbor_distance are not considered
580
        :param minimum_angle_factor: If not set to None, neighbors for which the angle is lower than
581
            minimum_angle_factor*largest_angle_neighbor are not considered
582
        :param max_cn: maximum coordination number to be considered
583
        :param min_cn: minimum coordination number to be considered
584
        :param only_symbols: if not set to None, consider only coordination environments with the given symbols
585
        :param valences: valences of the atoms
586
        :param additional_conditions: additional conditions to be considered in the bonds (example : only bonds
587
            between cation and anion
588
        :param info: additional info about the calculation
589
        :param timelimit: time limit (in secs) after which the calculation of the StructureEnvironments object stops
590
        :param initial_structure_environments: initial StructureEnvironments object (most probably incomplete)
591
        :param get_from_hints: whether to add neighbors sets from "hints" (e.g. capped environment => test the
592
            neighbors without the cap)
593
        :param voronoi_normalized_distance_tolerance: tolerance for the normalized distance used to distinguish
594
            neighbors sets
595
        :param voronoi_normalized_angle_tolerance: tolerance for the normalized angle used to distinguish
596
            neighbors sets
597
        :param voronoi_distance_cutoff: determines distance of considered neighbors. Especially important to increase it
598
            for molecules in a box.
599
        :param recompute: whether to recompute the sites already computed (when initial_structure_environments
600
            is not None)
601
        :param optimization: optimization algorithm
602
        :return: The StructureEnvironments object containing all the information about the coordination
603
            environments in the structure
604
        """
605
        time_init = time.process_time()
1✔
606
        if info is None:
1✔
607
            info = {}
1✔
608
        info.update(
1✔
609
            {
610
                "local_geometry_finder": {
611
                    "parameters": {
612
                        "centering_type": self.centering_type,
613
                        "include_central_site_in_centroid": self.include_central_site_in_centroid,
614
                        "structure_refinement": self.structure_refinement,
615
                        "spg_analyzer_options": self.spg_analyzer_options,
616
                    }
617
                }
618
            }
619
        )
620
        if only_symbols is not None:
1✔
621
            self.allcg = AllCoordinationGeometries(
1✔
622
                permutations_safe_override=self.permutations_safe_override,
623
                only_symbols=only_symbols,
624
            )
625

626
        if valences == "undefined":
1✔
627
            firstsite = self.structure[0]
1✔
628
            try:
1✔
629
                sp = firstsite.specie
1✔
630
                if isinstance(sp, Species):
1✔
631
                    self.valences = [int(site.specie.oxi_state) for site in self.structure]
1✔
632
                else:
633
                    self.valences = valences
1✔
634
            except AttributeError:
×
635
                self.valences = valences
×
636
        else:
637
            self.valences = valences
1✔
638

639
        # Get a list of indices of unequivalent sites from the initial structure
640
        self.equivalent_sites = [[site] for site in self.structure]
1✔
641
        self.struct_sites_to_irreducible_site_list_map = list(range(len(self.structure)))
1✔
642
        self.sites_map = list(range(len(self.structure)))
1✔
643
        indices = list(range(len(self.structure)))
1✔
644

645
        # Get list of unequivalent sites with valence >= 0
646
        if only_cations and self.valences != "undefined":
1✔
647
            sites_indices = [isite for isite in indices if self.valences[isite] >= 0]
1✔
648
        else:
649
            sites_indices = list(indices)
1✔
650

651
        # Include atoms that are in the list of "only_atoms" if it is provided
652
        if only_atoms is not None:
1✔
653
            sites_indices = [
1✔
654
                isite
655
                for isite in sites_indices
656
                if any(at in [sp.symbol for sp in self.structure[isite].species] for at in only_atoms)
657
            ]
658

659
        # Exclude atoms that are in the list of excluded atoms
660
        if excluded_atoms:
1✔
661
            sites_indices = [
×
662
                isite
663
                for isite in sites_indices
664
                if not any(at in [sp.symbol for sp in self.structure[isite].species] for at in excluded_atoms)
665
            ]
666

667
        if only_indices is not None:
1✔
668
            sites_indices = [isite for isite in indices if isite in only_indices]
1✔
669

670
        # Get the VoronoiContainer for the sites defined by their indices (sites_indices)
671
        logging.debug("Getting DetailedVoronoiContainer")
1✔
672
        if voronoi_normalized_distance_tolerance is None:
1✔
673
            normalized_distance_tolerance = DetailedVoronoiContainer.default_normalized_distance_tolerance
×
674
        else:
675
            normalized_distance_tolerance = voronoi_normalized_distance_tolerance
1✔
676
        if voronoi_normalized_angle_tolerance is None:
1✔
677
            normalized_angle_tolerance = DetailedVoronoiContainer.default_normalized_angle_tolerance
×
678
        else:
679
            normalized_angle_tolerance = voronoi_normalized_angle_tolerance
1✔
680
        if voronoi_distance_cutoff is None:
1✔
681
            voronoi_distance_cutoff = DetailedVoronoiContainer.default_voronoi_cutoff
1✔
682
        self.detailed_voronoi = DetailedVoronoiContainer(
1✔
683
            self.structure,
684
            isites=sites_indices,
685
            valences=self.valences,
686
            maximum_distance_factor=maximum_distance_factor,
687
            minimum_angle_factor=minimum_angle_factor,
688
            additional_conditions=additional_conditions,
689
            normalized_distance_tolerance=normalized_distance_tolerance,
690
            normalized_angle_tolerance=normalized_angle_tolerance,
691
            voronoi_cutoff=voronoi_distance_cutoff,
692
        )
693
        logging.debug("DetailedVoronoiContainer has been set up")
1✔
694

695
        # Initialize the StructureEnvironments object (either from initial_structure_environments or from scratch)
696
        if initial_structure_environments is not None:
1✔
697
            se = initial_structure_environments
×
698
            if se.structure != self.structure:
×
699
                raise ValueError("Structure is not the same in initial_structure_environments")
×
700
            if se.voronoi != self.detailed_voronoi:
×
701
                if self.detailed_voronoi.is_close_to(se.voronoi):
×
702
                    self.detailed_voronoi = se.voronoi
×
703
                else:
704
                    raise ValueError("Detailed Voronoi is not the same in initial_structure_environments")
×
705
            se.info = info
×
706
        else:
707
            se = StructureEnvironments(
1✔
708
                voronoi=self.detailed_voronoi,
709
                valences=self.valences,
710
                sites_map=self.sites_map,
711
                equivalent_sites=self.equivalent_sites,
712
                ce_list=[None] * len(self.structure),
713
                structure=self.structure,
714
                info=info,
715
            )
716

717
        # Set up the coordination numbers that have to be computed based on min_cn, max_cn and possibly the settings
718
        # for an update (argument "recompute") of an existing StructureEnvironments
719
        if min_cn is None:
1✔
720
            min_cn = 1
1✔
721
        if max_cn is None:
1✔
722
            max_cn = 20
1✔
723
        all_cns = range(min_cn, max_cn + 1)
1✔
724
        do_recompute = False
1✔
725
        if recompute is not None:
1✔
726
            if "cns" in recompute:
×
727
                cns_to_recompute = recompute["cns"]
×
728
                all_cns = list(set(all_cns).intersection(cns_to_recompute))
×
729
            do_recompute = True
×
730

731
        # Variables used for checking timelimit
732
        max_time_one_site = 0.0
1✔
733
        breakit = False
1✔
734

735
        if optimization > 0:
1✔
736
            self.detailed_voronoi.local_planes = [None] * len(self.structure)
1✔
737
            self.detailed_voronoi.separations = [None] * len(self.structure)
1✔
738

739
        # Loop on all the sites
740
        for isite, site in enumerate(self.structure):
1✔
741
            if isite not in sites_indices:
1✔
742
                logging.debug(f" ... in site #{isite:d}/{len(self.structure):d} ({site.species_string}) : skipped")
1✔
743
                continue
1✔
744
            if breakit:
1✔
745
                logging.debug(
×
746
                    f" ... in site #{isite}/{len(self.structure)} ({site.species_string}) : skipped (timelimit)"
747
                )
748
                continue
×
749
            logging.debug(f" ... in site #{isite:d}/{len(self.structure):d} ({site.species_string})")
1✔
750
            t1 = time.process_time()
1✔
751
            if optimization > 0:
1✔
752
                self.detailed_voronoi.local_planes[isite] = {}
1✔
753
                self.detailed_voronoi.separations[isite] = {}
1✔
754
            se.init_neighbors_sets(
1✔
755
                isite=isite,
756
                additional_conditions=additional_conditions,
757
                valences=valences,
758
            )
759

760
            to_add_from_hints = []
1✔
761
            nb_sets_info = {}
1✔
762

763
            for cn, nb_sets in se.neighbors_sets[isite].items():
1✔
764
                if cn not in all_cns:
1✔
765
                    continue
1✔
766
                for inb_set, nb_set in enumerate(nb_sets):
1✔
767
                    logging.debug(f"    ... getting environments for nb_set ({cn:d}, {inb_set:d})")
1✔
768
                    tnbset1 = time.process_time()
1✔
769
                    ce = self.update_nb_set_environments(
1✔
770
                        se=se,
771
                        isite=isite,
772
                        cn=cn,
773
                        inb_set=inb_set,
774
                        nb_set=nb_set,
775
                        recompute=do_recompute,
776
                        optimization=optimization,
777
                    )
778
                    tnbset2 = time.process_time()
1✔
779
                    if cn not in nb_sets_info:
1✔
780
                        nb_sets_info[cn] = {}
1✔
781
                    nb_sets_info[cn][inb_set] = {"time": tnbset2 - tnbset1}
1✔
782
                    if get_from_hints:
1✔
783
                        for cg_symbol, cg_dict in ce:
1✔
784
                            cg = self.allcg[cg_symbol]
1✔
785
                            # Get possibly missing neighbors sets
786
                            if cg.neighbors_sets_hints is None:
1✔
787
                                continue
1✔
788
                            logging.debug(f"       ... getting hints from cg with mp_symbol {cg_symbol!r} ...")
1✔
789
                            hints_info = {
1✔
790
                                "csm": cg_dict["symmetry_measure"],
791
                                "nb_set": nb_set,
792
                                "permutation": cg_dict["permutation"],
793
                            }
794
                            for nb_sets_hints in cg.neighbors_sets_hints:
1✔
795
                                suggested_nb_set_voronoi_indices = nb_sets_hints.hints(hints_info)
1✔
796
                                for inew, new_nb_set_voronoi_indices in enumerate(suggested_nb_set_voronoi_indices):
1✔
797
                                    logging.debug(f"           hint # {inew:d}")
1✔
798
                                    new_nb_set = se.NeighborsSet(
1✔
799
                                        structure=se.structure,
800
                                        isite=isite,
801
                                        detailed_voronoi=se.voronoi,
802
                                        site_voronoi_indices=new_nb_set_voronoi_indices,
803
                                        sources={
804
                                            "origin": "nb_set_hints",
805
                                            "hints_type": nb_sets_hints.hints_type,
806
                                            "suggestion_index": inew,
807
                                            "cn_map_source": [cn, inb_set],
808
                                            "cg_source_symbol": cg_symbol,
809
                                        },
810
                                    )
811
                                    cn_new_nb_set = len(new_nb_set)
1✔
812
                                    if max_cn is not None and cn_new_nb_set > max_cn:
1✔
813
                                        continue
×
814
                                    if min_cn is not None and cn_new_nb_set < min_cn:
1✔
815
                                        continue
×
816
                                    if new_nb_set in [ta["new_nb_set"] for ta in to_add_from_hints]:
1✔
817
                                        has_nb_set = True
×
818
                                    elif cn_new_nb_set not in se.neighbors_sets[isite]:
1✔
819
                                        has_nb_set = False
1✔
820
                                    else:
821
                                        has_nb_set = new_nb_set in se.neighbors_sets[isite][cn_new_nb_set]
×
822
                                    if not has_nb_set:
1✔
823
                                        to_add_from_hints.append(
1✔
824
                                            {
825
                                                "isite": isite,
826
                                                "new_nb_set": new_nb_set,
827
                                                "cn_new_nb_set": cn_new_nb_set,
828
                                            }
829
                                        )
830
                                        logging.debug("              => to be computed")
1✔
831
                                    else:
832
                                        logging.debug("              => already present")
×
833
            logging.debug("    ... getting environments for nb_sets added from hints")
1✔
834
            for missing_nb_set_to_add in to_add_from_hints:
1✔
835
                se.add_neighbors_set(isite=isite, nb_set=missing_nb_set_to_add["new_nb_set"])
1✔
836
            for missing_nb_set_to_add in to_add_from_hints:
1✔
837
                isite_new_nb_set = missing_nb_set_to_add["isite"]
1✔
838
                cn_new_nb_set = missing_nb_set_to_add["cn_new_nb_set"]
1✔
839
                new_nb_set = missing_nb_set_to_add["new_nb_set"]
1✔
840
                inew_nb_set = se.neighbors_sets[isite_new_nb_set][cn_new_nb_set].index(new_nb_set)
1✔
841
                logging.debug(f"    ... getting environments for nb_set ({cn_new_nb_set}, {inew_nb_set}) - from hints")
1✔
842
                tnbset1 = time.process_time()
1✔
843
                self.update_nb_set_environments(
1✔
844
                    se=se,
845
                    isite=isite_new_nb_set,
846
                    cn=cn_new_nb_set,
847
                    inb_set=inew_nb_set,
848
                    nb_set=new_nb_set,
849
                    optimization=optimization,
850
                )
851
                tnbset2 = time.process_time()
1✔
852
                if cn not in nb_sets_info:
1✔
853
                    nb_sets_info[cn] = {}
×
854
                nb_sets_info[cn][inew_nb_set] = {"time": tnbset2 - tnbset1}
1✔
855
            t2 = time.process_time()
1✔
856
            se.update_site_info(isite=isite, info_dict={"time": t2 - t1, "nb_sets_info": nb_sets_info})
1✔
857
            if timelimit is not None:
1✔
858
                time_elapsed = t2 - time_init
×
859
                time_left = timelimit - time_elapsed
×
860
                if time_left < 2.0 * max_time_one_site:
×
861
                    breakit = True
×
862
            max_time_one_site = max(max_time_one_site, t2 - t1)
1✔
863
            logging.debug(f"    ... computed in {t2 - t1:.2f} seconds")
1✔
864
        time_end = time.process_time()
1✔
865
        logging.debug(f"    ... compute_structure_environments ended in {time_end - time_init:.2f} seconds")
1✔
866
        return se
1✔
867

868
    def update_nb_set_environments(self, se, isite, cn, inb_set, nb_set, recompute=False, optimization=None):
1✔
869
        """
870
        :param se:
871
        :param isite:
872
        :param cn:
873
        :param inb_set:
874
        :param nb_set:
875
        :param recompute:
876
        :param optimization:
877
        :return:
878
        """
879
        ce = se.get_coordination_environments(isite=isite, cn=cn, nb_set=nb_set)
1✔
880
        if ce is not None and not recompute:
1✔
881
            return ce
×
882
        ce = ChemicalEnvironments()
1✔
883
        if optimization == 2:
1✔
884
            neighb_coords = nb_set.neighb_coordsOpt
1✔
885
        else:
886
            neighb_coords = nb_set.neighb_coords
×
887
        self.setup_local_geometry(isite, coords=neighb_coords, optimization=optimization)
1✔
888
        if optimization > 0:
1✔
889
            logging.debug("Getting StructureEnvironments with optimized algorithm")
1✔
890
            nb_set.local_planes = {}
1✔
891
            nb_set.separations = {}
1✔
892
            cncgsm = self.get_coordination_symmetry_measures_optim(nb_set=nb_set, optimization=optimization)
1✔
893
        else:
894
            logging.debug("Getting StructureEnvironments with standard algorithm")
×
895
            cncgsm = self.get_coordination_symmetry_measures()
×
896
        for cg, d in cncgsm.items():
1✔
897
            other_csms = {
1✔
898
                "csm_wocs_ctwocc": d["csm_wocs_ctwocc"],
899
                "csm_wocs_ctwcc": d["csm_wocs_ctwcc"],
900
                "csm_wocs_csc": d["csm_wocs_csc"],
901
                "csm_wcs_ctwocc": d["csm_wcs_ctwocc"],
902
                "csm_wcs_ctwcc": d["csm_wcs_ctwcc"],
903
                "csm_wcs_csc": d["csm_wcs_csc"],
904
                "rotation_matrix_wocs_ctwocc": d["rotation_matrix_wocs_ctwocc"],
905
                "rotation_matrix_wocs_ctwcc": d["rotation_matrix_wocs_ctwcc"],
906
                "rotation_matrix_wocs_csc": d["rotation_matrix_wocs_csc"],
907
                "rotation_matrix_wcs_ctwocc": d["rotation_matrix_wcs_ctwocc"],
908
                "rotation_matrix_wcs_ctwcc": d["rotation_matrix_wcs_ctwcc"],
909
                "rotation_matrix_wcs_csc": d["rotation_matrix_wcs_csc"],
910
                "scaling_factor_wocs_ctwocc": d["scaling_factor_wocs_ctwocc"],
911
                "scaling_factor_wocs_ctwcc": d["scaling_factor_wocs_ctwcc"],
912
                "scaling_factor_wocs_csc": d["scaling_factor_wocs_csc"],
913
                "scaling_factor_wcs_ctwocc": d["scaling_factor_wcs_ctwocc"],
914
                "scaling_factor_wcs_ctwcc": d["scaling_factor_wcs_ctwcc"],
915
                "scaling_factor_wcs_csc": d["scaling_factor_wcs_csc"],
916
                "translation_vector_wocs_ctwocc": d["translation_vector_wocs_ctwocc"],
917
                "translation_vector_wocs_ctwcc": d["translation_vector_wocs_ctwcc"],
918
                "translation_vector_wocs_csc": d["translation_vector_wocs_csc"],
919
                "translation_vector_wcs_ctwocc": d["translation_vector_wcs_ctwocc"],
920
                "translation_vector_wcs_ctwcc": d["translation_vector_wcs_ctwcc"],
921
                "translation_vector_wcs_csc": d["translation_vector_wcs_csc"],
922
            }
923
            ce.add_coord_geom(
1✔
924
                cg,
925
                d["csm"],
926
                algo=d["algo"],
927
                permutation=d["indices"],
928
                local2perfect_map=d["local2perfect_map"],
929
                perfect2local_map=d["perfect2local_map"],
930
                detailed_voronoi_index={"cn": cn, "index": inb_set},
931
                other_symmetry_measures=other_csms,
932
                rotation_matrix=d["rotation_matrix"],
933
                scaling_factor=d["scaling_factor"],
934
            )
935
        se.update_coordination_environments(isite=isite, cn=cn, nb_set=nb_set, ce=ce)
1✔
936
        return ce
1✔
937

938
    def setup_local_geometry(self, isite, coords, optimization=None):
1✔
939
        """
940
        Sets up the AbstractGeometry for the local geometry of site with index isite.
941
        :param isite: Index of the site for which the local geometry has to be set up
942
        :param coords: The coordinates of the (local) neighbors
943
        """
944
        self.local_geometry = AbstractGeometry(
1✔
945
            central_site=self.structure.cart_coords[isite],
946
            bare_coords=coords,
947
            centering_type=self.centering_type,
948
            include_central_site_in_centroid=self.include_central_site_in_centroid,
949
            optimization=optimization,
950
        )
951

952
    def setup_test_perfect_environment(
1✔
953
        self,
954
        symbol,
955
        randomness=False,
956
        max_random_dist=0.1,
957
        symbol_type="mp_symbol",
958
        indices="RANDOM",
959
        random_translation="NONE",
960
        random_rotation="NONE",
961
        random_scale="NONE",
962
        points=None,
963
    ):
964
        """
965
        :param symbol:
966
        :param randomness:
967
        :param max_random_dist:
968
        :param symbol_type:
969
        :param indices:
970
        :param random_translation:
971
        :param random_rotation:
972
        :param random_scale:
973
        :param points:
974
        :return:
975
        """
976
        if symbol_type == "IUPAC":
1✔
977
            cg = self.allcg.get_geometry_from_IUPAC_symbol(symbol)
×
978
        elif symbol_type in ("MP", "mp_symbol"):
1✔
979
            cg = self.allcg.get_geometry_from_mp_symbol(symbol)
1✔
980
        elif symbol_type == "CoordinationGeometry":
×
981
            cg = symbol
×
982
        else:
983
            raise ValueError("Wrong mp_symbol to setup coordination geometry")
×
984
        neighb_coords = []
1✔
985
        if points is not None:
1✔
986
            mypoints = points
1✔
987
        else:
988
            mypoints = cg.points
1✔
989
        if randomness:
1✔
990
            rv = np.random.random_sample(3)
×
991
            while norm(rv) > 1.0:
×
992
                rv = np.random.random_sample(3)
×
993
            coords = [np.zeros(3, np.float_) + max_random_dist * rv]
×
994
            for pp in mypoints:
×
995
                rv = np.random.random_sample(3)
×
996
                while norm(rv) > 1.0:
×
997
                    rv = np.random.random_sample(3)
×
998
                neighb_coords.append(np.array(pp) + max_random_dist * rv)
×
999
        else:
1000
            coords = [np.zeros(3, np.float_)]
1✔
1001
            for pp in mypoints:
1✔
1002
                neighb_coords.append(np.array(pp))
1✔
1003
        if indices == "RANDOM":
1✔
1004
            shuffle(neighb_coords)
×
1005
        elif indices == "ORDERED":
1✔
1006
            pass
×
1007
        else:
1008
            neighb_coords = [neighb_coords[ii] for ii in indices]
1✔
1009

1010
        # Scaling the test environment
1011
        if random_scale == "RANDOM":
1✔
1012
            scale = 0.1 * np.random.random_sample() + 0.95
×
1013
        elif random_scale == "NONE":
1✔
1014
            scale = 1.0
1✔
1015
        else:
1016
            scale = random_scale
×
1017
        coords = [scale * cc for cc in coords]
1✔
1018
        neighb_coords = [scale * cc for cc in neighb_coords]
1✔
1019

1020
        # Rotating the test environment
1021
        if random_rotation == "RANDOM":
1✔
1022
            uu = np.random.random_sample(3) + 0.1
×
1023
            uu = uu / norm(uu)
×
1024
            theta = np.pi * np.random.random_sample()
×
1025
            cc = np.cos(theta)
×
1026
            ss = np.sin(theta)
×
1027
            ux = uu[0]
×
1028
            uy = uu[1]
×
1029
            uz = uu[2]
×
1030
            RR = [
×
1031
                [
1032
                    ux * ux + (1.0 - ux * ux) * cc,
1033
                    ux * uy * (1.0 - cc) - uz * ss,
1034
                    ux * uz * (1.0 - cc) + uy * ss,
1035
                ],
1036
                [
1037
                    ux * uy * (1.0 - cc) + uz * ss,
1038
                    uy * uy + (1.0 - uy * uy) * cc,
1039
                    uy * uz * (1.0 - cc) - ux * ss,
1040
                ],
1041
                [
1042
                    ux * uz * (1.0 - cc) - uy * ss,
1043
                    uy * uz * (1.0 - cc) + ux * ss,
1044
                    uz * uz + (1.0 - uz * uz) * cc,
1045
                ],
1046
            ]
1047
        elif random_rotation == "NONE":
1✔
1048
            RR = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
1✔
1049
        else:
1050
            RR = random_rotation
×
1051
        newcoords = []
1✔
1052
        for cc in coords:
1✔
1053
            newcc = np.dot(RR, cc).T
1✔
1054
            newcoords.append(newcc.ravel())
1✔
1055
        coords = newcoords
1✔
1056
        newcoords = []
1✔
1057
        for cc in neighb_coords:
1✔
1058
            newcc = np.dot(RR, cc.T)
1✔
1059
            newcoords.append(newcc.ravel())
1✔
1060
        neighb_coords = newcoords
1✔
1061

1062
        # Translating the test environment
1063
        if random_translation == "RANDOM":
1✔
1064
            translation = 10.0 * (2.0 * np.random.random_sample(3) - 1.0)
×
1065
        elif random_translation == "NONE":
1✔
1066
            translation = np.zeros(3, np.float_)
1✔
1067
        else:
1068
            translation = random_translation
×
1069
        coords = [cc + translation for cc in coords]
1✔
1070
        neighb_coords = [cc + translation for cc in neighb_coords]
1✔
1071

1072
        coords.extend(neighb_coords)
1✔
1073
        myspecies = ["O"] * (len(coords))
1✔
1074
        myspecies[0] = "Cu"
1✔
1075

1076
        amin = np.min([cc[0] for cc in coords])
1✔
1077
        amax = np.max([cc[0] for cc in coords])
1✔
1078
        bmin = np.min([cc[1] for cc in coords])
1✔
1079
        bmax = np.max([cc[1] for cc in coords])
1✔
1080
        cmin = np.min([cc[2] for cc in coords])
1✔
1081
        cmax = np.max([cc[2] for cc in coords])
1✔
1082

1083
        factor = 5.0
1✔
1084
        aa = factor * max([amax - amin, bmax - bmin, cmax - cmin])
1✔
1085

1086
        lattice = Lattice.cubic(a=aa)
1✔
1087
        structure = Structure(
1✔
1088
            lattice=lattice,
1089
            species=myspecies,
1090
            coords=coords,
1091
            to_unit_cell=False,
1092
            coords_are_cartesian=True,
1093
        )
1094

1095
        self.setup_structure(structure=structure)
1✔
1096
        self.setup_local_geometry(isite=0, coords=neighb_coords)
1✔
1097
        self.perfect_geometry = AbstractGeometry.from_cg(cg=cg)
1✔
1098

1099
    def setup_random_structure(self, coordination):
1✔
1100
        """
1101
        Sets up a purely random structure with a given coordination.
1102
        :param coordination: coordination number for the random structure
1103
        """
1104
        aa = 0.4
1✔
1105
        bb = -0.2
1✔
1106
        coords = []
1✔
1107
        for _ in range(coordination + 1):
1✔
1108
            coords.append(aa * np.random.random_sample(3) + bb)
1✔
1109
        self.set_structure(
1✔
1110
            lattice=np.array([[10, 0, 0], [0, 10, 0], [0, 0, 10]], np.float_),
1111
            species=["Si"] * (coordination + 1),
1112
            coords=coords,
1113
            coords_are_cartesian=False,
1114
        )
1115
        self.setup_random_indices_local_geometry(coordination)
1✔
1116

1117
    def setup_random_indices_local_geometry(self, coordination):
1✔
1118
        """
1119
        Sets up random indices for the local geometry, for testing purposes
1120
        :param coordination: coordination of the local geometry
1121
        """
1122
        self.icentral_site = 0
1✔
1123
        self.indices = list(range(1, coordination + 1))
1✔
1124
        np.random.shuffle(self.indices)
1✔
1125

1126
    def setup_ordered_indices_local_geometry(self, coordination):
1✔
1127
        """
1128
        Sets up ordered indices for the local geometry, for testing purposes
1129
        :param coordination: coordination of the local geometry
1130
        """
1131
        self.icentral_site = 0
1✔
1132
        self.indices = list(range(1, coordination + 1))
1✔
1133

1134
    def setup_explicit_indices_local_geometry(self, explicit_indices):
1✔
1135
        """
1136
        Sets up explicit indices for the local geometry, for testing purposes
1137
        :param explicit_indices: explicit indices for the neighbors (set of numbers
1138
        from 0 to CN-1 in a given order)
1139
        """
1140
        self.icentral_site = 0
1✔
1141
        self.indices = [ii + 1 for ii in explicit_indices]
1✔
1142

1143
    def get_coordination_symmetry_measures(self, only_minimum=True, all_csms=True, optimization=None):
1✔
1144
        """
1145
        Returns the continuous symmetry measures of the current local geometry in a dictionary.
1146
        :return: the continuous symmetry measures of the current local geometry in a dictionary.
1147
        """
1148
        test_geometries = self.allcg.get_implemented_geometries(len(self.local_geometry.coords))
1✔
1149
        if len(self.local_geometry.coords) == 1:
1✔
1150
            if len(test_geometries) == 0:
1✔
1151
                return {}
×
1152
            result_dict = {
1✔
1153
                "S:1": {
1154
                    "csm": 0.0,
1155
                    "indices": [0],
1156
                    "algo": "EXPLICIT",
1157
                    "local2perfect_map": {0: 0},
1158
                    "perfect2local_map": {0: 0},
1159
                    "scaling_factor": None,
1160
                    "rotation_matrix": None,
1161
                    "translation_vector": None,
1162
                }
1163
            }
1164
            if all_csms:
1✔
1165
                for csmtype in [
1✔
1166
                    "wocs_ctwocc",
1167
                    "wocs_ctwcc",
1168
                    "wocs_csc",
1169
                    "wcs_ctwocc",
1170
                    "wcs_ctwcc",
1171
                    "wcs_csc",
1172
                ]:
1173
                    result_dict["S:1"][f"csm_{csmtype}"] = 0.0
1✔
1174
                    result_dict["S:1"][f"scaling_factor_{csmtype}"] = None
1✔
1175
                    result_dict["S:1"][f"rotation_matrix_{csmtype}"] = None
1✔
1176
                    result_dict["S:1"][f"translation_vector_{csmtype}"] = None
1✔
1177
            return result_dict
1✔
1178
        result_dict = {}
1✔
1179
        for geometry in test_geometries:
1✔
1180
            self.perfect_geometry = AbstractGeometry.from_cg(
1✔
1181
                cg=geometry,
1182
                centering_type=self.centering_type,
1183
                include_central_site_in_centroid=self.include_central_site_in_centroid,
1184
            )
1185
            points_perfect = self.perfect_geometry.points_wcs_ctwcc()
1✔
1186
            cgsm = self.coordination_geometry_symmetry_measures(
1✔
1187
                geometry, points_perfect=points_perfect, optimization=optimization
1188
            )
1189
            result, permutations, algos, local2perfect_maps, perfect2local_maps = cgsm
1✔
1190
            if only_minimum:
1✔
1191
                if len(result) > 0:
1✔
1192
                    imin = np.argmin([rr["symmetry_measure"] for rr in result])
1✔
1193
                    if geometry.algorithms is not None:
1✔
1194
                        algo = algos[imin]
1✔
1195
                    else:
1196
                        algo = algos
×
1197
                    result_dict[geometry.mp_symbol] = {
1✔
1198
                        "csm": result[imin]["symmetry_measure"],
1199
                        "indices": permutations[imin],
1200
                        "algo": algo,
1201
                        "local2perfect_map": local2perfect_maps[imin],
1202
                        "perfect2local_map": perfect2local_maps[imin],
1203
                        "scaling_factor": 1.0 / result[imin]["scaling_factor"],
1204
                        "rotation_matrix": np.linalg.inv(result[imin]["rotation_matrix"]),
1205
                        "translation_vector": result[imin]["translation_vector"],
1206
                    }
1207
                    if all_csms:
1✔
1208
                        self._update_results_all_csms(result_dict, permutations, imin, geometry)
1✔
1209
            else:
1210
                result_dict[geometry.mp_symbol] = {
×
1211
                    "csm": result,
1212
                    "indices": permutations,
1213
                    "algo": algos,
1214
                    "local2perfect_map": local2perfect_maps,
1215
                    "perfect2local_map": perfect2local_maps,
1216
                }
1217
        return result_dict
1✔
1218

1219
    def _update_results_all_csms(self, result_dict, permutations, imin, geometry):
1✔
1220
        permutation = permutations[imin]
1✔
1221
        # Without central site, centered on the centroid (centroid does not include the central site)
1222
        # result_dict[geometry.mp_symbol]['csm_wocs_ctwocc'] = \
1223
        # result[imin]
1224
        pdist = self.local_geometry.points_wocs_ctwocc(permutation=permutation)
1✔
1225
        pperf = self.perfect_geometry.points_wocs_ctwocc()
1✔
1226
        sm_info = symmetry_measure(points_distorted=pdist, points_perfect=pperf)
1✔
1227
        result_dict[geometry.mp_symbol]["csm_wocs_ctwocc"] = sm_info["symmetry_measure"]
1✔
1228
        result_dict[geometry.mp_symbol]["rotation_matrix_wocs_ctwocc"] = np.linalg.inv(sm_info["rotation_matrix"])
1✔
1229
        result_dict[geometry.mp_symbol]["scaling_factor_wocs_ctwocc"] = 1.0 / sm_info["scaling_factor"]
1✔
1230
        result_dict[geometry.mp_symbol]["translation_vector_wocs_ctwocc"] = self.local_geometry.centroid_without_centre
1✔
1231
        # Without central site, centered on the centroid (centroid includes the central site)
1232
        pdist = self.local_geometry.points_wocs_ctwcc(permutation=permutation)
1✔
1233
        pperf = self.perfect_geometry.points_wocs_ctwcc()
1✔
1234
        sm_info = symmetry_measure(points_distorted=pdist, points_perfect=pperf)
1✔
1235
        result_dict[geometry.mp_symbol]["csm_wocs_ctwcc"] = sm_info["symmetry_measure"]
1✔
1236
        result_dict[geometry.mp_symbol]["rotation_matrix_wocs_ctwcc"] = np.linalg.inv(sm_info["rotation_matrix"])
1✔
1237
        result_dict[geometry.mp_symbol]["scaling_factor_wocs_ctwcc"] = 1.0 / sm_info["scaling_factor"]
1✔
1238
        result_dict[geometry.mp_symbol]["translation_vector_wocs_ctwcc"] = self.local_geometry.centroid_with_centre
1✔
1239
        # Without central site, centered on the central site
1240
        pdist = self.local_geometry.points_wocs_csc(permutation=permutation)
1✔
1241
        pperf = self.perfect_geometry.points_wocs_csc()
1✔
1242
        sm_info = symmetry_measure(points_distorted=pdist, points_perfect=pperf)
1✔
1243
        result_dict[geometry.mp_symbol]["csm_wocs_csc"] = sm_info["symmetry_measure"]
1✔
1244
        result_dict[geometry.mp_symbol]["rotation_matrix_wocs_csc"] = np.linalg.inv(sm_info["rotation_matrix"])
1✔
1245
        result_dict[geometry.mp_symbol]["scaling_factor_wocs_csc"] = 1.0 / sm_info["scaling_factor"]
1✔
1246
        result_dict[geometry.mp_symbol]["translation_vector_wocs_csc"] = self.local_geometry.bare_centre
1✔
1247
        # With central site, centered on the centroid (centroid does not include the central site)
1248
        pdist = self.local_geometry.points_wcs_ctwocc(permutation=permutation)
1✔
1249
        pperf = self.perfect_geometry.points_wcs_ctwocc()
1✔
1250
        sm_info = symmetry_measure(points_distorted=pdist, points_perfect=pperf)
1✔
1251
        result_dict[geometry.mp_symbol]["csm_wcs_ctwocc"] = sm_info["symmetry_measure"]
1✔
1252
        result_dict[geometry.mp_symbol]["rotation_matrix_wcs_ctwocc"] = np.linalg.inv(sm_info["rotation_matrix"])
1✔
1253
        result_dict[geometry.mp_symbol]["scaling_factor_wcs_ctwocc"] = 1.0 / sm_info["scaling_factor"]
1✔
1254
        result_dict[geometry.mp_symbol]["translation_vector_wcs_ctwocc"] = self.local_geometry.centroid_without_centre
1✔
1255
        # With central site, centered on the centroid (centroid includes the central site)
1256
        pdist = self.local_geometry.points_wcs_ctwcc(permutation=permutation)
1✔
1257
        pperf = self.perfect_geometry.points_wcs_ctwcc()
1✔
1258
        sm_info = symmetry_measure(points_distorted=pdist, points_perfect=pperf)
1✔
1259
        result_dict[geometry.mp_symbol]["csm_wcs_ctwcc"] = sm_info["symmetry_measure"]
1✔
1260
        result_dict[geometry.mp_symbol]["rotation_matrix_wcs_ctwcc"] = np.linalg.inv(sm_info["rotation_matrix"])
1✔
1261
        result_dict[geometry.mp_symbol]["scaling_factor_wcs_ctwcc"] = 1.0 / sm_info["scaling_factor"]
1✔
1262
        result_dict[geometry.mp_symbol]["translation_vector_wcs_ctwcc"] = self.local_geometry.centroid_with_centre
1✔
1263
        # With central site, centered on the central site
1264
        pdist = self.local_geometry.points_wcs_csc(permutation=permutation)
1✔
1265
        pperf = self.perfect_geometry.points_wcs_csc()
1✔
1266
        sm_info = symmetry_measure(points_distorted=pdist, points_perfect=pperf)
1✔
1267
        result_dict[geometry.mp_symbol]["csm_wcs_csc"] = sm_info["symmetry_measure"]
1✔
1268
        result_dict[geometry.mp_symbol]["rotation_matrix_wcs_csc"] = np.linalg.inv(sm_info["rotation_matrix"])
1✔
1269
        result_dict[geometry.mp_symbol]["scaling_factor_wcs_csc"] = 1.0 / sm_info["scaling_factor"]
1✔
1270
        result_dict[geometry.mp_symbol]["translation_vector_wcs_csc"] = self.local_geometry.bare_centre
1✔
1271

1272
    def get_coordination_symmetry_measures_optim(
1✔
1273
        self, only_minimum=True, all_csms=True, nb_set=None, optimization=None
1274
    ):
1275
        """
1276
        Returns the continuous symmetry measures of the current local geometry in a dictionary.
1277
        :return: the continuous symmetry measures of the current local geometry in a dictionary.
1278
        """
1279
        cn = len(self.local_geometry.coords)
1✔
1280
        test_geometries = self.allcg.get_implemented_geometries(cn)
1✔
1281
        if all(cg.algorithms[0].algorithm_type == EXPLICIT_PERMUTATIONS for cg in test_geometries):
1✔
1282
            return self.get_coordination_symmetry_measures(
1✔
1283
                only_minimum=only_minimum, all_csms=all_csms, optimization=optimization
1284
            )
1285
        if not all(all(algo.algorithm_type == SEPARATION_PLANE for algo in cg.algorithms) for cg in test_geometries):
1✔
1286
            raise ValueError("All algorithms should be EXPLICIT_PERMUTATIONS or SEPARATION_PLANE")
×
1287

1288
        result_dict = {}
1✔
1289
        for geometry in test_geometries:
1✔
1290
            logging.log(
1✔
1291
                level=5,
1292
                msg="Getting Continuous Symmetry Measure with Separation Plane "
1293
                f'algorithm for geometry "{geometry.ce_symbol}"',
1294
            )
1295
            self.perfect_geometry = AbstractGeometry.from_cg(
1✔
1296
                cg=geometry,
1297
                centering_type=self.centering_type,
1298
                include_central_site_in_centroid=self.include_central_site_in_centroid,
1299
            )
1300
            points_perfect = self.perfect_geometry.points_wcs_ctwcc()
1✔
1301
            cgsm = self.coordination_geometry_symmetry_measures_sepplane_optim(
1✔
1302
                geometry,
1303
                points_perfect=points_perfect,
1304
                nb_set=nb_set,
1305
                optimization=optimization,
1306
            )
1307
            result, permutations, algos, local2perfect_maps, perfect2local_maps = cgsm
1✔
1308
            if only_minimum:
1✔
1309
                if len(result) > 0:
1✔
1310
                    imin = np.argmin([rr["symmetry_measure"] for rr in result])
1✔
1311
                    if geometry.algorithms is not None:
1✔
1312
                        algo = algos[imin]
1✔
1313
                    else:
1314
                        algo = algos
×
1315
                    result_dict[geometry.mp_symbol] = {
1✔
1316
                        "csm": result[imin]["symmetry_measure"],
1317
                        "indices": permutations[imin],
1318
                        "algo": algo,
1319
                        "local2perfect_map": local2perfect_maps[imin],
1320
                        "perfect2local_map": perfect2local_maps[imin],
1321
                        "scaling_factor": 1.0 / result[imin]["scaling_factor"],
1322
                        "rotation_matrix": np.linalg.inv(result[imin]["rotation_matrix"]),
1323
                        "translation_vector": result[imin]["translation_vector"],
1324
                    }
1325
                    if all_csms:
1✔
1326
                        self._update_results_all_csms(result_dict, permutations, imin, geometry)
1✔
1327
        return result_dict
1✔
1328

1329
    def coordination_geometry_symmetry_measures(
1✔
1330
        self,
1331
        coordination_geometry,
1332
        tested_permutations=False,
1333
        points_perfect=None,
1334
        optimization=None,
1335
    ):
1336
        """
1337
        Returns the symmetry measures of a given coordination_geometry for a set of permutations depending on
1338
        the permutation setup. Depending on the parameters of the LocalGeometryFinder and on the coordination
1339
         geometry, different methods are called.
1340
        :param coordination_geometry: Coordination geometry for which the symmetry measures are looked for
1341
        :return: the symmetry measures of a given coordination_geometry for a set of permutations
1342
        :raise: NotImplementedError if the permutation_setup does not exists
1343
        """
1344
        if tested_permutations:
1✔
1345
            tested_permutations = set()
×
1346
        if self.permutations_safe_override:
1✔
1347
            raise ValueError("No permutations safe override anymore")
×
1348
        csms = []
1✔
1349
        permutations = []
1✔
1350
        algos = []
1✔
1351
        local2perfect_maps = []
1✔
1352
        perfect2local_maps = []
1✔
1353
        for algo in coordination_geometry.algorithms:
1✔
1354
            if algo.algorithm_type == EXPLICIT_PERMUTATIONS:
1✔
1355
                return self.coordination_geometry_symmetry_measures_standard(
1✔
1356
                    coordination_geometry,
1357
                    algo,
1358
                    points_perfect=points_perfect,
1359
                    optimization=optimization,
1360
                )
1361
            if algo.algorithm_type == SEPARATION_PLANE:
1✔
1362
                cgsm = self.coordination_geometry_symmetry_measures_separation_plane(
1✔
1363
                    coordination_geometry,
1364
                    algo,
1365
                    tested_permutations=tested_permutations,
1366
                    points_perfect=points_perfect,
1367
                )
1368
                csm, perm, algo, local2perfect_map, perfect2local_map = cgsm
1✔
1369

1370
                csms.extend(csm)
1✔
1371
                permutations.extend(perm)
1✔
1372
                algos.extend(algo)
1✔
1373
                local2perfect_maps.extend(local2perfect_map)
1✔
1374
                perfect2local_maps.extend(perfect2local_map)
1✔
1375
        return csms, permutations, algos, local2perfect_maps, perfect2local_maps
1✔
1376

1377
    def coordination_geometry_symmetry_measures_sepplane_optim(
1✔
1378
        self, coordination_geometry, points_perfect=None, nb_set=None, optimization=None
1379
    ):
1380
        """
1381
        Returns the symmetry measures of a given coordination_geometry for a set of permutations depending on
1382
        the permutation setup. Depending on the parameters of the LocalGeometryFinder and on the coordination
1383
         geometry, different methods are called.
1384
        :param coordination_geometry: Coordination geometry for which the symmetry measures are looked for
1385
        :return: the symmetry measures of a given coordination_geometry for a set of permutations
1386
        :raise: NotImplementedError if the permutation_setup does not exists
1387
        """
1388
        csms = []
1✔
1389
        permutations = []
1✔
1390
        algos = []
1✔
1391
        local2perfect_maps = []
1✔
1392
        perfect2local_maps = []
1✔
1393
        for algo in coordination_geometry.algorithms:
1✔
1394
            if algo.algorithm_type == SEPARATION_PLANE:
1✔
1395
                cgsm = self.coordination_geometry_symmetry_measures_separation_plane_optim(
1✔
1396
                    coordination_geometry,
1397
                    algo,
1398
                    points_perfect=points_perfect,
1399
                    nb_set=nb_set,
1400
                    optimization=optimization,
1401
                )
1402
                csm, perm, algo, local2perfect_map, perfect2local_map = cgsm
1✔
1403

1404
                csms.extend(csm)
1✔
1405
                permutations.extend(perm)
1✔
1406
                algos.extend(algo)
1✔
1407
                local2perfect_maps.extend(local2perfect_map)
1✔
1408
                perfect2local_maps.extend(perfect2local_map)
1✔
1409
        return csms, permutations, algos, local2perfect_maps, perfect2local_maps
1✔
1410

1411
    def coordination_geometry_symmetry_measures_standard(
1✔
1412
        self, coordination_geometry, algo, points_perfect=None, optimization=None
1413
    ):
1414
        """
1415
        Returns the symmetry measures for a set of permutations (whose setup depends on the coordination geometry)
1416
        for the coordination geometry "coordination_geometry". Standard implementation looking for the symmetry
1417
        measures of each permutation
1418
        :param coordination_geometry: The coordination geometry to be investigated
1419
        :return: The symmetry measures for the given coordination geometry for each permutation investigated
1420
        """
1421
        # permutations_symmetry_measures = np.zeros(len(algo.permutations),
1422
        #                                           np.float_)
1423
        if optimization == 2:
1✔
1424
            permutations_symmetry_measures = [None] * len(algo.permutations)
1✔
1425
            permutations = []
1✔
1426
            algos = []
1✔
1427
            local2perfect_maps = []
1✔
1428
            perfect2local_maps = []
1✔
1429
            for iperm, perm in enumerate(algo.permutations):
1✔
1430
                local2perfect_map = {}
1✔
1431
                perfect2local_map = {}
1✔
1432
                permutations.append(perm)
1✔
1433
                for iperfect, ii in enumerate(perm):
1✔
1434
                    perfect2local_map[iperfect] = ii
1✔
1435
                    local2perfect_map[ii] = iperfect
1✔
1436
                local2perfect_maps.append(local2perfect_map)
1✔
1437
                perfect2local_maps.append(perfect2local_map)
1✔
1438

1439
                points_distorted = self.local_geometry.points_wcs_ctwcc(permutation=perm)
1✔
1440

1441
                sm_info = symmetry_measure(points_distorted=points_distorted, points_perfect=points_perfect)
1✔
1442
                sm_info["translation_vector"] = self.local_geometry.centroid_with_centre
1✔
1443

1444
                permutations_symmetry_measures[iperm] = sm_info
1✔
1445
                algos.append(str(algo))
1✔
1446
            return (
1✔
1447
                permutations_symmetry_measures,
1448
                permutations,
1449
                algos,
1450
                local2perfect_maps,
1451
                perfect2local_maps,
1452
            )
1453

1454
        permutations_symmetry_measures = [None] * len(algo.permutations)
×
1455
        permutations = []
×
1456
        algos = []
×
1457
        local2perfect_maps = []
×
1458
        perfect2local_maps = []
×
1459
        for iperm, perm in enumerate(algo.permutations):
×
1460
            local2perfect_map = {}
×
1461
            perfect2local_map = {}
×
1462
            permutations.append(perm)
×
1463
            for iperfect, ii in enumerate(perm):
×
1464
                perfect2local_map[iperfect] = ii
×
1465
                local2perfect_map[ii] = iperfect
×
1466
            local2perfect_maps.append(local2perfect_map)
×
1467
            perfect2local_maps.append(perfect2local_map)
×
1468

1469
            points_distorted = self.local_geometry.points_wcs_ctwcc(permutation=perm)
×
1470

1471
            sm_info = symmetry_measure(points_distorted=points_distorted, points_perfect=points_perfect)
×
1472
            sm_info["translation_vector"] = self.local_geometry.centroid_with_centre
×
1473

1474
            permutations_symmetry_measures[iperm] = sm_info
×
1475
            algos.append(str(algo))
×
1476
        return (
×
1477
            permutations_symmetry_measures,
1478
            permutations,
1479
            algos,
1480
            local2perfect_maps,
1481
            perfect2local_maps,
1482
        )
1483

1484
    def coordination_geometry_symmetry_measures_separation_plane(
1✔
1485
        self,
1486
        coordination_geometry,
1487
        separation_plane_algo,
1488
        testing=False,
1489
        tested_permutations=False,
1490
        points_perfect=None,
1491
    ):
1492
        """
1493
        Returns the symmetry measures of the given coordination geometry "coordination_geometry" using separation
1494
        facets to reduce the complexity of the system. Caller to the refined 2POINTS, 3POINTS and other ...
1495
        :param coordination_geometry: The coordination geometry to be investigated
1496
        :return: The symmetry measures for the given coordination geometry for each plane and permutation investigated
1497
        """
1498
        permutations = []
1✔
1499
        permutations_symmetry_measures = []
1✔
1500
        plane_separations = []
1✔
1501
        algos = []
1✔
1502
        perfect2local_maps = []
1✔
1503
        local2perfect_maps = []
1✔
1504
        if testing:
1✔
1505
            separation_permutations = []
×
1506
        nplanes = 0
1✔
1507
        for npoints in range(
1✔
1508
            separation_plane_algo.minimum_number_of_points,
1509
            min(separation_plane_algo.maximum_number_of_points, 4) + 1,
1510
        ):
1511
            for points_combination in itertools.combinations(self.local_geometry.coords, npoints):
1✔
1512
                if npoints == 2:
1✔
1513
                    if collinear(
1✔
1514
                        points_combination[0],
1515
                        points_combination[1],
1516
                        self.local_geometry.central_site,
1517
                        tolerance=0.25,
1518
                    ):
1519
                        continue
1✔
1520
                    plane = Plane.from_3points(
1✔
1521
                        points_combination[0],
1522
                        points_combination[1],
1523
                        self.local_geometry.central_site,
1524
                    )
1525
                elif npoints == 3:
1✔
1526
                    if collinear(
1✔
1527
                        points_combination[0],
1528
                        points_combination[1],
1529
                        points_combination[2],
1530
                        tolerance=0.25,
1531
                    ):
1532
                        continue
×
1533
                    plane = Plane.from_3points(
1✔
1534
                        points_combination[0],
1535
                        points_combination[1],
1536
                        points_combination[2],
1537
                    )
1538
                elif npoints > 3:
1✔
1539
                    plane = Plane.from_npoints(points_combination, best_fit="least_square_distance")
1✔
1540
                else:
1541
                    raise ValueError("Wrong number of points to initialize separation plane")
×
1542
                cgsm = self._cg_csm_separation_plane(
1✔
1543
                    coordination_geometry=coordination_geometry,
1544
                    sepplane=separation_plane_algo,
1545
                    local_plane=plane,
1546
                    plane_separations=plane_separations,
1547
                    dist_tolerances=DIST_TOLERANCES,
1548
                    testing=testing,
1549
                    tested_permutations=tested_permutations,
1550
                    points_perfect=points_perfect,
1551
                )
1552
                csm, perm, algo = cgsm[0], cgsm[1], cgsm[2]
1✔
1553

1554
                if csm is not None:
1✔
1555
                    permutations_symmetry_measures.extend(csm)
1✔
1556
                    permutations.extend(perm)
1✔
1557
                    for thisperm in perm:
1✔
1558
                        p2l = {}
1✔
1559
                        l2p = {}
1✔
1560
                        for i_p, pp in enumerate(thisperm):
1✔
1561
                            p2l[i_p] = pp
1✔
1562
                            l2p[pp] = i_p
1✔
1563
                        perfect2local_maps.append(p2l)
1✔
1564
                        local2perfect_maps.append(l2p)
1✔
1565
                    algos.extend(algo)
1✔
1566
                    if testing:
1✔
1567
                        separation_permutations.extend(cgsm[3])
×
1568
                    nplanes += 1
1✔
1569
            if nplanes > 0:
1✔
1570
                break
1✔
1571
        if nplanes == 0:
1✔
1572
            return self.coordination_geometry_symmetry_measures_fallback_random(
1✔
1573
                coordination_geometry, points_perfect=points_perfect
1574
            )
1575
        if testing:
1✔
1576
            return permutations_symmetry_measures, permutations, separation_permutations
×
1577
        return (
1✔
1578
            permutations_symmetry_measures,
1579
            permutations,
1580
            algos,
1581
            local2perfect_maps,
1582
            perfect2local_maps,
1583
        )
1584

1585
    def coordination_geometry_symmetry_measures_separation_plane_optim(
1✔
1586
        self,
1587
        coordination_geometry,
1588
        separation_plane_algo,
1589
        points_perfect=None,
1590
        nb_set=None,
1591
        optimization=None,
1592
    ):
1593
        """
1594
        Returns the symmetry measures of the given coordination geometry "coordination_geometry" using separation
1595
        facets to reduce the complexity of the system. Caller to the refined 2POINTS, 3POINTS and other ...
1596

1597
        Args:
1598
            coordination_geometry: The coordination geometry to be investigated.
1599
            separation_plane_algo: Separation Plane algorithm used.
1600
            points_perfect: Points corresponding to the perfect geometry.
1601
            nb_set: Neighbor set for this set of points. (used to store already computed separation planes)
1602
            optimization: Optimization level (1 or 2).
1603

1604
        Returns:
1605
            tuple: Continuous symmetry measures for the given coordination geometry for each plane and permutation
1606
                   investigated, corresponding permutations, corresponding algorithms,
1607
                   corresponding mappings from local to perfect environment and corresponding mappings
1608
                   from perfect to local environment.
1609
        """
1610
        if optimization == 2:
1✔
1611
            logging.log(level=5, msg="... using optimization = 2")
1✔
1612
            cgcsmoptim = self._cg_csm_separation_plane_optim2
1✔
1613
        elif optimization == 1:
×
1614
            logging.log(level=5, msg="... using optimization = 2")
×
1615
            cgcsmoptim = self._cg_csm_separation_plane_optim1
×
1616
        else:
1617
            raise ValueError("Optimization should be 1 or 2")
×
1618
        cn = len(self.local_geometry.coords)
1✔
1619

1620
        permutations = []
1✔
1621
        permutations_symmetry_measures = []
1✔
1622
        algos = []
1✔
1623
        perfect2local_maps = []
1✔
1624
        local2perfect_maps = []
1✔
1625

1626
        if separation_plane_algo.separation in nb_set.separations:
1✔
1627
            for local_plane, npsep in nb_set.separations[separation_plane_algo.separation].values():
1✔
1628
                cgsm = cgcsmoptim(
1✔
1629
                    coordination_geometry=coordination_geometry,
1630
                    sepplane=separation_plane_algo,
1631
                    local_plane=local_plane,
1632
                    points_perfect=points_perfect,
1633
                    separation_indices=npsep,
1634
                )
1635
                csm, perm, algo, _ = cgsm[0], cgsm[1], cgsm[2], cgsm[3]
1✔
1636
                permutations_symmetry_measures.extend(csm)
1✔
1637
                permutations.extend(perm)
1✔
1638
                for thisperm in perm:
1✔
1639
                    p2l = {}
1✔
1640
                    l2p = {}
1✔
1641
                    for i_p, pp in enumerate(thisperm):
1✔
1642
                        p2l[i_p] = pp
1✔
1643
                        l2p[pp] = i_p
1✔
1644
                    perfect2local_maps.append(p2l)
1✔
1645
                    local2perfect_maps.append(l2p)
1✔
1646
                algos.extend(algo)
1✔
1647

1648
        # Get the local planes and separations up to 3 points
1649
        for npoints in range(self.allcg.minpoints[cn], min(self.allcg.maxpoints[cn], 3) + 1):
1✔
1650
            for ipoints_combination in itertools.combinations(range(self.local_geometry.cn), npoints):
1✔
1651
                if ipoints_combination in nb_set.local_planes:
1✔
1652
                    continue
1✔
1653
                # Set up new plane
1654
                nb_set.local_planes[ipoints_combination] = None
1✔
1655
                points_combination = [self.local_geometry.coords[ip] for ip in ipoints_combination]
1✔
1656
                if npoints == 2:
1✔
1657
                    if collinear(
1✔
1658
                        points_combination[0],
1659
                        points_combination[1],
1660
                        self.local_geometry.central_site,
1661
                        tolerance=0.25,
1662
                    ):
1663
                        continue
1✔
1664
                    plane = Plane.from_3points(
1✔
1665
                        points_combination[0],
1666
                        points_combination[1],
1667
                        self.local_geometry.central_site,
1668
                    )
1669
                elif npoints == 3:
1✔
1670
                    if collinear(
1✔
1671
                        points_combination[0],
1672
                        points_combination[1],
1673
                        points_combination[2],
1674
                        tolerance=0.25,
1675
                    ):
1676
                        continue
×
1677
                    plane = Plane.from_3points(
1✔
1678
                        points_combination[0],
1679
                        points_combination[1],
1680
                        points_combination[2],
1681
                    )
1682
                elif npoints > 3:
×
1683
                    plane = Plane.from_npoints(points_combination, best_fit="least_square_distance")
×
1684
                else:
1685
                    raise ValueError("Wrong number of points to initialize separation plane")
×
1686
                # Takes a lot of time and happens rarely ...
1687
                # if any([plane.is_same_plane_as(plane2) for comb2, plane2 in nb_set.local_planes.items()
1688
                #         if plane2 is not None]):
1689
                #     continue
1690
                nb_set.local_planes[ipoints_combination] = plane
1✔
1691
                # Get the separations for this plane
1692
                # TODO: check sensitivity to delta/delta_factor parameter
1693
                dig = plane.distances_indices_groups(points=self.local_geometry._coords, delta_factor=0.1, sign=True)
1✔
1694
                grouped_indices = dig[2]
1✔
1695
                new_seps = []
1✔
1696
                for ng in range(1, len(grouped_indices) + 1):
1✔
1697
                    inplane = list(itertools.chain(*grouped_indices[:ng]))
1✔
1698
                    if len(inplane) > self.allcg.maxpoints_inplane[cn]:
1✔
1699
                        break
1✔
1700
                    inplane = [ii[0] for ii in inplane]
1✔
1701
                    outplane = list(itertools.chain(*grouped_indices[ng:]))
1✔
1702
                    s1 = [ii_sign[0] for ii_sign in outplane if ii_sign[1] < 0]
1✔
1703
                    s2 = [ii_sign[0] for ii_sign in outplane if ii_sign[1] > 0]
1✔
1704
                    separation = sort_separation_tuple([s1, inplane, s2])
1✔
1705
                    sep = tuple(len(gg) for gg in separation)
1✔
1706
                    if sep not in self.allcg.separations_cg[cn]:
1✔
1707
                        continue
1✔
1708
                    if sep not in nb_set.separations:
1✔
1709
                        nb_set.separations[sep] = {}
1✔
1710
                    mysep = [np.array(ss, dtype=int) for ss in separation]
1✔
1711
                    nb_set.separations[sep][separation] = (plane, mysep)
1✔
1712
                    if sep == separation_plane_algo.separation:
1✔
1713
                        new_seps.append(mysep)
1✔
1714

1715
                for separation_indices in new_seps:
1✔
1716
                    cgsm = cgcsmoptim(
1✔
1717
                        coordination_geometry=coordination_geometry,
1718
                        sepplane=separation_plane_algo,
1719
                        local_plane=plane,
1720
                        points_perfect=points_perfect,
1721
                        separation_indices=separation_indices,
1722
                    )
1723
                    csm, perm, algo, _ = cgsm[0], cgsm[1], cgsm[2], cgsm[3]
1✔
1724
                    permutations_symmetry_measures.extend(csm)
1✔
1725
                    permutations.extend(perm)
1✔
1726
                    for thisperm in perm:
1✔
1727
                        p2l = {}
1✔
1728
                        l2p = {}
1✔
1729
                        for i_p, pp in enumerate(thisperm):
1✔
1730
                            p2l[i_p] = pp
1✔
1731
                            l2p[pp] = i_p
1✔
1732
                        perfect2local_maps.append(p2l)
1✔
1733
                        local2perfect_maps.append(l2p)
1✔
1734
                    algos.extend(algo)
1✔
1735

1736
        if len(permutations_symmetry_measures) == 0:
1✔
1737
            return self.coordination_geometry_symmetry_measures_fallback_random(
1✔
1738
                coordination_geometry, points_perfect=points_perfect
1739
            )
1740
        return (
1✔
1741
            permutations_symmetry_measures,
1742
            permutations,
1743
            algos,
1744
            local2perfect_maps,
1745
            perfect2local_maps,
1746
        )
1747

1748
    def _cg_csm_separation_plane(
1✔
1749
        self,
1750
        coordination_geometry,
1751
        sepplane,
1752
        local_plane,
1753
        plane_separations,
1754
        dist_tolerances=None,
1755
        testing=False,
1756
        tested_permutations=False,
1757
        points_perfect=None,
1758
    ):
1759
        argref_separation = sepplane.argsorted_ref_separation_perm
1✔
1760
        plane_found = False
1✔
1761
        permutations = []
1✔
1762
        permutations_symmetry_measures = []
1✔
1763
        if testing:
1✔
1764
            separation_permutations = []
×
1765
        dist_tolerances = dist_tolerances or DIST_TOLERANCES
1✔
1766
        for dist_tolerance in dist_tolerances:
1✔
1767
            algo = "NOT_FOUND"
1✔
1768
            separation = local_plane.indices_separate(self.local_geometry._coords, dist_tolerance)
1✔
1769
            # Do not consider facets leading to the same separation indices
1770
            separation = sort_separation(separation)
1✔
1771

1772
            if separation_in_list(separation, plane_separations):
1✔
1773
                continue
1✔
1774
            # Do not consider a separation which does not follow the reference separation of the perfect
1775
            # coordination geometry
1776
            if len(separation[1]) != len(sepplane.plane_points):
1✔
1777
                continue
1✔
1778
            if len(separation[0]) == len(sepplane.point_groups[0]):
1✔
1779
                this_separation = separation
1✔
1780
                plane_separations.append(this_separation)
1✔
1781
            elif len(separation[0]) == len(sepplane.point_groups[1]):
1✔
1782
                this_separation = [
×
1783
                    list(separation[2]),
1784
                    list(separation[1]),
1785
                    list(separation[0]),
1786
                ]
1787
                plane_separations.append(this_separation)
×
1788
            else:
1789
                continue
×
1790

1791
            if sepplane.ordered_plane:
1✔
1792
                inp = [pp for ip, pp in enumerate(self.local_geometry._coords) if ip in this_separation[1]]
1✔
1793

1794
                if sepplane.ordered_point_groups[0]:
1✔
1795
                    pp_s0 = [pp for ip, pp in enumerate(self.local_geometry._coords) if ip in this_separation[0]]
×
1796
                    ordind_s0 = local_plane.project_and_to2dim_ordered_indices(pp_s0)
×
1797
                    sep0 = [this_separation[0][ii] for ii in ordind_s0]
×
1798
                else:
1799
                    sep0 = list(this_separation[0])
1✔
1800
                if sepplane.ordered_point_groups[1]:
1✔
1801
                    pp_s2 = [pp for ip, pp in enumerate(self.local_geometry._coords) if ip in this_separation[2]]
1✔
1802
                    ordind_s2 = local_plane.project_and_to2dim_ordered_indices(pp_s2)
1✔
1803
                    sep2 = [this_separation[2][ii] for ii in ordind_s2]
1✔
1804
                else:
1805
                    sep2 = list(this_separation[2])
1✔
1806
                separation_perm = list(sep0)
1✔
1807
                ordind = local_plane.project_and_to2dim_ordered_indices(inp)
1✔
1808
                separation_perm.extend([this_separation[1][ii] for ii in ordind])
1✔
1809
                algo = "SEPARATION_PLANE_2POINTS_ORDERED"
1✔
1810
                separation_perm.extend(sep2)
1✔
1811
            else:
1812
                separation_perm = list(this_separation[0])
×
1813
                separation_perm.extend(this_separation[1])
×
1814
                algo = "SEPARATION_PLANE_2POINTS"
×
1815
                separation_perm.extend(this_separation[2])
×
1816
            if self.plane_safe_permutations:
1✔
1817
                sep_perms = sepplane.safe_separation_permutations(
×
1818
                    ordered_plane=sepplane.ordered_plane,
1819
                    ordered_point_groups=sepplane.ordered_point_groups,
1820
                )
1821
            else:
1822
                sep_perms = sepplane.permutations
1✔
1823

1824
            # plane_found = True
1825

1826
            for sep_perm in sep_perms:
1✔
1827
                perm1 = [separation_perm[ii] for ii in sep_perm]
1✔
1828
                pp = [perm1[ii] for ii in argref_separation]
1✔
1829
                # Skip permutations that have already been performed
1830
                if isinstance(tested_permutations, set) and coordination_geometry.equivalent_indices is not None:
1✔
1831
                    tuple_ref_perm = coordination_geometry.ref_permutation(pp)
×
1832
                    if tuple_ref_perm in tested_permutations:
×
1833
                        continue
×
1834
                    tested_permutations.add(tuple_ref_perm)
×
1835

1836
                permutations.append(pp)
1✔
1837
                if testing:
1✔
1838
                    separation_permutations.append(sep_perm)
×
1839

1840
                points_distorted = self.local_geometry.points_wcs_ctwcc(permutation=pp)
1✔
1841

1842
                sm_info = symmetry_measure(points_distorted=points_distorted, points_perfect=points_perfect)
1✔
1843
                sm_info["translation_vector"] = self.local_geometry.centroid_with_centre
1✔
1844

1845
                permutations_symmetry_measures.append(sm_info)
1✔
1846
            if plane_found:
1✔
1847
                break
×
1848
        if len(permutations_symmetry_measures) > 0:
1✔
1849
            if testing:
1✔
1850
                return (
×
1851
                    permutations_symmetry_measures,
1852
                    permutations,
1853
                    algo,
1854
                    separation_permutations,
1855
                )
1856
            return (
1✔
1857
                permutations_symmetry_measures,
1858
                permutations,
1859
                [sepplane.algorithm_type] * len(permutations),
1860
            )
1861
        if plane_found:
1✔
1862
            if testing:
×
1863
                return permutations_symmetry_measures, permutations, [], []
×
1864
            return permutations_symmetry_measures, permutations, []
×
1865
        if testing:
1✔
1866
            return None, None, None, None
×
1867
        return None, None, None
1✔
1868

1869
    def _cg_csm_separation_plane_optim1(
1✔
1870
        self,
1871
        coordination_geometry,
1872
        sepplane,
1873
        local_plane,
1874
        points_perfect=None,
1875
        separation_indices=None,
1876
    ):
1877
        argref_separation = sepplane.argsorted_ref_separation_perm
×
1878
        permutations = []
×
1879
        permutations_symmetry_measures = []
×
1880
        stop_search = False
×
1881
        # TODO: do not do that several times ... also keep in memory
1882
        if sepplane.ordered_plane:
×
1883
            inp = [pp for ip, pp in enumerate(self.local_geometry._coords) if ip in separation_indices[1]]
×
1884
            if sepplane.ordered_point_groups[0]:
×
1885
                pp_s0 = [pp for ip, pp in enumerate(self.local_geometry._coords) if ip in separation_indices[0]]
×
1886
                ordind_s0 = local_plane.project_and_to2dim_ordered_indices(pp_s0)
×
1887
                sep0 = [separation_indices[0][ii] for ii in ordind_s0]
×
1888
            else:
1889
                sep0 = list(separation_indices[0])
×
1890
            if sepplane.ordered_point_groups[1]:
×
1891
                pp_s2 = [pp for ip, pp in enumerate(self.local_geometry._coords) if ip in separation_indices[2]]
×
1892
                ordind_s2 = local_plane.project_and_to2dim_ordered_indices(pp_s2)
×
1893
                sep2 = [separation_indices[2][ii] for ii in ordind_s2]
×
1894
            else:
1895
                sep2 = list(separation_indices[2])
×
1896
            separation_perm = list(sep0)
×
1897
            ordind = local_plane.project_and_to2dim_ordered_indices(inp)
×
1898
            separation_perm.extend([separation_indices[1][ii] for ii in ordind])
×
1899
            separation_perm.extend(sep2)
×
1900
        else:
1901
            separation_perm = list(separation_indices[0])
×
1902
            separation_perm.extend(separation_indices[1])
×
1903
            separation_perm.extend(separation_indices[2])
×
1904

1905
        if self.plane_safe_permutations:
×
1906
            sep_perms = sepplane.safe_separation_permutations(
×
1907
                ordered_plane=sepplane.ordered_plane,
1908
                ordered_point_groups=sepplane.ordered_point_groups,
1909
            )
1910
        else:
1911
            sep_perms = sepplane.permutations
×
1912

1913
        for sep_perm in sep_perms:
×
1914
            perm1 = [separation_perm[ii] for ii in sep_perm]
×
1915
            pp = [perm1[ii] for ii in argref_separation]
×
1916

1917
            permutations.append(pp)
×
1918

1919
            points_distorted = self.local_geometry.points_wcs_ctwcc(permutation=pp)
×
1920

1921
            sm_info = symmetry_measure(points_distorted=points_distorted, points_perfect=points_perfect)
×
1922

1923
            sm_info["translation_vector"] = self.local_geometry.centroid_with_centre
×
1924

1925
            permutations_symmetry_measures.append(sm_info)
×
1926

1927
        if len(permutations_symmetry_measures) > 0:
×
1928
            return (
×
1929
                permutations_symmetry_measures,
1930
                permutations,
1931
                [sepplane.algorithm_type] * len(permutations),
1932
                stop_search,
1933
            )
1934
        return [], [], [], stop_search
×
1935

1936
    def _cg_csm_separation_plane_optim2(
1✔
1937
        self,
1938
        coordination_geometry,
1939
        sepplane,
1940
        local_plane,
1941
        points_perfect=None,
1942
        separation_indices=None,
1943
    ):
1944
        argref_separation = sepplane.argsorted_ref_separation_perm
1✔
1945
        permutations = []
1✔
1946
        permutations_symmetry_measures = []
1✔
1947
        stop_search = False
1✔
1948
        # TODO: do not do that several times ... also keep in memory
1949
        if sepplane.ordered_plane:
1✔
1950
            inp = self.local_geometry.coords.take(separation_indices[1], axis=0)
1✔
1951
            if sepplane.ordered_point_groups[0]:
1✔
1952
                pp_s0 = self.local_geometry.coords.take(separation_indices[0], axis=0)
1✔
1953
                ordind_s0 = local_plane.project_and_to2dim_ordered_indices(pp_s0)
1✔
1954
                # sep0 = [separation_indices[0][ii] for ii in ordind_s0]
1955
                sep0 = separation_indices[0].take(ordind_s0)
1✔
1956
            else:
1957
                # sep0 = list(separation_indices[0])
1958
                sep0 = separation_indices[0]
1✔
1959
            if sepplane.ordered_point_groups[1]:
1✔
1960
                pp_s2 = self.local_geometry.coords.take(separation_indices[2], axis=0)
1✔
1961
                ordind_s2 = local_plane.project_and_to2dim_ordered_indices(pp_s2)
1✔
1962
                # sep2 = [separation_indices[2][ii] for ii in ordind_s2]
1963
                sep2 = separation_indices[2].take(ordind_s2)
1✔
1964
            else:
1965
                # sep2 = list(separation_indices[2])
1966
                sep2 = separation_indices[2]
1✔
1967
            # separation_perm = list(sep0)
1968
            ordind = local_plane.project_and_to2dim_ordered_indices(inp)
1✔
1969
            # separation_perm.extend(
1970
            #     [separation_indices[1][ii] for ii in ordind])
1971
            inp1 = separation_indices[1].take(ordind)
1✔
1972
            # separation_perm.extend(sep2)
1973
            separation_perm = np.concatenate((sep0, inp1, sep2))
1✔
1974
        else:
1975
            # separation_perm = list(separation_indices[0])
1976
            # separation_perm.extend(separation_indices[1])
1977
            # separation_perm.extend(separation_indices[2])
1978
            separation_perm = np.concatenate(separation_indices)
1✔
1979

1980
        if self.plane_safe_permutations:
1✔
1981
            sep_perms = sepplane.safe_separation_permutations(
×
1982
                ordered_plane=sepplane.ordered_plane,
1983
                ordered_point_groups=sepplane.ordered_point_groups,
1984
            )
1985
        else:
1986
            sep_perms = sepplane.permutations
1✔
1987

1988
        for sep_perm in sep_perms:
1✔
1989
            perm1 = separation_perm.take(sep_perm)
1✔
1990
            pp = perm1.take(argref_separation)
1✔
1991

1992
            permutations.append(pp)
1✔
1993

1994
            points_distorted = self.local_geometry.points_wcs_ctwcc(permutation=pp)
1✔
1995

1996
            sm_info = symmetry_measure(points_distorted=points_distorted, points_perfect=points_perfect)
1✔
1997

1998
            sm_info["translation_vector"] = self.local_geometry.centroid_with_centre
1✔
1999

2000
            permutations_symmetry_measures.append(sm_info)
1✔
2001

2002
        if len(permutations_symmetry_measures) > 0:
1✔
2003
            return (
1✔
2004
                permutations_symmetry_measures,
2005
                permutations,
2006
                [sepplane.algorithm_type] * len(permutations),
2007
                stop_search,
2008
            )
2009
        return [], [], [], stop_search
×
2010

2011
    def coordination_geometry_symmetry_measures_fallback_random(
1✔
2012
        self, coordination_geometry, NRANDOM=10, points_perfect=None
2013
    ):
2014
        """
2015
        Returns the symmetry measures for a random set of permutations for the coordination geometry
2016
        "coordination_geometry". Fallback implementation for the plane separation algorithms measures
2017
        of each permutation
2018
        :param coordination_geometry: The coordination geometry to be investigated
2019
        :param NRANDOM: Number of random permutations to be tested
2020
        :return: The symmetry measures for the given coordination geometry for each permutation investigated
2021
        """
2022
        permutations_symmetry_measures = [None] * NRANDOM
1✔
2023
        permutations = []
1✔
2024
        algos = []
1✔
2025
        perfect2local_maps = []
1✔
2026
        local2perfect_maps = []
1✔
2027
        for iperm in range(NRANDOM):
1✔
2028
            perm = np.random.permutation(coordination_geometry.coordination_number)
1✔
2029
            permutations.append(perm)
1✔
2030
            p2l = {}
1✔
2031
            l2p = {}
1✔
2032
            for i_p, pp in enumerate(perm):
1✔
2033
                p2l[i_p] = pp
1✔
2034
                l2p[pp] = i_p
1✔
2035
            perfect2local_maps.append(p2l)
1✔
2036
            local2perfect_maps.append(l2p)
1✔
2037

2038
            points_distorted = self.local_geometry.points_wcs_ctwcc(permutation=perm)
1✔
2039
            sm_info = symmetry_measure(points_distorted=points_distorted, points_perfect=points_perfect)
1✔
2040
            sm_info["translation_vector"] = self.local_geometry.centroid_with_centre
1✔
2041

2042
            permutations_symmetry_measures[iperm] = sm_info
1✔
2043
            algos.append("APPROXIMATE_FALLBACK")
1✔
2044
        return (
1✔
2045
            permutations_symmetry_measures,
2046
            permutations,
2047
            algos,
2048
            local2perfect_maps,
2049
            perfect2local_maps,
2050
        )
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