• 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

82.2
/pymatgen/io/lobster/lobsterenv.py
1
# Copyright (c) Pymatgen Development Team.
2
# Distributed under the terms of the MIT License.
3

4
"""
1✔
5
This module provides classes to perform analyses of
6
the local environments (e.g., finding near neighbors)
7
of single sites in molecules and structures based on
8
bonding analysis with Lobster.
9
"""
10
from __future__ import annotations
1✔
11

12
import collections
1✔
13
import copy
1✔
14
import math
1✔
15
import os
1✔
16

17
import numpy as np
1✔
18

19
from pymatgen.analysis.bond_valence import BVAnalyzer
1✔
20
from pymatgen.analysis.chemenv.coordination_environments.coordination_geometry_finder import (
1✔
21
    LocalGeometryFinder,
22
)
23
from pymatgen.analysis.chemenv.coordination_environments.structure_environments import (
1✔
24
    LightStructureEnvironments,
25
)
26
from pymatgen.analysis.local_env import NearNeighbors
1✔
27
from pymatgen.core.structure import Structure
1✔
28
from pymatgen.electronic_structure.cohp import CompleteCohp
1✔
29
from pymatgen.electronic_structure.core import Spin
1✔
30
from pymatgen.electronic_structure.plotter import CohpPlotter
1✔
31
from pymatgen.io.lobster import Charge, Icohplist
1✔
32

33
__author__ = "Janine George"
1✔
34
__copyright__ = "Copyright 2021, The Materials Project"
1✔
35
__version__ = "1.0"
1✔
36
__maintainer__ = "J. George"
1✔
37
__email__ = "janinegeorge.ulfen@gmail.com"
1✔
38
__status__ = "Production"
1✔
39
__date__ = "February 2, 2021"
1✔
40

41

42
class LobsterNeighbors(NearNeighbors):
1✔
43
    """
44
    This class combines capabilities from LocalEnv and ChemEnv to determine coordination environments based on
45
    bonding analysis
46
    """
47

48
    def __init__(
1✔
49
        self,
50
        are_coops=False,
51
        filename_ICOHP=None,
52
        valences=None,
53
        limits=None,
54
        structure=None,
55
        additional_condition=0,
56
        only_bonds_to=None,
57
        perc_strength_ICOHP=0.15,
58
        valences_from_charges=False,
59
        filename_CHARGE=None,
60
        which_charge="Mulliken",
61
        adapt_extremum_to_add_cond=False,
62
        add_additional_data_sg=False,
63
        filename_blist_sg1=None,
64
        filename_blist_sg2=None,
65
        id_blist_sg1="ICOOP",
66
        id_blist_sg2="ICOBI",
67
    ):
68
        """
69

70
        Args:
71
            are_coops: (bool) if True, the file is a ICOOPLIST.lobster and not a ICOHPLIST.lobster; only tested for
72
            ICOHPLIST.lobster so far
73
            filename_ICOHP: (str) Path to ICOHPLIST.lobster
74
            valences: (list of integers/floats) gives valence/charge for each element
75
            limits: limit to decide which ICOHPs should be considered
76
            structure: (Structure Object) typically constructed by: Structure.from_file("POSCAR") (Structure object
77
            from pymatgen.core.structure)
78
            additional_condition:   Additional condition that decides which kind of bonds will be considered
79
                                    NO_ADDITIONAL_CONDITION = 0
80
                                    ONLY_ANION_CATION_BONDS = 1
81
                                    NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 2
82
                                    ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3
83
                                    ONLY_ELEMENT_TO_OXYGEN_BONDS = 4
84
                                    DO_NOT_CONSIDER_ANION_CATION_BONDS=5
85
                                    ONLY_CATION_CATION_BONDS=6
86
            only_bonds_to: (list of str) will only consider bonds to certain elements (e.g. ["O"] for oxygen)
87
            perc_strength_ICOHP: if no limits are given, this will decide which icohps will still be considered (
88
            relative to
89
            the strongest ICOHP)
90
            valences_from_charges: if True and path to CHARGE.lobster is provided, will use Lobster charges (
91
            Mulliken) instead of valences
92
            filename_CHARGE: (str) Path to Charge.lobster
93
            which_charge: (str) "Mulliken" or "Loewdin"
94
            adapt_extremum_to_add_cond: (bool) will adapt the limits to only focus on the bonds determined by the
95
            additional condition
96
            add_additional_data_sg: (bool) will add the information from filename_add_bondinglist_sg1,
97
            filename_blist_sg1: (str) Path to additional ICOOP, ICOBI data for structure graphs
98
            filename_blist_sg2: (str) Path to dditional ICOOP, ICOBI data for structure graphs
99
            id_blist_sg1: (str) Identity of data in filename_blist_sg1,
100
                e.g., "icoop" or "icobi"
101
            id_blist_sg2: (str) Identity of data in filename_blist_sg2,
102
                e.g., "icoop" or "icobi"
103
        """
104
        self.ICOHP = Icohplist(are_coops=are_coops, filename=filename_ICOHP)
1✔
105
        self.Icohpcollection = self.ICOHP.icohpcollection
1✔
106
        self.structure = structure
1✔
107
        self.limits = limits
1✔
108
        self.only_bonds_to = only_bonds_to
1✔
109
        self.adapt_extremum_to_add_cond = adapt_extremum_to_add_cond
1✔
110
        self.are_coops = are_coops
1✔
111
        self.add_additional_data_sg = add_additional_data_sg
1✔
112
        self.filename_blist_sg1 = filename_blist_sg1
1✔
113
        self.filename_blist_sg2 = filename_blist_sg2
1✔
114

115
        allowed_arguments = ["icoop", "icobi"]
1✔
116
        if id_blist_sg1.lower() not in allowed_arguments or id_blist_sg2.lower() not in allowed_arguments:
1✔
117
            raise ValueError("Algorithm can only work with ICOOPs, ICOBIs")
1✔
118
        self.id_blist_sg1 = id_blist_sg1
1✔
119
        self.id_blist_sg2 = id_blist_sg2
1✔
120
        if add_additional_data_sg:
1✔
121
            if self.id_blist_sg1.lower() == "icoop":
1✔
122
                are_coops_id1 = True
×
123
                are_cobis_id1 = False
×
124
            elif self.id_blist_sg1.lower() == "icobi":
1✔
125
                are_coops_id1 = False
1✔
126
                are_cobis_id1 = True
1✔
127
            else:
128
                raise ValueError("only icoops and icobis can be added")
×
129
            self.bonding_list_1 = Icohplist(
1✔
130
                filename=self.filename_blist_sg1,
131
                are_coops=are_coops_id1,
132
                are_cobis=are_cobis_id1,
133
            )
134

135
            if self.id_blist_sg2.lower() == "icoop":
1✔
136
                are_coops_id2 = True
1✔
137
                are_cobis_id2 = False
1✔
138
            elif self.id_blist_sg2.lower() == "icobi":
×
139
                are_coops_id2 = False
×
140
                are_cobis_id2 = True
×
141

142
            self.bonding_list_2 = Icohplist(
1✔
143
                filename=self.filename_blist_sg2,
144
                are_coops=are_coops_id2,
145
                are_cobis=are_cobis_id2,
146
            )
147

148
        if are_coops:
1✔
149
            raise ValueError("Algorithm only works correctly for ICOHPLIST.lobster")
1✔
150

151
        # will check if the additional condition is correctly delivered
152
        if additional_condition in range(0, 7):
1✔
153
            self.additional_condition = additional_condition
1✔
154
        else:
155
            raise ValueError("No correct additional condition")
1✔
156

157
        # will read in valences, will prefer manual setting of valences
158
        if valences is None:
1✔
159
            if valences_from_charges and filename_CHARGE is not None:
