Skip to content

Commit 5bdc1ca

Browse files
authored
Extend lobsterenv for coop/cobi (#3050)
* add method to adapt extremum values
1 parent d4c3cc5 commit 5bdc1ca

File tree

7 files changed

+157
-42
lines changed

7 files changed

+157
-42
lines changed

pymatgen/io/lobster/lobsterenv.py

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,13 @@ def __init__(
5858
structure: Structure,
5959
filename_ICOHP: str = "ICOHPLIST.lobster",
6060
are_coops: bool = False,
61+
are_cobis: bool = False,
6162
valences: list[int | float] | None = None,
6263
limits: tuple[float, float] | None = None,
6364
additional_condition: int = 0,
6465
only_bonds_to: list[str] | None = None,
6566
perc_strength_ICOHP: float = 0.15,
67+
noise_cutoff: float = 0.1,
6668
valences_from_charges: bool = False,
6769
filename_CHARGE: str | None = None,
6870
which_charge: str = "Mulliken",
@@ -76,12 +78,13 @@ def __init__(
7678
"""
7779
7880
Args:
79-
filename_ICOHP: (str) Path to ICOHPLIST.lobster
80-
structure: (Structure): typically constructed by Structure.from_file("POSCAR")
81+
filename_ICOHP: (str) Path to ICOHPLIST.lobster or ICOOPLIST.lobster or ICOBILIST.lobster
82+
structure: (Structure) typically constructed by Structure.from_file("POSCAR")
8183
are_coops: (bool) if True, the file is a ICOOPLIST.lobster and not a ICOHPLIST.lobster; only tested for
8284
ICOHPLIST.lobster so far
85+
are_cobis: (bool) if True, the file is a ICOBILIST.lobster and not a ICOHPLIST.lobster
8386
valences: (list[int | float]): gives valence/charge for each element
84-
limits (tuple[float, float] | None): limit to decide which ICOHPs should be considered
87+
limits (tuple[float, float] | None): limit to decide which ICOHPs (ICOOP or ICOBI) should be considered
8588
additional_condition (int): Additional condition that decides which kind of bonds will be considered
8689
NO_ADDITIONAL_CONDITION = 0
8790
ONLY_ANION_CATION_BONDS = 1
@@ -93,7 +96,8 @@ def __init__(
9396
only_bonds_to: (list[str]) will only consider bonds to certain elements (e.g. ["O"] for oxygen)
9497
perc_strength_ICOHP: if no limits are given, this will decide which icohps will still be considered (
9598
relative to
96-
the strongest ICOHP)
99+
the strongest ICOHP (ICOOP or ICOBI)
100+
noise_cutoff: if provided hardcodes the lower limit of icohps considered
97101
valences_from_charges: if True and path to CHARGE.lobster is provided, will use Lobster charges (
98102
Mulliken) instead of valences
99103
filename_CHARGE: (str) Path to Charge.lobster
@@ -108,16 +112,18 @@ def __init__(
108112
id_blist_sg2: (str) Identity of data in filename_blist_sg2,
109113
e.g., "icoop" or "icobi".
110114
"""
111-
self.ICOHP = Icohplist(are_coops=are_coops, filename=filename_ICOHP)
115+
self.ICOHP = Icohplist(are_coops=are_coops, are_cobis=are_cobis, filename=filename_ICOHP)
112116
self.Icohpcollection = self.ICOHP.icohpcollection
113117
self.structure = structure
114118
self.limits = limits
115119
self.only_bonds_to = only_bonds_to
116120
self.adapt_extremum_to_add_cond = adapt_extremum_to_add_cond
117121
self.are_coops = are_coops
122+
self.are_cobis = are_cobis
118123
self.add_additional_data_sg = add_additional_data_sg
119124
self.filename_blist_sg1 = filename_blist_sg1
120125
self.filename_blist_sg2 = filename_blist_sg2
126+
self.noise_cutoff = noise_cutoff
121127

122128
allowed_arguments = ["icoop", "icobi"]
123129
if id_blist_sg1.lower() not in allowed_arguments or id_blist_sg2.lower() not in allowed_arguments:
@@ -152,9 +158,6 @@ def __init__(
152158
are_cobis=are_cobis_id2,
153159
)
154160

155-
if are_coops:
156-
raise ValueError("Algorithm only works correctly for ICOHPLIST.lobster")
157-
158161
# will check if the additional condition is correctly delivered
159162
if additional_condition not in range(7):
160163
raise ValueError(f"Unexpected {additional_condition=}, must be one of {list(range(7))}")
@@ -364,12 +367,12 @@ def get_info_icohps_to_neighbors(self, isites=None, onlycation_isites=True):
364367
"""
365368
This method returns information on the icohps of neighbors for certain sites as identified by their site id.
366369
This is useful for plotting the relevant cohps of a site in the structure.
370+
(could be ICOOPLIST.lobster or ICOHPLIST.lobster or ICOBILIST.lobster)
367371
368372
Args:
369373
isites: list of site ids. If isite==None, all isites will be used to add the icohps of the neighbors
370374
onlycation_isites: if True and if isite==None, it will only analyse the sites of the cations
371375
372-
373376
Returns: ICOHPNeighborsInfo
374377
"""
375378
if self.valences is None and onlycation_isites:
@@ -415,7 +418,8 @@ def plot_cohps_of_neighbors(
415418
integrated=False,
416419
):
417420
"""
418-
Will plot summed cohps (please be careful in the spin polarized case (plots might overlap (exactly!)).
421+
Will plot summed cohps or cobis or coops
422+
(please be careful in the spin polarized case (plots might overlap (exactly!)).
419423
420424
Args:
421425
isites: list of site ids, if isite==[], all isites will be used to add the icohps of the neighbors
@@ -429,12 +433,12 @@ def plot_cohps_of_neighbors(
429433
integrated: bool, if true will show integrated cohp instead of cohp
430434
431435
Returns:
432-
plt of the cohps
436+
plt of the cohps or coops or cobis
433437
"""
434438
# include COHPPlotter and plot a sum of these COHPs
435439
# might include option to add Spin channels
436440
# implement only_bonds_to
437-
cp = CohpPlotter()
441+
cp = CohpPlotter(are_cobis=self.are_cobis, are_coops=self.are_coops)
438442

439443
plotlabel, summed_cohp = self.get_info_cohps_to_neighbors(
440444
path_to_COHPCAR,
@@ -465,19 +469,19 @@ def get_info_cohps_to_neighbors(
465469
summed_spin_channels=False,
466470
):
467471
"""
468-
Return info about the cohps as a summed cohp object and a label
472+
Return info about the cohps (coops or cobis) as a summed cohp object and a label
469473
from all sites mentioned in isites with neighbors.
470474
471475
Args:
472-
path_to_COHPCAR: str, path to COHPCAR
476+
path_to_COHPCAR: str, path to COHPCAR or COOPCAR or COBICAR
473477
isites: list of int that indicate the number of the site
474478
only_bonds_to: list of str, e.g. ["O"] to only show cohps of anything to oxygen
475479
onlycation_isites: if isites=None, only cation sites will be returned
476480
per_bond: will normalize per bond
477481
summed_spin_channels: will sum all spin channels
478482
479-
Returns: label for cohp (str), CompleteCohp object which describes all cohps of the sites as given by isites
480-
and the other parameters
483+
Returns: label for cohp (str), CompleteCohp object which describes all cohps (coops or cobis) of the sites
484+
as given by isites and the other parameters
481485
"""
482486
# TODO: add options for orbital-resolved cohps
483487
(
@@ -497,7 +501,13 @@ def get_info_cohps_to_neighbors(
497501
self.structure.to(filename=path, fmt="POSCAR")
498502

499503
if not hasattr(self, "completecohp"):
500-
self.completecohp = CompleteCohp.from_file(fmt="LOBSTER", filename=path_to_COHPCAR, structure_file=path)
504+
self.completecohp = CompleteCohp.from_file(
505+
fmt="LOBSTER",
506+
filename=path_to_COHPCAR,
507+
structure_file=path,
508+
are_coops=self.are_coops,
509+
are_cobis=self.are_cobis,
510+
)
501511

502512
# will check that the number of bonds in ICOHPLIST and COHPCAR are identical
503513
# further checks could be implemented
@@ -1117,6 +1127,19 @@ def _determine_unit_cell(site):
11171127

11181128
return unitcell
11191129

1130+
def _adapt_extremum_to_add_cond(self, list_icohps, percentage):
1131+
"""
1132+
Convinicence method for returning the extremum of the given icohps or icoops or icobis list
1133+
1134+
Args:
1135+
list_icohps: can be a list of icohps or icobis or icobis
1136+
1137+
Returns: min value of input list of icohps / max value of input list of icobis or icobis
1138+
"""
1139+
1140+
which_extr = min if not self.are_coops and not self.are_cobis else max
1141+
return which_extr(list_icohps) * percentage
1142+
11201143
def _get_limit_from_extremum(
11211144
self,
11221145
icohpcollection,
@@ -1130,12 +1153,14 @@ def _get_limit_from_extremum(
11301153
11311154
Args:
11321155
icohpcollection: icohpcollection object
1133-
percentage: will determine which ICOHPs will be considered (only 0.15 from the maximum value)
1156+
percentage: will determine which ICOHPs or ICOOP or ICOBI will be considered
1157+
(only 0.15 from the maximum value)
11341158
adapt_extremum_to_add_cond: should the extrumum be adapted to the additional condition
11351159
additional_condition: additional condition to determine which bonds are relevant
1136-
Returns: [-float('inf'), min(max_icohp*0.15,-0.1)]
1160+
1161+
Returns: [-inf, min(strongest_icohp*0.15,-noise_cutoff)] / [max(strongest_icohp*0.15, noise_cutoff),inf]
11371162
"""
1138-
# TODO: make it work for COOPs/COBIs
1163+
11391164
if not adapt_extremum_to_add_cond or additional_condition == 0:
11401165
extremum_based = icohpcollection.extremum_icohpvalue(summed_spin_channels=True) * percentage
11411166
elif additional_condition == 1:
@@ -1150,15 +1175,16 @@ def _get_limit_from_extremum(
11501175
if (val1 < 0.0 < val2) or (val2 < 0.0 < val1):
11511176
list_icohps.append(value.summed_icohp)
11521177

1153-
extremum_based = min(list_icohps) * percentage
1178+
extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage)
11541179

11551180
elif additional_condition == 2:
11561181
# NO_ELEMENT_TO_SAME_ELEMENT_BONDS
11571182
list_icohps = []
11581183
for value in icohpcollection._icohplist.values():
11591184
if value._atom1.rstrip("0123456789") != value._atom2.rstrip("0123456789"):
11601185
list_icohps.append(value.summed_icohp)
1161-
extremum_based = min(list_icohps) * percentage
1186+
1187+
extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage)
11621188

11631189
elif additional_condition == 3:
11641190
# ONLY_ANION_CATION_BONDS_AND_NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 3
@@ -1175,13 +1201,17 @@ def _get_limit_from_extremum(
11751201
and value._atom1.rstrip("0123456789") != value._atom2.rstrip("0123456789")
11761202
):
11771203
list_icohps.append(value.summed_icohp)
1178-
extremum_based = min(list_icohps) * percentage
1204+
1205+
extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage)
1206+
11791207
elif additional_condition == 4:
11801208
list_icohps = []
11811209
for value in icohpcollection._icohplist.values():
11821210
if value._atom1.rstrip("0123456789") == "O" or value._atom2.rstrip("0123456789") == "O":
11831211
list_icohps.append(value.summed_icohp)
1184-
extremum_based = min(list_icohps) * percentage
1212+
1213+
extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage)
1214+
11851215
elif additional_condition == 5:
11861216
# DO_NOT_CONSIDER_ANION_CATION_BONDS=5
11871217
list_icohps = []
@@ -1193,7 +1223,8 @@ def _get_limit_from_extremum(
11931223

11941224
if (val1 > 0.0 and val2 > 0.0) or (val1 < 0.0 and val2 < 0.0):
11951225
list_icohps.append(value.summed_icohp)
1196-
extremum_based = min(list_icohps) * percentage
1226+
1227+
extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage)
11971228

11981229
elif additional_condition == 6:
11991230
# ONLY_CATION_CATION_BONDS=6
@@ -1206,13 +1237,17 @@ def _get_limit_from_extremum(
12061237

12071238
if val1 > 0.0 and val2 > 0.0:
12081239
list_icohps.append(value.summed_icohp)
1209-
extremum_based = min(list_icohps) * percentage
12101240

1211-
# if not self.are_coops:
1212-
max_here = min(extremum_based, -0.1)
1213-
return -float("inf"), max_here
1214-
# else:
1215-
# return extremum_based, 100000
1241+
extremum_based = self._adapt_extremum_to_add_cond(list_icohps, percentage)
1242+
1243+
if not self.are_coops and not self.are_cobis:
1244+
max_here = min(extremum_based, -self.noise_cutoff) if self.noise_cutoff is not None else extremum_based
1245+
return -float("inf"), max_here
1246+
if self.are_coops or self.are_cobis:
1247+
min_here = max(extremum_based, self.noise_cutoff) if self.noise_cutoff is not None else extremum_based
1248+
return min_here, float("inf")
1249+
1250+
return None
12161251

12171252

12181253
class LobsterLightStructureEnvironments(LightStructureEnvironments):

pymatgen/io/lobster/tests/test_lobsterenv.py

Lines changed: 91 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,29 @@ def setUp(self):
125125
structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")),
126126
additional_condition=6,
127127
)
128+
# coop / cobi
129+
self.chemenvlobster1_coop_NaCl = LobsterNeighbors(
130+
are_coops=True,
131+
filename_ICOHP=os.path.join(test_dir_env, "ICOOPLIST.lobster.NaCl.gz"),
132+
structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")),
133+
additional_condition=1,
134+
noise_cutoff=None,
135+
)
136+
137+
self.chemenvlobster1_cobi_NaCl = LobsterNeighbors(
138+
are_coops=True,
139+
filename_ICOHP=os.path.join(test_dir_env, "ICOBILIST.lobster.NaCl.gz"),
140+
structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")),
141+
additional_condition=1,
142+
noise_cutoff=None,
143+
)
144+
145+
self.chemenvlobster1_cobi_mp470 = LobsterNeighbors(
146+
are_coops=True,
147+
filename_ICOHP=os.path.join(test_dir_env, "ICOBILIST.lobster.mp_470.gz"),
148+
structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_470.gz")),
149+
additional_condition=1,
150+
)
128151

129152
# TODO: use charge instead of valence
130153
self.chemenvlobster1_charges = LobsterNeighbors(
@@ -135,6 +158,26 @@ def setUp(self):
135158
filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"),
136159
additional_condition=1,
137160
)
161+
self.chemenvlobster1_charges_noisecutoff = LobsterNeighbors(
162+
are_coops=False,
163+
filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_632319.gz"),
164+
structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_632319.gz")),
165+
valences_from_charges=True,
166+
filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp_632319.gz"),
167+
additional_condition=1,
168+
perc_strength_ICOHP=0.05,
169+
noise_cutoff=0.1,
170+
)
171+
self.chemenvlobster1_charges_wo_noisecutoff = LobsterNeighbors(
172+
are_coops=False,
173+
filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_632319.gz"),
174+
structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_632319.gz")),
175+
valences_from_charges=True,
176+
filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp_632319.gz"),
177+
additional_condition=1,
178+
perc_strength_ICOHP=0.05,
179+
noise_cutoff=None,
180+
)
138181
self.chemenvlobster1_charges_loewdin = LobsterNeighbors(
139182
are_coops=False,
140183
filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"),
@@ -218,17 +261,6 @@ def setUp(self):
218261
adapt_extremum_to_add_cond=True,
219262
)
220263

221-
def test_use_of_coop(self):
222-
with pytest.raises(ValueError, match="Algorithm only works correctly for ICOHPLIST.lobster"):
223-
_ = LobsterNeighbors(
224-
are_coops=True,
225-
filename_ICOHP=os.path.join(test_dir_env, "ICOHPLIST.lobster.mp_353.gz"),
226-
structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_353.gz")),
227-
valences_from_charges=True,
228-
filename_CHARGE=os.path.join(test_dir_env, "CHARGE.lobster.mp-353.gz"),
229-
additional_condition=1,
230-
)
231-
232264
def test_cation_anion_mode_without_ions(self):
233265
with pytest.raises(
234266
ValueError, match="Valences cannot be assigned, additional_conditions 1, 3, 5 and 6 will not work"
@@ -332,6 +364,24 @@ def test_get_nn_info(self):
332364
)
333365
== 2
334366
)
367+
assert (
368+
len(
369+
self.chemenvlobster1_charges_noisecutoff.get_nn(
370+
structure=self.chemenvlobster1_charges_noisecutoff.structure,
371+
n=1,
372+
)
373+
)
374+
== 0
375+
)
376+
assert (
377+
len(
378+
self.chemenvlobster1_charges_wo_noisecutoff.get_nn(
379+
structure=self.chemenvlobster1_charges_wo_noisecutoff.structure,
380+
n=1,
381+
)
382+
)
383+
== 8
384+
)
335385
# NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 2
336386
assert (
337387
len(
@@ -453,6 +503,36 @@ def test_get_nn_info(self):
453503
== 2
454504
)
455505

506+
assert (
507+
len(
508+
self.chemenvlobster1_coop_NaCl.get_nn(
509+
structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")),
510+
n=0,
511+
)
512+
)
513+
== 6
514+
)
515+
516+
assert (
517+
len(
518+
self.chemenvlobster1_cobi_NaCl.get_nn(
519+
structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.NaCl.gz")),
520+
n=0,
521+
)
522+
)
523+
== 6
524+
)
525+
526+
assert (
527+
len(
528+
self.chemenvlobster1_cobi_mp470.get_nn(
529+
structure=Structure.from_file(os.path.join(test_dir_env, "POSCAR.mp_470.gz")),
530+
n=3,
531+
)
532+
)
533+
== 3
534+
)
535+
456536
# NO_ELEMENT_TO_SAME_ELEMENT_BONDS = 2
457537
assert (
458538
len(
164 Bytes
Binary file not shown.
89.5 KB
Binary file not shown.
5.91 KB
Binary file not shown.
151 Bytes
Binary file not shown.
106 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)