-
Notifications
You must be signed in to change notification settings - Fork 855
/
volume_predictor.py
260 lines (217 loc) · 9.95 KB
/
volume_predictor.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# coding: utf-8
# Copyright (c) Pymatgen Development Team.
# Distributed under the terms of the MIT License.
"""
Predict volumes of crystal structures.
"""
import warnings
import os
import numpy as np
from monty.serialization import loadfn
from pymatgen.analysis.bond_valence import BVAnalyzer
from pymatgen.analysis.structure_matcher import StructureMatcher
from pymatgen.core import Structure
MODULE_DIR = os.path.dirname(os.path.abspath(__file__))
bond_params = loadfn(os.path.join(MODULE_DIR, 'DLS_bond_params.yaml'))
def _is_ox(structure):
comp = structure.composition
for k in comp.keys():
try:
k.oxi_state
except AttributeError:
return False
return True
class RLSVolumePredictor:
"""
Reference lattice scaling (RLS) scheme that predicts the volume of a
structure based on a known crystal structure.
"""
def __init__(self, check_isostructural=True, radii_type="ionic-atomic",
use_bv=True):
"""
Args:
check_isostructural: Whether to test that the two structures are
isostructural. This algo works best for isostructural compounds.
Defaults to True.
radii_type (str): Types of radii to use. You can specify "ionic"
(only uses ionic radii), "atomic" (only uses atomic radii) or
"ionic-atomic" (uses either ionic or atomic radii, with a
preference for ionic where possible).
use_bv (bool): Whether to use BVAnalyzer to determine oxidation
states if not present.
"""
self.check_isostructural = check_isostructural
self.radii_type = radii_type
self.use_bv = use_bv
def predict(self, structure, ref_structure):
"""
Given a structure, returns the predicted volume.
Args:
structure (Structure): structure w/unknown volume
ref_structure (Structure): A reference structure with a similar
structure but different species.
Returns:
a float value of the predicted volume
"""
if self.check_isostructural:
m = StructureMatcher()
mapping = m.get_best_electronegativity_anonymous_mapping(
structure, ref_structure)
if mapping is None:
raise ValueError("Input structures do not match!")
if "ionic" in self.radii_type:
try:
# Use BV analyzer to determine oxidation states only if the
# oxidation states are not already specified in the structure
# and use_bv is true.
if (not _is_ox(structure)) and self.use_bv:
a = BVAnalyzer()
structure = a.get_oxi_state_decorated_structure(structure)
if (not _is_ox(ref_structure)) and self.use_bv:
a = BVAnalyzer()
ref_structure = a.get_oxi_state_decorated_structure(
ref_structure)
comp = structure.composition
ref_comp = ref_structure.composition
# Check if all the associated ionic radii are available.
if any([k.ionic_radius is None for k in list(comp.keys())]) or \
any([k.ionic_radius is None for k in
list(ref_comp.keys())]):
raise ValueError("Not all the ionic radii are available!")
numerator = 0
denominator = 0
# Here, the 1/3 factor on the composition accounts for atomic
# packing. We want the number per unit length.
for k, v in comp.items():
numerator += k.ionic_radius * v ** (1 / 3)
for k, v in ref_comp.items():
denominator += k.ionic_radius * v ** (1 / 3)
return ref_structure.volume * (numerator / denominator) ** 3
except Exception:
warnings.warn("Exception occured. Will attempt atomic radii.")
# If error occurs during use of ionic radii scheme, pass
# and see if we can resolve it using atomic radii.
pass
if "atomic" in self.radii_type:
comp = structure.composition
ref_comp = ref_structure.composition
# Here, the 1/3 factor on the composition accounts for atomic
# packing. We want the number per unit length.
numerator = 0
denominator = 0
for k, v in comp.items():
numerator += k.atomic_radius * v ** (1 / 3)
for k, v in ref_comp.items():
denominator += k.atomic_radius * v ** (1 / 3)
return ref_structure.volume * (numerator / denominator) ** 3
raise ValueError("Cannot find volume scaling based on radii choices "
"specified!")
def get_predicted_structure(self, structure, ref_structure):
"""
Given a structure, returns back the structure scaled to predicted
volume.
Args:
structure (Structure): structure w/unknown volume
ref_structure (Structure): A reference structure with a similar
structure but different species.
Returns:
a Structure object with predicted volume
"""
new_structure = structure.copy()
new_structure.scale_lattice(self.predict(structure, ref_structure))
return new_structure
class DLSVolumePredictor:
"""
Data-mined lattice scaling (DLS) scheme that relies on data-mined bond
lengths to predict the crystal volume of a given structure.
As of 2/12/19, we suggest this method be used in conjunction with
min_scaling and max_scaling to prevent instances of very large, unphysical
predicted volumes found in a small subset of structures.
"""
def __init__(self, cutoff=4.0, min_scaling=0.5, max_scaling=1.5):
"""
Args:
cutoff (float): cutoff radius added to site radius for finding
site pairs. Necessary to increase only if your initial
structure guess is extremely bad (atoms way too far apart). In
all other instances, increasing cutoff gives same answer
but takes more time.
min_scaling (float): if not None, this will ensure that the new
volume is at least this fraction of the original (preventing
too-small volumes)
max_scaling (float): if not None, this will ensure that the new
volume is at most this fraction of the original (preventing
too-large volumes)
"""
self.cutoff = cutoff
self.min_scaling = min_scaling
self.max_scaling = max_scaling
def predict(self, structure, icsd_vol=False):
"""
Given a structure, returns the predicted volume.
Args:
structure (Structure) : a crystal structure with an unknown volume.
icsd_vol (bool) : True if the input structure's volume comes from
ICSD.
Returns:
a float value of the predicted volume.
"""
# Get standard deviation of electronnegativity in the structure.
std_x = np.std([site.specie.X for site in structure])
# Sites that have atomic radii
sub_sites = []
# Record the "DLS estimated radius" from bond_params.
bp_dict = {}
for sp in list(structure.composition.keys()):
if sp.atomic_radius:
sub_sites.extend([site for site in structure
if site.specie == sp])
else:
warnings.warn("VolumePredictor: no atomic radius data for "
"{}".format(sp))
if sp.symbol not in bond_params:
warnings.warn("VolumePredictor: bond parameters not found, "
"used atomic radii for {}".format(sp))
else:
r, k = bond_params[sp.symbol]["r"], bond_params[sp.symbol]["k"]
bp_dict[sp] = float(r) + float(k) * std_x
# Structure object that include only sites with known atomic radii.
reduced_structure = Structure.from_sites(sub_sites)
smallest_ratio = None
for site1 in reduced_structure:
sp1 = site1.specie
neighbors = reduced_structure.get_neighbors(site1,
sp1.atomic_radius +
self.cutoff)
for nn in neighbors:
sp2 = nn.specie
if sp1 in bp_dict and sp2 in bp_dict:
expected_dist = bp_dict[sp1] + bp_dict[sp2]
else:
expected_dist = sp1.atomic_radius + sp2.atomic_radius
if not smallest_ratio or nn.nn_distance / expected_dist < smallest_ratio:
smallest_ratio = nn.nn_distance / expected_dist
if not smallest_ratio:
raise ValueError("Could not find any bonds within the given cutoff "
"in this structure.")
volume_factor = (1 / smallest_ratio) ** 3
# icsd volume fudge factor
if icsd_vol:
volume_factor *= 1.05
if self.min_scaling:
volume_factor = max(self.min_scaling, volume_factor)
if self.max_scaling:
volume_factor = min(self.max_scaling, volume_factor)
return structure.volume * volume_factor
def get_predicted_structure(self, structure, icsd_vol=False):
"""
Given a structure, returns back the structure scaled to predicted
volume.
Args:
structure (Structure): structure w/unknown volume
Returns:
a Structure object with predicted volume
"""
new_structure = structure.copy()
new_structure.scale_lattice(self.predict(structure, icsd_vol=icsd_vol))
return new_structure