1✔
160
                chg = Charge(filename=filename_CHARGE)
1✔
161
                if which_charge == "Mulliken":
1✔
162
                    self.valences = chg.Mulliken
1✔
163
                elif which_charge == "Loewdin":
1✔
164
                    self.valences = chg.Loewdin
1✔
165
            else:
166
                bv_analyzer = BVAnalyzer()
1✔
167
                try:
1✔
168
                    self.valences = bv_analyzer.get_valences(structure=self.structure)
1✔
169
                except ValueError:
1✔
170
                    self.valences = None
1✔
171
                    if additional_condition in [1, 3, 5, 6]:
1✔
172
                        raise ValueError(
1✔
173
                            "Valences cannot be assigned, additional_conditions 1 and 3 and 5 and 6 will not work"
174
                        )
175
        else:
176
            self.valences = valences
1✔
177
        if np.allclose(np.array(self.valences), np.zeros(np.array(self.valences).shape)) and additional_condition in [
1✔
178
            1,
179
            3,
180
            5,
181
            6,
182
        ]:
183
            raise ValueError("All valences are equal to 0, additional_conditions 1 and 3 and 5 and 6 will not work")
1✔
184

185
        if limits is None:
1✔
186
            self.lowerlimit = None
1✔
187
            self.upperlimit = None
1✔
188

189
        else:
190
            self.lowerlimit = limits[0]
1✔
191
            self.upperlimit = limits[1]
1✔
192

193
        # will evaluate coordination environments
194
        self._evaluate_ce(
1✔
195
            lowerlimit=self.lowerlimit,
196
            upperlimit=self.upperlimit,
197
            only_bonds_to=only_bonds_to,
198
            additional_condition=self.additional_condition,
199
            perc_strength_ICOHP=perc_strength_ICOHP,
200
            adapt_extremum_to_add_cond=adapt_extremum_to_add_cond,
201
        )
202

203
    @property
1✔
204
    def structures_allowed(self):
1✔
205
        """
206
        Boolean property: can this NearNeighbors class be used with Structure
207
        objects?
208
        """
209
        return True
1✔
210

211
    @property
1✔
212
    def molecules_allowed(self):
1✔
213
        """
214
        Boolean property: can this NearNeighbors class be used with Molecule
215
        objects?
216
        """
217
        return False
1✔
218

219
    def get_anion_types(self):
1✔
220
        """
221
        Return the types of anions present in crystal structure
222
        Returns:
223
        """
224
        if self.valences is None:
×
225
            raise ValueError("No cations and anions defined")
×
226

227
        anion_species = []
×
228
        for site, val in zip(self.structure, self.valences):
×
229
            if val < 0.0:
×
230
                anion_species.append(site.specie)
×
231

232
        return set(anion_species)
×
233

234
    def get_nn_info(self, structure: Structure, n, use_weights=False):
1✔
235
        """
236
        Get coordination number, CN, of site with index n in structure.
237

238
        Args:
239
            structure (Structure): input structure.
240
            n (int): index of site for which to determine CN.
241
            use_weights (bool): flag indicating whether (True)
242
                to use weights for computing the coordination number
243
                or not (False, default: each coordinated site has equal
244
                weight).
245
                True is not implemented for LobsterNeighbors
246
        Returns:
247
            cn (integer or float): coordination number.
248
        """
249
        if use_weights:
1✔
250
            raise ValueError("LobsterEnv cannot use weights")
×
251
        if len(structure) != len(self.structure):
1✔
252
            raise ValueError("The wrong structure was provided")
×
253
        return self.sg_list[n]
1✔
254

255
    def get_light_structure_environment(self, only_cation_environments=False, only_indices=None):
1✔
256
        """
257
        Return a LobsterLightStructureEnvironments object
258
        if the structure only contains coordination environments smaller 13
259

260
        Args:
261
            only_cation_environments: only data for cations will be returned
262
            only_indices: will only evaluate the list of isites in this list
263
        Returns: LobsterLightStructureEnvironments Object
264
        """
265
        lgf = LocalGeometryFinder()
1✔
266
        lgf.setup_structure(structure=self.structure)
1✔
267
        list_ce_symbols = []
1✔
268
        list_csm = []
1✔
269
        list_permut = []
1✔
270
        for ival, _neigh_coords in enumerate(self.list_coords):
1✔
271
            if (len(_neigh_coords)) > 13:
1✔
272
                raise ValueError("Environment cannot be determined. Number of neighbors is larger than 13.")
×
273
            # to avoid problems if _neigh_coords is empty
274
            if _neigh_coords != []:
1✔
275
                lgf.setup_local_geometry(isite=ival, coords=_neigh_coords, optimization=2)
1✔
276
                cncgsm = lgf.get_coordination_symmetry_measures(optimization=2)
1✔
277
                list_ce_symbols.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[0])
1✔
278
                list_csm.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[1]["csm_wcs_ctwcc"])
1✔
279
                list_permut.append(min(cncgsm.items(), key=lambda t: t[1]["csm_wcs_ctwcc"])[1]["indices"])
1✔
280
            else:
281
                list_ce_symbols.append(None)
×
282
                list_csm.append(None)
×
283
                list_permut.append(None)
×
284

285
        if only_indices is None:
1✔
286
            if not only_cation_environments:
1✔
287
                lse = LobsterLightStructureEnvironments.from_Lobster(
1✔
288
                    list_ce_symbol=list_ce_symbols,
289
                    list_csm=list_csm,
290
                    list_permutation=list_permut,
291
                    list_neighsite=self.list_neighsite,
292
                    list_neighisite=self.list_neighisite,
293
                    structure=self.structure,
294
                    valences=self.valences,
295
                )
296
            else:
297
                new_list_ce_symbols = []
×
298
                new_list_csm = []
×
299
                new_list_permut = []
×
300
                new_list_neighsite = []
×
301
                new_list_neighisite = []
×
302

303
                for ival, val in enumerate(self.valences):
×
304
                    if val >= 0.0:
×
305
                        new_list_ce_symbols.append(list_ce_symbols[ival])
×
306
                        new_list_csm.append(list_csm[ival])
×
307
                        new_list_permut.append(list_permut[ival])
×
308
                        new_list_neighisite.append(self.list_neighisite[ival])
×
309
                        new_list_neighsite.append(self.list_neighsite[ival])
×
310
                    else:
311
                        new_list_ce_symbols.append(None)
×
312
                        new_list_csm.append(None)
×
313
                        new_list_permut.append([])
×
314
                        new_list_neighisite.append([])
×
315
                        new_list_neighsite.append([])
×
316

317
                lse = LobsterLightStructureEnvironments.from_Lobster(
×
318
                    list_ce_symbol=new_list_ce_symbols,
319
                    list_csm=new_list_csm,
320
                    list_permutation=new_list_permut,
321
                    list_neighsite=new_list_neighsite,
322
                    list_neighisite=new_list_neighisite,
323
                    structure=self.structure,
324
                    valences=self.valences,
325
                )
326
        else:
327
            new_list_ce_symbols = []
×
328
            new_list_csm = []
×
329
            new_list_permut = []
×
330
            new_list_neighsite = []
×
331
            new_list_neighisite = []
×
332

333
            for isite, _site in enumerate(self.structure):
×
334
                if isite in only_indices:
×
335
                    new_list_ce_symbols.append(list_ce_symbols[isite])
×
336
                    new_list_csm.append(list_csm[isite])
×
337
                    new_list_permut.append(list_permut[isite])
×
338
                    new_list_neighisite.append(self.list_neighisite[isite])
×
339
                    new_list_neighsite.append(self.list_neighsite[isite])
×
340
                else:
341
                    new_list_ce_symbols.append(None)
×
342
                    new_list_csm.append(None)
×
343
                    new_list_permut.append([])
×
344
                    new_list_neighisite.append([])
×
345
                    new_list_neighsite.append([])
×
346

347
            lse = LobsterLightStructureEnvironments.from_Lobster(
×
348
                list_ce_symbol=new_list_ce_symbols,
349
                list_csm=new_list_csm,
350
                list_permutation=new_list_permut,
351
                list_neighsite=new_list_neighsite,
352
                list_neighisite=new_list_neighisite,
353
                structure=self.structure,
354
                valences=self.valences,
355
            )
356

357
        return lse
1✔
358

359
    def get_info_icohps_to_neighbors(self, isites=None, onlycation_isites=True):
1✔
360
        """
361
        this method Return information of cohps of neighbors
362

363
        Args:
364
            isites: list of site ids, if isite==None, all isites will be used to add the icohps of the neighbors
365
            onlycation_isites: will only use cations, if isite==[]
366

367

368
        Returns:
369
            sum of icohps of neighbors to certain sites [given by the id in structure], number of bonds to this site,
370
            labels (from ICOHPLIST) for
371
            these bonds
372
            [the latter is useful for plotting summed COHP plots],
373
            list of the central isite for each label
374
        """
375
        if self.valences is None and onlycation_isites:
1✔
376
            raise ValueError("No valences are provided")
×
377
        if isites is None:
1✔
378
            if onlycation_isites:
1✔
379
                isites = [i for i in range(len(self.structure)) if self.valences[i] >= 0.0]
1✔
380
            else:
381
                isites = list(range(len(self.structure)))
×
382

383
        summed_icohps = 0.0
1✔
384
        list_icohps = []
1✔
385
        number_bonds = 0
1✔
386
        labels = []
1✔
387
        atoms = []
1✔
388
        final_isites = []
1✔
389
        for ival, _site in enumerate(self.structure):
1✔
390
            if ival in isites:
1✔
391
                for keys, icohpsum in zip(self.list_keys[ival], self.list_icohps[ival]):
1✔
392
                    summed_icohps += icohpsum
1✔
393
                    list_icohps.append(icohpsum)
1✔
394
                    labels.append(keys)
1✔
395
                    atoms.append(
1✔
396
                        [
397
                            self.Icohpcollection._list_atom1[int(keys) - 1],
398
                            self.Icohpcollection._list_atom2[int(keys) - 1],
399
                        ]
400
                    )
401
                    number_bonds += 1
1✔
402
                    final_isites.append(ival)
1✔
403
        return summed_icohps, list_icohps, number_bonds, labels, atoms, final_isites
1✔
404

405
    def plot_cohps_of_neighbors(
1✔
406
        self,
407
        path_to_COHPCAR="COHPCAR.lobster",
408
        isites=None,
409
        onlycation_isites=True,
410
        only_bonds_to=None,
411
        per_bond=False,
412
        summed_spin_channels=False,
413
        xlim=None,
414
        ylim=(-10, 6),
415
        integrated=False,
416
    ):
417
        """
418
        Will plot summed cohps (please be careful in the spin polarized case (plots might overlap (exactly!))
419

420
        Args:
421
            isites: list of site ids, if isite==[], all isites will be used to add the icohps of the neighbors
422
            onlycation_isites: bool, will only use cations, if isite==[]
423
            only_bonds_to: list of str, only anions in this list will be considered
424
            per_bond: bool, will lead to a normalization of the plotted COHP per number of bond if True,
425
            otherwise the sum
426
            will be plotted
427
            xlim: list of float, limits of x values
428
            ylim: list of float, limits of y values
429
            integrated: bool, if true will show integrated cohp instead of cohp
430

431
        Returns:
432
            plt of the cohps
433
        """
434
        # include COHPPlotter and plot a sum of these COHPs
435
        # might include option to add Spin channels
436
        # implement only_bonds_to
437
        cp = CohpPlotter()
1✔
438

439
        plotlabel, summed_cohp = self.get_info_cohps_to_neighbors(
1✔
440
            path_to_COHPCAR,
441
            isites,
442
            only_bonds_to,
443
            onlycation_isites,
444
            per_bond,
445
            summed_spin_channels=summed_spin_channels,
446
        )
447

448
        cp.add_cohp(plotlabel, summed_cohp)
1✔
449
        plot = cp.get_plot(integrated=integrated)
1✔
450
        if xlim is not None:
1✔
451
            plot.xlim(xlim)
1✔
452

453
        if ylim is not None:
1✔
454
            plot.ylim(ylim)
1✔
455

456
        return plot
1✔
457

458
    def get_info_cohps_to_neighbors(
1✔
459
        self,
460
        path_to_COHPCAR="COHPCAR.lobster",
461
        isites=None,
462
        only_bonds_to=None,
463
        onlycation_isites=True,
464
        per_bond=True,
465
        summed_spin_channels=False,
466
    ):
467
        """
468
        Return info about the cohps from all sites mentioned in isites with neighbors
469

470
        Args:
471
            path_to_COHPCAR: str, path to COHPCAR
472
            isites: list of int that indicate the number of the site
473
            only_bonds_to: list of str, e.g. ["O"] to only show cohps of anything to oxygen
474
            onlycation_isites: if isites=None, only cation sites will be returned
475
            per_bond: will normalize per bond
476
            summed_spin_channels: will sum all spin channels
477

478
        Returns: label for cohp (str), CompleteCohp object which describes all cohps of the sites as given by isites
479
        and the other parameters
480
        """
481
        # TODO: add options for orbital-resolved cohps
482
        (
1✔
483
            summed_icohps,
484
            list_icohps,
485
            number_bonds,
486
            labels,
487
            atoms,
488
            final_isites,
489
        ) = self.get_info_icohps_to_neighbors(isites=isites, onlycation_isites=onlycation_isites)
490

491
        import tempfile
1✔
492

493
        with tempfile.TemporaryDirectory() as t:
1✔
494
            path = os.path.join(t, "POSCAR.vasp")
1✔
495

496
            self.structure.to(filename=path, fmt="POSCAR")
1✔
497

498
            if not hasattr(self, "completecohp"):
1✔
499
                self.completecohp = CompleteCohp.from_file(fmt="LOBSTER", filename=path_to_COHPCAR, structure_file=path)
1✔
500

501
        # will check that the number of bonds in ICOHPLIST and COHPCAR are identical
502
        # further checks could be implemented
503
        if len(self.Icohpcollection._list_atom1) != len(self.completecohp.bonds):
1✔
504
            raise ValueError("COHPCAR and ICOHPLIST do not fit together")
×
505
        is_spin_completecohp = Spin.down in self.completecohp.get_cohp_by_label("1").cohp
1✔
506
        if self.Icohpcollection.is_spin_polarized != is_spin_completecohp:
1✔
507
            raise ValueError("COHPCAR and ICOHPLIST do not fit together")
1✔
508

509
        if only_bonds_to is None:
1✔
510
            # sort by anion type
511
            if per_bond:
1✔
512
                divisor = len(labels)
×
513
            else:
514
                divisor = 1
1✔
515

516
            plotlabel = self._get_plot_label(atoms, per_bond)
1✔
517
            summed_cohp = self.completecohp.get_summed_cohp_by_label_list(
1✔
518
                label_list=labels,
519
                divisor=divisor,
520
                summed_spin_channels=summed_spin_channels,
521
            )
522

523
        else:
524
            # labels of the COHPs that will be summed!
525
            # iterate through labels and atoms and check which bonds can be included
526
            new_labels = []
1✔
527
            new_atoms = []
1✔
528
            # print(labels)
529
            # print(atoms)
530
            for label, atompair, isite in zip(labels, atoms, final_isites):
1✔
531
                present = False
1✔
532
                for atomtype in only_bonds_to:
1✔
533
                    # This is necessary to identify also bonds between the same elements correctly!
534
                    if str(self.structure[isite].species.elements[0]) != atomtype:
1✔
535
                        if atomtype in (
1✔
536
                            self._split_string(atompair[0])[0],
537
                            self._split_string(atompair[1])[0],
538
                        ):
539
                            present = True
1✔
540
                    else:
541
                        if (
1✔
542
                            atomtype == self._split_string(atompair[0])[0]
543
                            and atomtype == self._split_string(atompair[1])[0]
544
                        ):
545
                            present = True
1✔
546

547
                if present:
1✔
548
                    new_labels.append(label)
1✔
549
                    new_atoms.append(atompair)
1✔
550
            # print(new_labels)
551
            if len(new_labels) > 0:
1✔
552
                if per_bond:
1✔
553
                    divisor = len(new_labels)
1✔
554
                else:
555
                    divisor = 1
1✔
556

557
                plotlabel = self._get_plot_label(new_atoms, per_bond)
1✔
558
                summed_cohp = self.completecohp.get_summed_cohp_by_label_list(
1✔
559
                    label_list=new_labels,
560
                    divisor=divisor,
561
                    summed_spin_channels=summed_spin_channels,
562
                )
563
            else:
564
                plotlabel = None
1✔
565

566
                summed_cohp = None
1✔
567

568
        return plotlabel, summed_cohp
1✔
569

570
    def _get_plot_label(self, atoms, per_bond):
1✔
571
        # count the types of bonds and append a label:
572
        all_labels = []
1✔
573
        for atomsnames in atoms:
1✔
574
            new = [
1✔
575
                self._split_string(atomsnames[0])[0],
576
                self._split_string(atomsnames[1])[0],
577
            ]
578
            new.sort()
1✔
579
            # print(new2)
580
            string_here = new[0] + "-" + new[1]
1✔
581
            all_labels.append(string_here)
1✔
582
        count = collections.Counter(all_labels)
1✔
583
        plotlabels = []
1✔
584
        for key, item in count.items():
1✔
585
            plotlabels.append(str(item) + " x " + str(key))
1✔
586
        plotlabel = ", ".join(plotlabels)
1✔
587
        if per_bond:
1✔
588
            plotlabel = plotlabel + " (per bond)"
1✔
589
        return plotlabel
1✔
590

591
    def get_info_icohps_between_neighbors(self, isites=None, onlycation_isites=True):
1✔
592
        """
593
        Return infos about interactions between neighbors of a certain atom
594

595
        Args:
596
            isites: list of site ids, if isite==None, all isites will be used
597
            onlycation_isites: will only use cations, if isite==None
598

599
        Returns:
600
            tuple: summed_icohps, list_icohps, number_bonds, label_list, atoms_list
601
        """
602
        lowerlimit = self.lowerlimit
1✔
603
        upperlimit = self.upperlimit
1✔
604

605
        if self.valences is None and onlycation_isites:
1✔
606
            raise ValueError("No valences are provided")
×
607
        if isites is None:
1✔
608
            if onlycation_isites:
×
609
                isites = [i for i in range(len(self.structure)) if self.valences[i] >= 0.0]
×
610
            else:
611
                isites = list(range(len(self.structure)))
×
612

613
        summed_icohps = 0.0
1✔
614
        list_icohps = []
1✔
615
        number_bonds = 0
1✔
616
        label_list = []
1✔
617
        atoms_list = []
1✔
618
        for isite in isites:
1✔
619
            for in_site, n_site in enumerate(self.list_neighsite[isite]):
1✔
620
                for in_site2, n_site2 in enumerate(self.list_neighsite[isite]):
1✔
621
                    if in_site < in_site2:
1✔
622
                        unitcell1 = self._determine_unit_cell(n_site)
1✔
623
                        unitcell2 = self._determine_unit_cell(n_site2)
1✔
624

625
                        index_n_site = self._get_original_site(self.structure, n_site)
1✔
626
                        index_n_site2 = self._get_original_site(self.structure, n_site2)
1✔
627

628
                        if index_n_site < index_n_site2:
1✔
629
                            translation = list(np.array(unitcell1) - np.array(unitcell2))
1✔
630
                        elif index_n_site2 < index_n_site:
1✔
631
                            translation = list(np.array(unitcell2) - np.array(unitcell1))
1✔
632
                        else:
633
                            translation = list(np.array(unitcell1) - np.array(unitcell2))
1✔
634

635
                        icohps = self._get_icohps(
1✔
636
                            icohpcollection=self.Icohpcollection,
637
                            isite=index_n_site,
638
                            lowerlimit=lowerlimit,
639
                            upperlimit=upperlimit,
640
                            only_bonds_to=self.only_bonds_to,
641
                        )
642

643
                        done = False
1✔
644
                        for icohp in icohps.values():
1✔
645
                            atomnr1 = self._get_atomnumber(icohp._atom1)
1✔
646
                            atomnr2 = self._get_atomnumber(icohp._atom2)
1✔
647
                            label = icohp._label
1✔
648

649
                            if (index_n_site == atomnr1 and index_n_site2 == atomnr2) or (
1✔
650
                                index_n_site == atomnr2 and index_n_site2 == atomnr1
651
                            ):
652
                                if atomnr1 != atomnr2:
1✔
653
                                    if np.all(np.asarray(translation) == np.asarray(icohp._translation)):
1✔
654
                                        summed_icohps += icohp.summed_icohp
1✔
655
                                        list_icohps.append(icohp.summed_icohp)
1✔
656
                                        number_bonds += 1
1✔
657
                                        label_list.append(label)
1✔
658
                                        atoms_list.append(
1✔
659
                                            [
660
                                                self.Icohpcollection._list_atom1[int(label) - 1],
661
                                                self.Icohpcollection._list_atom2[int(label) - 1],
662
                                            ]
663
                                        )
664

665
                                else:
666
                                    if not done:
1✔
667
                                        if (np.all(np.asarray(translation) == np.asarray(icohp._translation))) or (
1✔
668
                                            np.all(
669
                                                np.asarray(translation)
670
                                                == np.asarray(
671
                                                    [
672
                                                        -icohp._translation[0],
673
                                                        -icohp._translation[1],
674
                                                        -icohp._translation[2],
675
                                                    ]
676
                                                )
677
                                            )
678
                                        ):
679
                                            summed_icohps += icohp.summed_icohp
1✔
680
                                            list_icohps.append(icohp.summed_icohp)
1✔
681
                                            number_bonds += 1
1✔
682
                                            label_list.append(label)
1✔
683
                                            atoms_list.append(
1✔
684
                                                [
685
                                                    self.Icohpcollection._list_atom1[int(label) - 1],
686
                                                    self.Icohpcollection._list_atom2[int(label) - 1],
687
                                                ]
688
                                            )
689
                                            done = True
1✔
690

691
        return summed_icohps, list_icohps, number_bonds, label_list, atoms_list
1✔
692

693
    def _evaluate_ce(
1✔
694
        self,
695
        lowerlimit,
696
        upperlimit,
697
        only_bonds_to=None,
698
        additional_condition=0,
699
        perc_strength_ICOHP=0.15,
700
        adapt_extremum_to_add_cond=False,
701
    ):
702
        """
703
        Args:
704
            lowerlimit: lower limit which determines the ICOHPs that are considered for the determination of the
705
            neighbors
706
            upperlimit: upper limit which determines the ICOHPs that are considered for the determination of the
707
            neighbors
708
            only_bonds_to: restricts the types of bonds that will be considered
709
            additional_condition: Additional condition for the evaluation
710
            perc_strength_ICOHP: will be used to determine how strong the ICOHPs (percentage*strongest ICOHP) will be
711
            that are still considered for the evalulation
712
            adapt_extremum_to_add_cond: will recalculate the limit based on the bonding type and not on the overall
713
            extremum
714

715
        Returns:
716
        """
717
        # get extremum
718
        if lowerlimit is None and upperlimit is None:
1✔
719
            lowerlimit, upperlimit = self._get_limit_from_extremum(
1✔
720
                self.Icohpcollection,
721
                percentage=perc_strength_ICOHP,
722
                adapt_extremum_to_add_cond=adapt_extremum_to_add_cond,
723
                additional_condition=additional_condition,
724
            )
725

726
        elif lowerlimit is None and upperlimit is not None:
1✔
727
            raise ValueError("Please give two limits or leave them both at None")
×
728
        elif upperlimit is None and lowerlimit is not None:
1✔
729
            raise ValueError("Please give two limits or leave them both at None")
×
730

731
        # find environments based on ICOHP values
732
        (
1✔
733
            list_icohps,
734
            list_keys,
735
            list_lengths,
736
            list_neighisite,
737
            list_neighsite,
738
            list_coords,
739
        ) = self._find_environments(additional_condition, lowerlimit, upperlimit, only_bonds_to)
740

741
        self.list_icohps = list_icohps
1✔
742
        self.list_lengths = list_lengths
1✔
743
        self.list_keys = list_keys
1✔
744
        self.list_neighsite = list_neighsite
1✔
745
        self.list_neighisite = list_neighisite
1✔
746
        self.list_coords = list_coords
1✔
747

748
        # make a structure graph
749
        # make sure everything is relative to the given Structure and not just the atoms in the unit cell
750
        if self.add_additional_data_sg:
1✔
751
            self.sg_list = [
1✔
752
                [
753
                    {
754
                        "site": neighbor,
755
                        "image": tuple(
756
                            int(round(i))
757
                            for i in (
758
                                neighbor.frac_coords
759
                                - self.structure[
760
                                    [
761
                                        isite
762
                                        for isite, site in enumerate(self.structure)
763
                                        if neighbor.is_periodic_image(site)
764
                                    ][0]
765
                                ].frac_coords
766
                            )
767
                        ),
768
                        "weight": 1,
769
                        # Here, the ICOBIs and ICOOPs are added based on the bond
770
                        # strength cutoff of the ICOHP
771
                        # more changes are necessary here if we use icobis for cutoffs
772
                        "edge_properties": {
773
                            "ICOHP": self.list_icohps[ineighbors][ineighbor],
774
                            "bond_length": self.list_lengths[ineighbors][ineighbor],
775
                            "bond_label": self.list_keys[ineighbors][ineighbor],
776
                            self.id_blist_sg1.upper(): self.bonding_list_1.icohpcollection.get_icohp_by_label(
777
                                self.list_keys[ineighbors][ineighbor]
778
                            ),
779
                            self.id_blist_sg2.upper(): self.bonding_list_2.icohpcollection.get_icohp_by_label(
780
                                self.list_keys[ineighbors][ineighbor]
781
                            ),
782
                        },
783
                        "site_index": [
784
                            isite for isite, site in enumerate(self.structure) if neighbor.is_periodic_image(site)
785
                        ][0],
786
                    }
787
                    for ineighbor, neighbor in enumerate(neighbors)
788
                ]
789
                for ineighbors, neighbors in enumerate(self.list_neighsite)
790
            ]
791
        else:
792
            self.sg_list = [
1✔
793
                [
794
                    {
795
                        "site": neighbor,
796
                        "image": tuple(
797
                            int(round(i))
798
                            for i in (
799
                                neighbor.frac_coords
800
                                - self.structure[
801
                                    [
802
                                        isite
803
                                        for isite, site in enumerate(self.structure)
804
                                        if neighbor.is_periodic_image(site)
805
                                    ][0]
806
                                ].frac_coords
807
                            )
808
                        ),
809
                        "weight": 1,
810
                        "edge_properties": {
811
                            "ICOHP": self.list_icohps[ineighbors][ineighbor],
812
                            "bond_length": self.list_lengths[ineighbors][ineighbor],
813
                            "bond_label": self.list_keys[ineighbors][ineighbor],
814
                        },
815
                        "site_index": [
816
                            isite for isite, site in enumerate(self.structure) if neighbor.is_periodic_image(site)
817
                        ][0],
818
                    }
819
                    for ineighbor, neighbor in enumerate(neighbors)
820
                ]
821
                for ineighbors, neighbors in enumerate(self.list_neighsite)
822
            ]
823

824
    def _find_environments(self, additional_condition, lowerlimit, upperlimit, only_bonds_to):
1✔
825
        """
826
        Will find all relevant neighbors based on certain restrictions
827

828
        Args:
829
            additional_condition (int): additional condition (see above)
830
            lowerlimit (float): lower limit that tells you which ICOHPs are considered
831
            upperlimit (float): upper limit that tells you which ICOHPs are considered
832
            only_bonds_to (list): list of str, e.g. ["O"] that will ensure that only bonds to "O" will be considered
833

834
        Returns:
835
        """
836
        # run over structure
837
        list_neighsite = []
1✔
838
        list_neighisite = []
1✔
839
        list_coords = []
1✔
840
        list_icohps = []
1✔
841
        list_lengths = []
1✔
842
        list_keys = []
1✔
843
        for isite in range(len(self.structure)):
1✔
844
            icohps = self._get_icohps(
1✔
845
                icohpcollection=self.Icohpcollection,
846
                isite=isite,
847
                lowerlimit=lowerlimit,
848
                upperlimit=upperlimit,
849
                only_bonds_to=only_bonds_to,
850
            )
851

852
            (
1✔
853
                keys_from_ICOHPs,
854
                lengths_from_ICOHPs,
855
                neighbors_from_ICOHPs,
856
                selected_ICOHPs,
857
            ) = self._find_relevant_atoms_additional_condition(isite, icohps, additional_condition)
858

859
            if len(neighbors_from_ICOHPs) > 0:
1✔
860
                centralsite = self.structure.sites[isite]
1✔
861

862
                neighbors_by_distance_start = self.structure.get_sites_in_sphere(
1✔
863
                    pt=centralsite.coords,
864
                    r=np.max(lengths_from_ICOHPs) + 0.5,
865
                    include_image=True,
866
                    include_index=True,
867
                )
868

869
                neighbors_by_distance = []
1✔
870
                list_distances = []
1✔
871
                index_here_list = []
1✔
872
                coords = []
1✔
873
                for neigh_new in sorted(neighbors_by_distance_start, key=lambda x: x[1]):
1✔
874
                    site_here = neigh_new[0].to_unit_cell()
1✔
875
                    index_here = neigh_new[2]
1✔
876
                    index_here_list.append(index_here)
1✔
877
                    cell_here = neigh_new[3]
1✔
878
                    newcoords = [
1✔
879
                        site_here.frac_coords[0] + float(cell_here[0]),
880
                        site_here.frac_coords[1] + float(cell_here[1]),
881
                        site_here.frac_coords[2] + float(cell_here[2]),
882
                    ]
883
                    coords.append(site_here.lattice.get_cartesian_coords(newcoords))
1✔
884

885
                    # new_site = PeriodicSite(species=site_here.species_string,
886
                    #                         coords=site_here.lattice.get_cartesian_coords(newcoords),
887
                    #                         lattice=site_here.lattice, to_unit_cell=False, coords_are_cartesian=True)
888
                    neighbors_by_distance.append(neigh_new[0])
1✔
889
                    list_distances.append(neigh_new[1])
1✔
890
                _list_neighsite = []
1✔
891
                _list_neighisite = []
1✔
892
                copied_neighbors_from_ICOHPs = copy.copy(neighbors_from_ICOHPs)
1✔
893
                copied_distances_from_ICOHPs = copy.copy(lengths_from_ICOHPs)
1✔
894
                _neigh_coords = []
1✔
895
                _neigh_frac_coords = []
1✔
896

897
                for ineigh, neigh in enumerate(neighbors_by_distance):
1✔
898
                    index_here2 = index_here_list[ineigh]
1✔
899

900
                    for idist, dist in enumerate(copied_distances_from_ICOHPs):
1✔
901
                        if (
1✔
902
                            np.isclose(dist, list_distances[ineigh], rtol=1e-4)
903
                            and copied_neighbors_from_ICOHPs[idist] == index_here2
904
                        ):
905
                            _list_neighsite.append(neigh)
1✔
906
                            _list_neighisite.append(index_here2)
1✔
907
                            _neigh_coords.append(coords[ineigh])
1✔
908
                            _neigh_frac_coords.append(neigh.frac_coords)
1✔
909
                            del copied_distances_from_ICOHPs[idist]
1✔
910
                            del copied_neighbors_from_ICOHPs[idist]
1✔
911
                            break
1✔
912

913
                list_neighisite.append(_list_neighisite)
1✔
914
                list_neighsite.append(_list_neighsite)
1✔
915
                list_lengths.append(lengths_from_ICOHPs)
1✔
916
                list_keys.append(keys_from_ICOHPs)
1✔
917
                list_coords.append(_neigh_coords)
1✔
918
                list_icohps.append(selected_ICOHPs)
1✔
919

920
            else:
921
                list_neighsite.append([])
1✔
922
                list_neighisite.append([])
1✔
923
                list_icohps.append([])
1✔
924
                list_lengths.append([])
1✔
925
                list_keys.append([])
1✔
926
                list_coords.append([])
1✔
927
        return (
1✔
928
            list_icohps,
929
            list_keys,
930
            list_lengths,
931
            list_neighisite,
932
            list_neighsite,
933
            list_coords,
934
        )
935

936
    def _find_relevant_atoms_additional_condition(self, isite, icohps, additional_condition):
1✔
937
        """
938
        Will find all relevant atoms that fulfill the additional_conditions
939

940
        Args:
941
            isite: number of site in structure (starts with 0)
942
            icohps: icohps
943
            additional_condition (int): additional condition
944

945
        Returns:
946
        """
947
        neighbors_from_ICOHPs = []
1✔
948
        lengths_from_ICOHPs = []
1✔
949
        icohps_from_ICOHPs = []
1✔
950
        keys_from_ICOHPs = []
1✔
951

952
        for key, icohp in icohps.items():
1✔
953
            atomnr1 = self._get_atomnumber(icohp._atom1)
1✔
954
            atomnr2 = self._get_atomnumber(icohp._atom2)
1✔
955

956
            # test additional conditions
957
            if additional_condition in (1, 3, 5, 6):
1✔
958
                val1 = self.valences[atomnr1]
1✔
959
                val2 = self.valences[atomnr2]
1✔
960

961
            if additional_condition == 0:
1✔
962
                # NO_ADDITIONAL_CONDITION
963
                if atomnr1 == isite:
1✔
964
                    neighbors_from_ICOHPs.append(atomnr2)
1✔
965
                    lengths_from_ICOHPs.append(icohp._length)
1✔
966
                    icohps_from_ICOHPs.append(icohp.summed_icohp)
1✔
967
                    keys_from_ICOHPs.append(key)
1✔
968
                elif atomnr2 == isite:
×
969
                    neighbors_from_ICOHPs.append(atomnr1)
×
970
                    lengths_from_ICOHPs.append(icohp._length)
×
971
                    icohps_from_ICOHPs.append(icohp.summed_icohp)
×
972
                    keys_from_ICOHPs.append(key)
×
973

974
            elif additional_condition == 1:
1✔
975
                # ONLY_ANION_CATION_BONDS
976
                if (val1 < 0.0 < val2) or (val2 < 0.0 < val1):
1✔
977
                    if atomnr1 == isite:
1✔
978
                        neighbors_from_ICOHPs.append(atomnr2)
1✔
979
                        lengths_from_ICOHPs.append(icohp._length)
1✔
980
                        icohps_from_ICOHPs.append(icohp.summed_icohp)
1✔
981
                        keys_from_ICOHPs.append(key)
1✔
982

983
                    elif atomnr2 == isite:
×
984
                        neighbors_from_ICOHPs.append(atomnr1)
×
985
                        lengths_from_ICOHPs.append(icohp._length)
×
986
                        icohps_from_ICOHPs.append(icohp.summed_icohp)
×
987
                        keys_from_ICOHPs.append(key)
×
988

989
            elif additional_condition == 2:
1✔
990
                # NO_ELEMENT_TO_SAME_ELEMENT_BONDS
991
                if icohp._atom1.rstrip("0123456789") != icohp._atom2.rstrip("0123456789"):
1✔
992
                    if atomnr1 == isite:
1✔
993
                        neighbors_from_ICOHPs.append(atomnr2)
1✔
994
                        lengths_from_ICOHPs.append(icohp._length)
1✔
995
                        icohps_from_ICOHPs.append(icohp.summed_icohp)
1✔
996
                        keys_from_ICOHPs.append(key)
1✔
997

998
                    elif atomnr2 == isite:
×
999
                        neighbors_from_ICOHPs.append(atomnr1)
×
1000
                        lengths_from_ICOHPs.append(icohp._length)
×
1001
                        icohps_from_ICOHPs.append(icohp.summed_icohp)
×
1002
                        keys_from_ICOHPs.append(key)
×
1003

1004
            elif additional_condition == 3:
1✔
1005
                # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3
1006
                if (val1 < 0.0 < val2) or (val2 < 0.0 < val1):
1✔
1007
                    if icohp._atom1.rstrip("0123456789") != icohp._atom2.rstrip("0123456789"):
1✔
1008
                        if atomnr1 == isite:
1✔
1009
                            neighbors_from_ICOHPs.append(atomnr2)
1✔
1010
                            lengths_from_ICOHPs.append(icohp._length)
1✔
1011
                            icohps_from_ICOHPs.append(icohp.summed_icohp)
1✔
1012
                            keys_from_ICOHPs.append(key)
1✔
1013

1014
                        elif atomnr2 == isite:
×
1015
                            neighbors_from_ICOHPs.append(atomnr1)
×
1016
                            lengths_from_ICOHPs.append(icohp._length)
×
1017
                            icohps_from_ICOHPs.append(icohp.summed_icohp)
×
1018
                            keys_from_ICOHPs.append(key)
×
1019

1020
            elif additional_condition == 4:
1✔
1021
                # ONLY_ELEMENT_TO_OXYGEN_BONDS = 4
1022
                if icohp._atom1.rstrip("0123456789") == "O" or icohp._atom2.rstrip("0123456789") == "O":
1✔
1023
                    if atomnr1 == isite:
1✔
1024
                        neighbors_from_ICOHPs.append(atomnr2)
1✔
1025
                        lengths_from_ICOHPs.append(icohp._length)
1✔
1026
                        icohps_from_ICOHPs.append(icohp.summed_icohp)
1✔
1027
                        keys_from_ICOHPs.append(key)
1✔
1028

1029
                    elif atomnr2 == isite:
×
1030
                        neighbors_from_ICOHPs.append(atomnr1)
×
1031
                        lengths_from_ICOHPs.append(icohp._length)
×
1032
                        icohps_from_ICOHPs.append(icohp.summed_icohp)
×
1033
                        keys_from_ICOHPs.append(key)
×
1034

1035
            elif additional_condition == 5:
1✔
1036
                # DO_NOT_CONSIDER_ANION_CATION_BONDS=5
1037
                if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0):
1✔
1038
                    if atomnr1 == isite:
1✔
1039
                        neighbors_from_ICOHPs.append(atomnr2)
1✔
1040
                        lengths_from_ICOHPs.append(icohp._length)
1✔
1041
                        icohps_from_ICOHPs.append(icohp.summed_icohp)
1✔
1042
                        keys_from_ICOHPs.append(key)
1✔
1043

1044
                    elif atomnr2 == isite:
×
1045
                        neighbors_from_ICOHPs.append(atomnr1)
×
1046
                        lengths_from_ICOHPs.append(icohp._length)
×
1047
                        icohps_from_ICOHPs.append(icohp.summed_icohp)
×
1048
                        keys_from_ICOHPs.append(key)
×
1049

1050
            elif additional_condition == 6:
1✔
1051
                # ONLY_CATION_CATION_BONDS=6
1052
                if val1 > 0.0 and val2 > 0.0:
1✔
1053
                    if atomnr1 == isite:
1✔
1054
                        neighbors_from_ICOHPs.append(atomnr2)
1✔
1055
                        lengths_from_ICOHPs.append(icohp._length)
1✔
1056
                        icohps_from_ICOHPs.append(icohp.summed_icohp)
1✔
1057
                        keys_from_ICOHPs.append(key)
1✔
1058

1059
                    elif atomnr2 == isite:
×
1060
                        neighbors_from_ICOHPs.append(atomnr1)
×
1061
                        lengths_from_ICOHPs.append(icohp._length)
×
1062
                        icohps_from_ICOHPs.append(icohp.summed_icohp)
×
1063
                        keys_from_ICOHPs.append(key)
×
1064

1065
        return (
1✔
1066
            keys_from_ICOHPs,
1067
            lengths_from_ICOHPs,
1068
            neighbors_from_ICOHPs,
1069
            icohps_from_ICOHPs,
1070
        )
1071

1072
    @staticmethod
1✔
1073
    def _get_icohps(icohpcollection, isite, lowerlimit, upperlimit, only_bonds_to):
1✔
1074
        """
1075
        Return icohp dict for certain site
1076

1077
        Args:
1078
            icohpcollection: Icohpcollection object
1079
            isite (int): number of a site
1080
            lowerlimit (float): lower limit that tells you which ICOHPs are considered
1081
            upperlimit (float): upper limit that tells you which ICOHPs are considered
1082
            only_bonds_to (list): list of str, e.g. ["O"] that will ensure that only bonds to "O" will be considered
1083

1084
        Returns:
1085
        """
1086
        icohps = icohpcollection.get_icohp_dict_of_site(
1✔
1087
            site=isite,
1088
            maxbondlength=6.0,
1089
            minsummedicohp=lowerlimit,
1090
            maxsummedicohp=upperlimit,
1091
            only_bonds_to=only_bonds_to,
1092
        )
1093
        return icohps
1✔
1094

1095
    @staticmethod
1✔
1096
    def _get_atomnumber(atomstring):
1✔
1097
        """
1098
        Return the number of the atom within the initial POSCAR (e.g., Return 0 for "Na1")
1099

1100
        Args:
1101
            atomstring: string such as "Na1"
1102

1103
        Returns: integer indicating the position in the POSCAR
1104
        """
1105
        return int(LobsterNeighbors._split_string(atomstring)[1]) - 1
1✔
1106

1107
    @staticmethod
1✔
1108
    def _split_string(s):
1✔
1109
        """
1110
        Will split strings such as "Na1" in "Na" and "1" and return "1"
1111

1112
        Args:
1113
            s (str): string
1114
        """
1115
        head = s.rstrip("0123456789")
1✔
1116
        tail = s[len(head) :]
1✔
1117
        return head, tail
1✔
1118

1119
    @staticmethod
1✔
1120
    def _determine_unit_cell(site):
1✔
1121
        """
1122
        Based on the site it will determine the unit cell, in which this site is based
1123

1124
        Args:
1125
            site: site object
1126
        """
1127
        unitcell = []
1✔
1128
        for coord in site.frac_coords:
1✔
1129
            value = math.floor(round(coord, 4))
1✔
1130
            unitcell.append(value)
1✔
1131

1132
        return unitcell
1✔
1133

1134
    def _get_limit_from_extremum(
1✔
1135
        self,
1136
        icohpcollection,
1137
        percentage=0.15,
1138
        adapt_extremum_to_add_cond=False,
1139
        additional_condition=0,
1140
    ):
1141
        # TODO: adapt this to give the extremum for the correct type of bond
1142
        # TODO add tests
1143
        """
1144
        Return limits for the evaluation of the icohp values from an icohpcollection
1145
        Return -100000, min(max_icohp*0.15,-0.1)
1146

1147
        Args:
1148
            icohpcollection: icohpcollection object
1149
            percentage: will determine which ICOHPs will be considered (only 0.15 from the maximum value)
1150
            adapt_extremum_to_add_cond: should the extrumum be adapted to the additional condition
1151
            additional_condition: additional condition to determine which bonds are relevant
1152
        Returns: [-100000, min(max_icohp*0.15,-0.1)]
1153
        """
1154
        # TODO: make it work for COOPs
1155
        if not adapt_extremum_to_add_cond:
1✔
1156
            extremum_based = icohpcollection.extremum_icohpvalue(summed_spin_channels=True) * percentage
1✔
1157
        else:
1158
            if additional_condition == 0:
1✔
1159
                extremum_based = icohpcollection.extremum_icohpvalue(summed_spin_channels=True) * percentage
1✔
1160
            elif additional_condition == 1:
1✔
1161
                # only cation anion bonds
1162
                list_icohps = []
1✔
1163
                for value in icohpcollection._icohplist.values():
1✔
1164
                    atomnr1 = LobsterNeighbors._get_atomnumber(value._atom1)
1✔
1165
                    atomnr2 = LobsterNeighbors._get_atomnumber(value._atom2)
1✔
1166

1167
                    val1 = self.valences[atomnr1]
1✔
1168
                    val2 = self.valences[atomnr2]
1✔
1169
                    if (val1 < 0.0 < val2) or (val2 < 0.0 < val1):
1✔
1170
                        list_icohps.append(value.summed_icohp)
1✔
1171

1172
                extremum_based = min(list_icohps) * percentage
1✔
1173

1174
            elif additional_condition == 2:
1✔
1175
                # NO_ELEMENT_TO_SAME_ELEMENT_BONDS
1176
                list_icohps = []
1✔
1177
                for value in icohpcollection._icohplist.values():
1✔
1178
                    if value._atom1.rstrip("0123456789") != value._atom2.rstrip("0123456789"):
1✔
1179
                        list_icohps.append(value.summed_icohp)
1✔
1180
                extremum_based = min(list_icohps) * percentage
1✔
1181

1182
            elif additional_condition == 3:
1✔
1183
                # ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3
1184
                list_icohps = []
1✔
1185
                for value in icohpcollection._icohplist.values():
1✔
1186
                    atomnr1 = LobsterNeighbors._get_atomnumber(value._atom1)
1✔
1187
                    atomnr2 = LobsterNeighbors._get_atomnumber(value._atom2)
1✔
1188
                    val1 = self.valences[atomnr1]
1✔
1189
                    val2 = self.valences[atomnr2]
1✔
1190

1191
                    if (val1 < 0.0 < val2) or (val2 < 0.0 < val1):
1✔
1192
                        if value._atom1.rstrip("0123456789") != value._atom2.rstrip("0123456789"):
1✔
1193
                            list_icohps.append(value.summed_icohp)
1✔
1194
                extremum_based = min(list_icohps) * percentage
1✔
1195
            elif additional_condition == 4:
1✔
1196
                list_icohps = []
1✔
1197
                for value in icohpcollection._icohplist.values():
1✔
1198
                    if value._atom1.rstrip("0123456789") == "O" or value._atom2.rstrip("0123456789") == "O":
1✔
1199
                        list_icohps.append(value.summed_icohp)
1✔
1200
                extremum_based = min(list_icohps) * percentage
1✔
1201
            elif additional_condition == 5:
1✔
1202
                # DO_NOT_CONSIDER_ANION_CATION_BONDS=5
1203
                list_icohps = []
1✔
1204
                for value in icohpcollection._icohplist.values():
1✔
1205
                    atomnr1 = LobsterNeighbors._get_atomnumber(value._atom1)
1✔
1206
                    atomnr2 = LobsterNeighbors._get_atomnumber(value._atom2)
1✔
1207
                    val1 = self.valences[atomnr1]
1✔
1208
                    val2 = self.valences[atomnr2]
1✔
1209

1210
                    if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0):
1✔
1211
                        list_icohps.append(value.summed_icohp)
1✔
1212
                extremum_based = min(list_icohps) * percentage
1✔
1213

1214
            elif additional_condition == 6:
1✔
1215
                # ONLY_CATION_CATION_BONDS=6
1216
                list_icohps = []
1✔
1217
                for value in icohpcollection._icohplist.values():
1✔
1218
                    atomnr1 = LobsterNeighbors._get_atomnumber(value._atom1)
1✔
1219
                    atomnr2 = LobsterNeighbors._get_atomnumber(value._atom2)
1✔
1220
                    val1 = self.valences[atomnr1]
1✔
1221
                    val2 = self.valences[atomnr2]
1✔
1222

1223
                    if val1 > 0.0 and val2 > 0.0:
1✔
1224
                        list_icohps.append(value.summed_icohp)
1✔
1225
                extremum_based = min(list_icohps) * percentage
1✔
1226

1227
        # if not self.are_coops:
1228
        max_here = min(extremum_based, -0.1)
1✔
1229
        return -100000, max_here
1✔
1230
        # else:
1231
        #    return extremum_based, 100000
1232

1233

1234
class LobsterLightStructureEnvironments(LightStructureEnvironments):
1✔
1235
    """
1236
    Class to store LightStructureEnvironments based on Lobster outputs
1237
    """
1238

1239
    @classmethod
1✔
1240
    def from_Lobster(
1✔
1241
        cls,
1242
        list_ce_symbol,
1243
        list_csm,
1244
        list_permutation,
1245
        list_neighsite,
1246
        list_neighisite,
1247
        structure: Structure,
1248
        valences=None,
1249
    ):
1250
        """
1251
        Will set up a LightStructureEnvironments from Lobster
1252

1253
        Args:
1254
            structure: Structure object
1255
            list_ce_symbol: list of symbols for coordination environments
1256
            list_csm: list of continuous symmetry measures
1257
            list_permutation: list of permutations
1258
            list_neighsite: list of neighboring sites
1259
            list_neighisite: list of neighboring isites (number of a site)
1260
            valences: list of valences
1261

1262
        Returns: LobsterLightStructureEnvironments
1263
        """
1264
        strategy = None
1✔
1265
        valences = valences
1✔
1266
        valences_origin = "user-defined"
1✔
1267
        structure = structure
1✔
1268

1269
        coordination_environments = []
1✔
1270

1271
        all_nbs_sites = []
1✔
1272
        all_nbs_sites_indices = []
1✔
1273
        neighbors_sets = []
1✔
1274
        counter = 0
1✔
1275
        for isite, _site in enumerate(structure):
1✔
1276
            # all_nbs_sites_here=[]
1277
            all_nbs_sites_indices_here = []
1✔
1278
            # Coordination environment
1279
            if list_ce_symbol is not None:
1✔
1280
                ce_dict = {
1✔
1281
                    "ce_symbol": list_ce_symbol[isite],
1282
                    "ce_fraction": 1.0,
1283
                    "csm": list_csm[isite],
1284
                    "permutation": list_permutation[isite],
1285
                }
1286
            else:
1287
                ce_dict = None
×
1288

1289
            if list_neighisite[isite] is not None:
1✔
1290
                for ineighsite, neighsite in enumerate(list_neighsite[isite]):
1✔
1291
                    diff = neighsite.frac_coords - structure[list_neighisite[isite][ineighsite]].frac_coords
1✔
1292
                    rounddiff = np.round(diff)
1✔
1293
                    if not np.allclose(diff, rounddiff):
1✔
1294
                        raise ValueError(
×
1295
                            "Weird, differences between one site in a periodic image cell is not integer ..."
1296
                        )
1297
                    nb_image_cell = np.array(rounddiff, int)
1✔
1298

1299
                    all_nbs_sites_indices_here.append(counter)
1✔
1300

1301
                    all_nbs_sites.append(
1✔
1302
                        {
1303
                            "site": neighsite,
1304
                            "index": list_neighisite[isite][ineighsite],
1305
                            "image_cell": nb_image_cell,
1306
                        }
1307
                    )
1308
                    counter = counter + 1
1✔
1309

1310
                all_nbs_sites_indices.append(all_nbs_sites_indices_here)
1✔
1311
            else:
1312
                all_nbs_sites.append({"site": None, "index": None, "image_cell": None})  # all_nbs_sites_here)
×
1313
                all_nbs_sites_indices.append([])  # all_nbs_sites_indices_here)
×
1314

1315
            if list_neighisite[isite] is not None:
1✔
1316
                nb_set = cls.NeighborsSet(
1✔
1317
                    structure=structure,
1318
                    isite=isite,
1319
                    all_nbs_sites=all_nbs_sites,
1320
                    all_nbs_sites_indices=all_nbs_sites_indices[isite],
1321
                )
1322

1323
            else:
1324
                nb_set = cls.NeighborsSet(
×
1325
                    structure=structure,
1326
                    isite=isite,
1327
                    all_nbs_sites=[],
1328
                    all_nbs_sites_indices=[],
1329
                )
1330

1331
            coordination_environments.append([ce_dict])
1✔
1332
            neighbors_sets.append([nb_set])
1✔
1333

1334
        return cls(
1✔
1335
            strategy=strategy,
1336
            coordination_environments=coordination_environments,
1337
            all_nbs_sites=all_nbs_sites,
1338
            neighbors_sets=neighbors_sets,
1339
            structure=structure,
1340
            valences=valences,
1341
            valences_origin=valences_origin,
1342
        )
1343

1344
    @property
1✔
1345
    def uniquely_determines_coordination_environments(self):
1✔
1346
        """
1347
        True if the coordination environments are uniquely determined.
1348
        """
1349
        return True
1✔
1350

1351
    def as_dict(self):
1✔
1352
        """
1353
        Bson-serializable dict representation of the LightStructureEnvironments object.
1354
        :return: Bson-serializable dict representation of the LightStructureEnvironments object.
1355
        """
1356
        return {
1✔
1357
            "@module": type(self).__module__,
1358
            "@class": type(self).__name__,
1359
            "strategy": self.strategy,
1360
            "structure": self.structure.as_dict(),
1361
            "coordination_environments": self.coordination_environments,
1362
            "all_nbs_sites": [
1363
                {
1364
                    "site": nb_site["site"].as_dict(),
1365
                    "index": nb_site["index"],
1366
                    "image_cell": [int(ii) for ii in nb_site["image_cell"]],
1367
                }
1368
                for nb_site in self._all_nbs_sites
1369
            ],
1370
            "neighbors_sets": [
1371
                [nb_set.as_dict() for nb_set in site_nb_sets] if site_nb_sets is not None else None
1372
                for site_nb_sets in self.neighbors_sets
1373
            ],
1374
            "valences": self.valences,
1375
        }
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