kim-tools 0.2.0b0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kim_tools/__init__.py +14 -0
- kim_tools/aflow_util/__init__.py +4 -0
- kim_tools/aflow_util/core.py +1782 -0
- kim_tools/aflow_util/data/README_PROTO.TXT +3241 -0
- kim_tools/ase/__init__.py +4 -0
- kim_tools/ase/core.py +749 -0
- kim_tools/kimunits.py +162 -0
- kim_tools/symmetry_util/__init__.py +4 -0
- kim_tools/symmetry_util/core.py +552 -0
- kim_tools/symmetry_util/data/possible_primitive_shifts.json +1 -0
- kim_tools/symmetry_util/data/primitive_GENPOS_ops.json +1 -0
- kim_tools/symmetry_util/data/space_groups_for_each_bravais_lattice.json +179 -0
- kim_tools/symmetry_util/data/wyck_pos_xform_under_normalizer.json +1344 -0
- kim_tools/symmetry_util/data/wyckoff_multiplicities.json +2193 -0
- kim_tools/symmetry_util/data/wyckoff_sets.json +232 -0
- kim_tools/test_driver/__init__.py +4 -0
- kim_tools/test_driver/core.py +1932 -0
- kim_tools/vc/__init__.py +4 -0
- kim_tools/vc/core.py +397 -0
- kim_tools-0.2.0b0.dist-info/METADATA +32 -0
- kim_tools-0.2.0b0.dist-info/RECORD +24 -0
- kim_tools-0.2.0b0.dist-info/WHEEL +5 -0
- kim_tools-0.2.0b0.dist-info/licenses/LICENSE.CDDL +380 -0
- kim_tools-0.2.0b0.dist-info/top_level.txt +1 -0
kim_tools/ase/core.py
ADDED
@@ -0,0 +1,749 @@
|
|
1
|
+
################################################################################
|
2
|
+
#
|
3
|
+
# CDDL HEADER START
|
4
|
+
#
|
5
|
+
# The contents of this file are subject to the terms of the Common Development
|
6
|
+
# and Distribution License Version 1.0 (the "License").
|
7
|
+
#
|
8
|
+
# You can obtain a copy of the license at
|
9
|
+
# http:# www.opensource.org/licenses/CDDL-1.0. See the License for the
|
10
|
+
# specific language governing permissions and limitations under the License.
|
11
|
+
#
|
12
|
+
# When distributing Covered Code, include this CDDL HEADER in each file and
|
13
|
+
# include the License file in a prominent location with the name LICENSE.CDDL.
|
14
|
+
# If applicable, add the following below this CDDL HEADER, with the fields
|
15
|
+
# enclosed by brackets "[]" replaced with your own identifying information:
|
16
|
+
#
|
17
|
+
# Portions Copyright (c) [yyyy] [name of copyright owner]. All rights reserved.
|
18
|
+
#
|
19
|
+
# CDDL HEADER END
|
20
|
+
#
|
21
|
+
# Copyright (c) 2017-2019, Regents of the University of Minnesota.
|
22
|
+
# All rights reserved.
|
23
|
+
#
|
24
|
+
# Contributor(s):
|
25
|
+
# Ellad B. Tadmor
|
26
|
+
# Daniel S. Karls
|
27
|
+
#
|
28
|
+
################################################################################
|
29
|
+
"""
|
30
|
+
Helper routines for KIM Tests and Verification Checks
|
31
|
+
|
32
|
+
"""
|
33
|
+
|
34
|
+
import itertools
|
35
|
+
import random
|
36
|
+
|
37
|
+
import numpy as np
|
38
|
+
from ase import Atoms
|
39
|
+
from ase.calculators.kim.kim import KIM
|
40
|
+
from ase.data import chemical_symbols
|
41
|
+
|
42
|
+
__all__ = [
|
43
|
+
"KIMASEError",
|
44
|
+
"atom_outside_cell_along_nonperiodic_dim",
|
45
|
+
"check_if_atoms_interacting_energy",
|
46
|
+
"check_if_atoms_interacting_force",
|
47
|
+
"check_if_atoms_interacting",
|
48
|
+
"get_isolated_energy_per_atom",
|
49
|
+
"get_model_energy_cutoff",
|
50
|
+
"fractional_coords_transformation",
|
51
|
+
"perturb_until_all_forces_sizeable",
|
52
|
+
"randomize_positions",
|
53
|
+
"randomize_species",
|
54
|
+
"remove_species_not_supported_by_ASE",
|
55
|
+
"rescale_to_get_nonzero_energy",
|
56
|
+
"rescale_to_get_nonzero_forces",
|
57
|
+
]
|
58
|
+
|
59
|
+
|
60
|
+
################################################################################
|
61
|
+
class KIMASEError(Exception):
|
62
|
+
def __init__(self, msg):
|
63
|
+
# Call the base class constructor with the parameters it needs
|
64
|
+
super(KIMASEError, self).__init__(msg)
|
65
|
+
self.msg = msg
|
66
|
+
|
67
|
+
def __str__(self):
|
68
|
+
return self.msg
|
69
|
+
|
70
|
+
|
71
|
+
################################################################################
|
72
|
+
def remove_species_not_supported_by_ASE(species):
|
73
|
+
"""
|
74
|
+
Remove any species from the 'species' list that are not supported by ASE
|
75
|
+
"""
|
76
|
+
supported_species = chemical_symbols[1:]
|
77
|
+
return [s for s in species if s in supported_species]
|
78
|
+
|
79
|
+
|
80
|
+
################################################################################
|
81
|
+
def randomize_species(atoms, species, seed=None):
|
82
|
+
"""
|
83
|
+
Given an ASE 'atoms' object, set random element for each atom selected
|
84
|
+
from the list of available 'species' in a way that ensures all are
|
85
|
+
represented with the same probabilities.
|
86
|
+
"""
|
87
|
+
if seed is not None:
|
88
|
+
random.seed(seed)
|
89
|
+
|
90
|
+
# Indefinitely iterate through species putting them at random
|
91
|
+
# unoccupied sites until all atoms are exhausted.
|
92
|
+
indices = list(range(len(atoms)))
|
93
|
+
num_occupied = 0
|
94
|
+
for element in itertools.cycle(species):
|
95
|
+
i = random.randint(0, len(indices) - 1)
|
96
|
+
atoms[indices[i]].symbol = element
|
97
|
+
del indices[i]
|
98
|
+
num_occupied += 1
|
99
|
+
if num_occupied == len(atoms):
|
100
|
+
break
|
101
|
+
|
102
|
+
|
103
|
+
################################################################################
|
104
|
+
def fractional_coords_transformation(cell):
|
105
|
+
"""
|
106
|
+
Given a set of cell vectors, this will return a transformation matrix T that can be
|
107
|
+
used to multiply any arbitrary position to get its fractional coordinates in the
|
108
|
+
basis of those cell vectors.
|
109
|
+
"""
|
110
|
+
simulation_cell_volume = np.linalg.det(cell)
|
111
|
+
|
112
|
+
T = (
|
113
|
+
np.vstack(
|
114
|
+
(
|
115
|
+
np.cross(cell[:, 1], cell[:, 2]),
|
116
|
+
np.cross(cell[:, 2], cell[:, 0]),
|
117
|
+
np.cross(cell[:, 0], cell[:, 1]),
|
118
|
+
)
|
119
|
+
)
|
120
|
+
/ simulation_cell_volume
|
121
|
+
)
|
122
|
+
|
123
|
+
return T
|
124
|
+
|
125
|
+
|
126
|
+
################################################################################
|
127
|
+
def atom_outside_cell_along_nonperiodic_dim(T, atom_coord, pbc, tol=1e-12):
|
128
|
+
"""
|
129
|
+
Given a transformation matrix to apply to an atomic position to get its fractional
|
130
|
+
position in the basis of some cell vectors and the corresponding boundary
|
131
|
+
conditions, determine if the atom is outside of the cell along any non-periodic
|
132
|
+
directions (measured using tolerance 'tol'). This is relevant when using an SM from
|
133
|
+
a simulator such as LAMMPS that performs spatial decomposition -- atoms that leave
|
134
|
+
the box along non-periodic dimensions are likely to become "lost."
|
135
|
+
"""
|
136
|
+
if all(pbc):
|
137
|
+
# Skip the actual checks
|
138
|
+
return False
|
139
|
+
|
140
|
+
# Calculate fractional coordinate of this atom
|
141
|
+
atom_coord_fractional = np.dot(T, atom_coord)
|
142
|
+
for dof in range(0, 3):
|
143
|
+
if not pbc[dof] and (
|
144
|
+
atom_coord_fractional[dof] < -tol or atom_coord_fractional[dof] > 1 + tol
|
145
|
+
):
|
146
|
+
return True
|
147
|
+
|
148
|
+
# If we made it to here, this atom's position is OK
|
149
|
+
return False
|
150
|
+
|
151
|
+
|
152
|
+
################################################################################
|
153
|
+
def randomize_positions(atoms, pert_amp, seed=None):
|
154
|
+
"""
|
155
|
+
Given an ASE 'atoms' object, displace all atomic coordinates by a random amount in
|
156
|
+
the range [-pert_amp, pert_amp] along *each* dimension. Note that all atomic
|
157
|
+
coordinates must be inside of the corresponding cell along non-periodic dimensions.
|
158
|
+
As each atom is looped over, we continue generating perturbations until the
|
159
|
+
displaced position is inside of the cell along any non-periodic directions
|
160
|
+
(displacing outside the cell along periodic dimensions is allowed, although it's up
|
161
|
+
to the calling function to wrap the positions if they need to be).
|
162
|
+
"""
|
163
|
+
if seed is not None:
|
164
|
+
random.seed(seed)
|
165
|
+
|
166
|
+
pbc = atoms.get_pbc()
|
167
|
+
if all(pbc):
|
168
|
+
# Don't need to worry about moving atoms outside of cell
|
169
|
+
for at in range(0, len(atoms)):
|
170
|
+
atoms[at].position += [
|
171
|
+
pert_amp * random.uniform(-1.0, 1.0) for i in range(3)
|
172
|
+
]
|
173
|
+
|
174
|
+
else:
|
175
|
+
# Get transformation matrix to get fractional coords
|
176
|
+
T = fractional_coords_transformation(atoms.get_cell())
|
177
|
+
|
178
|
+
for at in range(0, len(atoms)):
|
179
|
+
# Check if the positional coordinate of this atom is valid to begin
|
180
|
+
# with, i.e. it's inside the box along along all non-periodic directions
|
181
|
+
if atom_outside_cell_along_nonperiodic_dim(T, atoms[at].position, pbc):
|
182
|
+
raise KIMASEError(
|
183
|
+
"ERROR: Determined that atom {} with position {} is outside of the "
|
184
|
+
"simulation cell ({}) along one or more non-periodic directions. "
|
185
|
+
"In order to prevent atoms from being lost, they must all be "
|
186
|
+
"contained inside of the simulation cell along non-periodic "
|
187
|
+
"dimensions.".format(at, atoms[at].position, atoms.get_cell())
|
188
|
+
)
|
189
|
+
|
190
|
+
for dof in range(0, 3):
|
191
|
+
coord = atoms[at].position[dof].copy()
|
192
|
+
done = False
|
193
|
+
while not done:
|
194
|
+
atoms[at].position[dof] += random.uniform(-1.0, 1.0) * pert_amp
|
195
|
+
if not atom_outside_cell_along_nonperiodic_dim(
|
196
|
+
T, atoms[at].position, pbc
|
197
|
+
):
|
198
|
+
done = True
|
199
|
+
else:
|
200
|
+
atoms[at].position[dof] = coord
|
201
|
+
|
202
|
+
|
203
|
+
################################################################################
|
204
|
+
def get_isolated_energy_per_atom(model, symbol):
|
205
|
+
"""
|
206
|
+
Construct a non-periodic cell containing a single atom and compute its energy.
|
207
|
+
"""
|
208
|
+
single_atom = Atoms(
|
209
|
+
symbol,
|
210
|
+
positions=[(0.1, 0.1, 0.1)],
|
211
|
+
cell=(20, 20, 20),
|
212
|
+
pbc=(False, False, False),
|
213
|
+
)
|
214
|
+
calc = KIM(model)
|
215
|
+
single_atom.calc = calc
|
216
|
+
energy_per_atom = single_atom.get_potential_energy()
|
217
|
+
if hasattr(calc, "clean"):
|
218
|
+
calc.clean()
|
219
|
+
if hasattr(calc, "__del__"):
|
220
|
+
calc.__del__()
|
221
|
+
del single_atom
|
222
|
+
return energy_per_atom
|
223
|
+
|
224
|
+
|
225
|
+
################################################################################
|
226
|
+
def rescale_to_get_nonzero_energy(atoms, isolated_energy_per_atom, etol):
|
227
|
+
"""
|
228
|
+
If the given configuration has a potential energy, relative to the sum of the
|
229
|
+
isolated energy corresponding to each atom present, smaller in magnitude than 'etol'
|
230
|
+
(presumably because the distance between atoms is too large), rescale it making it
|
231
|
+
smaller. The 'isolated_energy_per_atom' arg should be a dict containing an entry
|
232
|
+
for each atomic species present in the atoms object
|
233
|
+
(additional entries are ignored).
|
234
|
+
"""
|
235
|
+
num_atoms = len(atoms)
|
236
|
+
if num_atoms < 2:
|
237
|
+
# If we're only using a single atom, then we need to make sure that the cell is
|
238
|
+
# periodic along at least one direction
|
239
|
+
if num_atoms == 1:
|
240
|
+
pbc = atoms.get_pbc()
|
241
|
+
if not any(pbc):
|
242
|
+
raise RuntimeError(
|
243
|
+
"ERROR: If only a single atom is present, the cell must "
|
244
|
+
"be periodic along at least one direction."
|
245
|
+
)
|
246
|
+
else:
|
247
|
+
raise RuntimeError(
|
248
|
+
"ERROR: Invalid configuration. Must have at least one atom"
|
249
|
+
)
|
250
|
+
|
251
|
+
if not isinstance(isolated_energy_per_atom, dict):
|
252
|
+
raise ValueError(
|
253
|
+
"Argument 'isolated_energy_per_atom' passed to "
|
254
|
+
"rescale_to_get_nonzero_energy must be a dict containing and entry "
|
255
|
+
"for each atomic species present in the atoms object."
|
256
|
+
)
|
257
|
+
|
258
|
+
# Check for any flat directions in the initial configuration and ignore them when
|
259
|
+
# determining whether to stop rescaling
|
260
|
+
pmin = atoms.get_positions().min(axis=0) # minimum x,y,z coordinates
|
261
|
+
pmax = atoms.get_positions().max(axis=0) # maximum x,y,z coordinates
|
262
|
+
delp = pmax - pmin # system extent across x, y, z
|
263
|
+
flat = [(extent <= np.finfo(extent).tiny) for extent in delp]
|
264
|
+
|
265
|
+
# Compute the "trivial energy", i.e. the energy assuming none of the atoms interact
|
266
|
+
# with each other at all
|
267
|
+
species_of_each_atom = atoms.get_chemical_symbols()
|
268
|
+
energy_trivial = 0.0
|
269
|
+
for atom_species in species_of_each_atom:
|
270
|
+
energy_trivial += isolated_energy_per_atom[atom_species]
|
271
|
+
|
272
|
+
# Rescale cell and atoms
|
273
|
+
cell = atoms.get_cell()
|
274
|
+
energy = atoms.get_potential_energy()
|
275
|
+
adjusted_energy = energy - energy_trivial
|
276
|
+
if abs(adjusted_energy) <= etol:
|
277
|
+
pmin = atoms.get_positions().min(axis=0) # minimum x,y,z coordinates
|
278
|
+
pmax = atoms.get_positions().max(axis=0) # maximum x,y,z coordinates
|
279
|
+
extent_along_nonflat_directions = [
|
280
|
+
extent for direction, extent in enumerate(delp) if not flat[direction]
|
281
|
+
]
|
282
|
+
delpmin = min(extent_along_nonflat_directions)
|
283
|
+
while delpmin > np.finfo(delpmin).tiny:
|
284
|
+
atoms.positions *= 0.5 # make configuration half the size
|
285
|
+
cell *= 0.5 # make cell half the size
|
286
|
+
atoms.set_cell(cell) # need to adjust cell in case it's periodic
|
287
|
+
delpmin *= 0.5
|
288
|
+
energy = atoms.get_potential_energy()
|
289
|
+
adjusted_energy = energy - energy_trivial
|
290
|
+
if abs(adjusted_energy) > etol:
|
291
|
+
return # success!
|
292
|
+
|
293
|
+
# Get species and write out error
|
294
|
+
raise RuntimeError(
|
295
|
+
"ERROR: Unable to scale configuration down to nonzero energy. This was "
|
296
|
+
"determined by computing the total potential energy relative to the sum of "
|
297
|
+
"the isolated energy corresponding to each atom present and checking if "
|
298
|
+
"the magnitude of the difference was larger than the supplied tolerance of "
|
299
|
+
"{} eV. This may mean that the species present in the cell ({}) do not "
|
300
|
+
"have a non-trivial energy interaction for the potential being used."
|
301
|
+
"".format(etol, set(species_of_each_atom))
|
302
|
+
)
|
303
|
+
|
304
|
+
|
305
|
+
################################################################################
|
306
|
+
def check_if_atoms_interacting_energy(model, symbols, etol):
|
307
|
+
"""
|
308
|
+
First, get the energy of a single isolated atom of each species given in 'symbols'.
|
309
|
+
Then, construct a dimer consisting of these two species and try to decrease its bond
|
310
|
+
length until a discernible difference in the energy (from the sum of the isolated
|
311
|
+
energy of each species) is detected. The 'symbols' arg should be a list or tuple of
|
312
|
+
length 2 indicating which species pair to check, e.g. to check if Al interacts with
|
313
|
+
Al, one should specify ['Al', 'Al'].
|
314
|
+
"""
|
315
|
+
if not isinstance(symbols, (list, tuple)) or len(symbols) != 2:
|
316
|
+
raise ValueError(
|
317
|
+
"Argument 'symbols' passed to check_if_atoms_interacting_energy "
|
318
|
+
"must be a list of tuple of length 2 indicating the species pair to "
|
319
|
+
"check"
|
320
|
+
)
|
321
|
+
|
322
|
+
isolated_energy_per_atom = {}
|
323
|
+
isolated_energy_per_atom[symbols[0]] = get_isolated_energy_per_atom(
|
324
|
+
model, symbols[0]
|
325
|
+
)
|
326
|
+
isolated_energy_per_atom[symbols[1]] = get_isolated_energy_per_atom(
|
327
|
+
model, symbols[1]
|
328
|
+
)
|
329
|
+
|
330
|
+
dimer = Atoms(
|
331
|
+
symbols,
|
332
|
+
positions=[(0.1, 0.1, 0.1), (5.1, 0.1, 0.1)],
|
333
|
+
cell=(20, 20, 20),
|
334
|
+
pbc=(False, False, False),
|
335
|
+
)
|
336
|
+
calc = KIM(model)
|
337
|
+
dimer.calc = calc
|
338
|
+
try:
|
339
|
+
rescale_to_get_nonzero_energy(dimer, isolated_energy_per_atom, etol)
|
340
|
+
atoms_interacting = True
|
341
|
+
return atoms_interacting
|
342
|
+
except: # noqa: E722
|
343
|
+
atoms_interacting = False
|
344
|
+
return atoms_interacting
|
345
|
+
finally:
|
346
|
+
if hasattr(calc, "clean"):
|
347
|
+
calc.clean()
|
348
|
+
if hasattr(calc, "__del__"):
|
349
|
+
calc.__del__()
|
350
|
+
del dimer
|
351
|
+
|
352
|
+
|
353
|
+
################################################################################
|
354
|
+
def check_if_atoms_interacting_force(model, symbols, ftol):
|
355
|
+
"""
|
356
|
+
Construct a dimer and try to decrease its bond length until the force acting on each
|
357
|
+
atom is larger than 'ftol' in magnitude. The 'symbols' arg should be a list or
|
358
|
+
tuple of length 2 indicating which species pair to check, e.g. to check if Al
|
359
|
+
interacts with Al, one should specify ['Al', 'Al'].
|
360
|
+
"""
|
361
|
+
if not isinstance(symbols, (list, tuple)) or len(symbols) != 2:
|
362
|
+
raise ValueError(
|
363
|
+
"Argument 'symbols' passed to check_if_atoms_interacting_force "
|
364
|
+
"must be a list of tuple of length 2 indicating the species pair to "
|
365
|
+
"check"
|
366
|
+
)
|
367
|
+
|
368
|
+
dimer = Atoms(
|
369
|
+
symbols,
|
370
|
+
positions=[(0.1, 0.1, 0.1), (5.1, 0.1, 0.1)],
|
371
|
+
cell=(20, 20, 20),
|
372
|
+
pbc=(False, False, False),
|
373
|
+
)
|
374
|
+
calc = KIM(model)
|
375
|
+
dimer.calc = calc
|
376
|
+
try:
|
377
|
+
rescale_to_get_nonzero_forces(dimer, ftol)
|
378
|
+
atoms_interacting = True
|
379
|
+
return atoms_interacting
|
380
|
+
except: # noqa: E722
|
381
|
+
atoms_interacting = False
|
382
|
+
return atoms_interacting
|
383
|
+
finally:
|
384
|
+
if hasattr(calc, "clean"):
|
385
|
+
calc.clean()
|
386
|
+
if hasattr(calc, "__del__"):
|
387
|
+
calc.__del__()
|
388
|
+
del dimer
|
389
|
+
|
390
|
+
|
391
|
+
################################################################################
|
392
|
+
def check_if_atoms_interacting(
|
393
|
+
model, symbols, check_energy=True, etol=1e-6, check_force=True, ftol=1e-3
|
394
|
+
):
|
395
|
+
"""
|
396
|
+
Check to see whether non-trivial energy and/or forces can be detected using the
|
397
|
+
current model. The 'symbols' arg should be a list or tuple of length 2 indicating
|
398
|
+
which species pair to check, e.g. to check if Al interacts with Al, one should
|
399
|
+
specify ['Al', 'Al'].
|
400
|
+
"""
|
401
|
+
if check_energy and not check_force:
|
402
|
+
return check_if_atoms_interacting_energy(model, symbols, etol)
|
403
|
+
elif not check_energy and check_force:
|
404
|
+
return check_if_atoms_interacting_force(model, symbols, ftol)
|
405
|
+
|
406
|
+
elif check_energy and check_force:
|
407
|
+
atoms_interacting_energy = check_if_atoms_interacting_energy(
|
408
|
+
model, symbols, etol
|
409
|
+
)
|
410
|
+
atoms_interacting_force = check_if_atoms_interacting_energy(
|
411
|
+
model, symbols, ftol
|
412
|
+
)
|
413
|
+
return atoms_interacting_energy, atoms_interacting_force
|
414
|
+
|
415
|
+
|
416
|
+
################################################################################
|
417
|
+
def rescale_to_get_nonzero_forces(atoms, ftol):
|
418
|
+
"""
|
419
|
+
If the given configuration has force components which are all smaller in absolute
|
420
|
+
value than 'ftol' (presumably because the distance between atoms is too large),
|
421
|
+
rescale it to be smaller until the largest force component in absolute value is
|
422
|
+
greater than or equal to 'ftol'. In a perfect crystal, the crystal is rescaled
|
423
|
+
until the atoms on the surface reach the minimum value (internal atoms padded with
|
424
|
+
another atoms around them will have zero force). Note that any periodicity is
|
425
|
+
turned off for the rescaling and then restored at the end.
|
426
|
+
"""
|
427
|
+
if len(atoms) < 2:
|
428
|
+
raise KIMASEError(
|
429
|
+
"ERROR: Invalid configuration. Must have at least 2 atoms. Number of atoms "
|
430
|
+
"= {}".format(len(atoms))
|
431
|
+
)
|
432
|
+
|
433
|
+
# Check for any flat directions in the initial configuration and ignore them when
|
434
|
+
# determining whether to stop rescaling
|
435
|
+
pmin = atoms.get_positions().min(axis=0) # minimum x,y,z coordinates
|
436
|
+
pmax = atoms.get_positions().max(axis=0) # maximum x,y,z coordinates
|
437
|
+
delp = pmax - pmin # system extent across x, y, z
|
438
|
+
flat = [(extent <= np.finfo(extent).tiny) for extent in delp]
|
439
|
+
|
440
|
+
# Temporarily turn off any periodicity
|
441
|
+
pbc_save = atoms.get_pbc()
|
442
|
+
cell = atoms.get_cell()
|
443
|
+
atoms.set_pbc([False, False, False])
|
444
|
+
# Rescale cell and atoms
|
445
|
+
forces = atoms.get_forces()
|
446
|
+
if np.isnan(forces).any():
|
447
|
+
raise RuntimeError("ERROR: Computed forces include at least one nan.")
|
448
|
+
fmax = max(abs(forces.min()), abs(forces.max())) # find max in abs value
|
449
|
+
if fmax < ftol:
|
450
|
+
pmin = atoms.get_positions().min(axis=0) # minimum x,y,z coordinates
|
451
|
+
pmax = atoms.get_positions().max(axis=0) # maximum x,y,z coordinates
|
452
|
+
extent_along_nonflat_directions = [
|
453
|
+
extent for direction, extent in enumerate(delp) if not flat[direction]
|
454
|
+
]
|
455
|
+
delpmin = min(extent_along_nonflat_directions)
|
456
|
+
while delpmin > np.finfo(delpmin).tiny:
|
457
|
+
atoms.positions *= 0.75 # make configuration 3/4 the size
|
458
|
+
cell *= 0.75 # make cell 3/4 the size
|
459
|
+
delpmin *= 0.75
|
460
|
+
forces = atoms.get_forces() # get max force
|
461
|
+
fmax = max(abs(forces.min()), abs(forces.max()))
|
462
|
+
if fmax >= ftol:
|
463
|
+
# Restore periodicity
|
464
|
+
atoms.set_pbc(pbc_save)
|
465
|
+
atoms.set_cell(cell)
|
466
|
+
return # success!
|
467
|
+
raise KIMASEError(
|
468
|
+
"ERROR: Unable to scale configuration down to nonzero forces."
|
469
|
+
)
|
470
|
+
else:
|
471
|
+
# Restore periodicity
|
472
|
+
atoms.set_pbc(pbc_save)
|
473
|
+
|
474
|
+
|
475
|
+
################################################################################
|
476
|
+
def perturb_until_all_forces_sizeable(
|
477
|
+
atoms, pert_amp, minfact=0.1, maxfact=5.0, max_iter=1000
|
478
|
+
):
|
479
|
+
"""
|
480
|
+
Keep perturbing atoms in the ASE 'atoms' object until all force components on each
|
481
|
+
atom have an absolute value of least 'minfact' times the largest (in absolute value)
|
482
|
+
component across all force vectors coming in. Note that all atomic coordinates must
|
483
|
+
be inside of the corresponding cell along non-periodic dimensions. Perturbations
|
484
|
+
leading to a force component on any atom that is larger than 'maxfact' times the
|
485
|
+
largest force component coming in are rejected. Perturbations leading to atoms
|
486
|
+
outside of the span across x, y, and z of the atomic positions coming in or outside
|
487
|
+
of the simulation cell along non-periodic directions are also rejected. The process
|
488
|
+
repeats until max_iter iterations have been reached, at which point an exception is
|
489
|
+
raised.
|
490
|
+
"""
|
491
|
+
pbc = atoms.get_pbc()
|
492
|
+
|
493
|
+
# Get transformation matrix to get fractional coords
|
494
|
+
T = fractional_coords_transformation(atoms.get_cell())
|
495
|
+
|
496
|
+
# First, ensure that all atomic positions are inside of the cell along non-periodic
|
497
|
+
# dimensions
|
498
|
+
if not all(pbc):
|
499
|
+
for at in range(0, len(atoms)):
|
500
|
+
# Check if the positional coordinate of this atom is valid to begin
|
501
|
+
# with, i.e. it's inside the box along along all non-periodic directions
|
502
|
+
if atom_outside_cell_along_nonperiodic_dim(T, atoms[at].position, pbc):
|
503
|
+
raise KIMASEError(
|
504
|
+
"ERROR: Determined that atom {} with position {} is outside of the "
|
505
|
+
"simulation cell ({}) along one or more non-periodic directions. "
|
506
|
+
"In order to prevent atoms from being lost, they must all be "
|
507
|
+
"contained inside of the simulation cell along non-periodic "
|
508
|
+
"dimensions.".format(at, atoms[at].position, atoms.get_cell())
|
509
|
+
)
|
510
|
+
|
511
|
+
forces = atoms.get_forces()
|
512
|
+
if np.isnan(forces).any():
|
513
|
+
raise RuntimeError("ERROR: Computed forces include at least one nan.")
|
514
|
+
fmax = max(abs(forces.min()), abs(forces.max())) # find max in abs value
|
515
|
+
|
516
|
+
if fmax <= 1e2 * np.finfo(float).eps:
|
517
|
+
raise KIMASEError(
|
518
|
+
"ERROR: Largest force component on configuration is "
|
519
|
+
"less than or equal to 1e2*machine epsilon. Cannot proceed."
|
520
|
+
)
|
521
|
+
|
522
|
+
pmin = atoms.get_positions().min(axis=0) # minimum x,y,z coordinates
|
523
|
+
pmax = atoms.get_positions().max(axis=0) # maximum x,y,z coordinates
|
524
|
+
saved_posns = atoms.get_positions().copy()
|
525
|
+
saved_forces = atoms.get_forces().copy()
|
526
|
+
some_forces_too_small = True
|
527
|
+
|
528
|
+
# Counter to enforce max iterations
|
529
|
+
iters = 0
|
530
|
+
|
531
|
+
while some_forces_too_small:
|
532
|
+
for at in range(0, len(atoms)):
|
533
|
+
for dof in range(0, 3):
|
534
|
+
if abs(forces[at, dof]) < minfact * fmax:
|
535
|
+
done = False
|
536
|
+
coord = atoms[at].position[dof].copy()
|
537
|
+
while not done:
|
538
|
+
atoms[at].position[dof] += random.uniform(-1.0, 1.0) * pert_amp
|
539
|
+
if (
|
540
|
+
pmin[dof] <= atoms[at].position[dof] <= pmax[dof]
|
541
|
+
) and not atom_outside_cell_along_nonperiodic_dim(
|
542
|
+
T, atoms[at].position, pbc
|
543
|
+
):
|
544
|
+
done = True
|
545
|
+
else:
|
546
|
+
atoms[at].position[dof] = coord
|
547
|
+
try:
|
548
|
+
forces = atoms.get_forces()
|
549
|
+
if np.isnan(forces).any():
|
550
|
+
raise RuntimeError("ERROR: Computed forces include at least one nan.")
|
551
|
+
fmax_new = max(abs(forces.min()), abs(forces.max()))
|
552
|
+
if fmax_new > maxfact * fmax:
|
553
|
+
# forces too large, abort perturbation
|
554
|
+
atoms.set_positions(saved_posns)
|
555
|
+
forces = saved_forces.copy()
|
556
|
+
continue
|
557
|
+
fmin_new = min(abs(forces.min()), abs(forces.max()))
|
558
|
+
if fmin_new > minfact * fmax:
|
559
|
+
some_forces_too_small = False
|
560
|
+
except: # noqa: E722
|
561
|
+
# force calculation failed, abort perturbation
|
562
|
+
atoms.set_positions(saved_posns)
|
563
|
+
continue
|
564
|
+
finally:
|
565
|
+
iters = iters + 1
|
566
|
+
if iters == max_iter:
|
567
|
+
raise KIMASEError(
|
568
|
+
"Maximum iterations ({}) exceeded in call to "
|
569
|
+
"function perturb_until_all_forces_sizeable()".format(max_iter)
|
570
|
+
)
|
571
|
+
|
572
|
+
|
573
|
+
################################################################################
|
574
|
+
def get_model_energy_cutoff(
|
575
|
+
model,
|
576
|
+
symbols,
|
577
|
+
xtol=1e-8,
|
578
|
+
etol_coarse=1e-6,
|
579
|
+
etol_fine=1e-15,
|
580
|
+
max_bisect_iters=1000,
|
581
|
+
max_upper_cutoff_bracket=20.0,
|
582
|
+
):
|
583
|
+
"""
|
584
|
+
Compute the distance at which energy interactions become non-trival for a given
|
585
|
+
model and a species pair it supports. This is done by constructing a dimer composed
|
586
|
+
of these species in a large finite box, increasing the separation if necessary until
|
587
|
+
the total potential energy is within 'etol_fine' of the sum of the corresponding
|
588
|
+
isolated energies, and then shrinking the separation until the energy differs from
|
589
|
+
that value by more than 'etol_coarse'. Using these two separations to bound the
|
590
|
+
search range, bisection is used to refine in order to locate the cutoff. The
|
591
|
+
'symbols' arg should be a list or tuple of length 2 indicating which species pair to
|
592
|
+
check, e.g. to get the energy cutoff of Al with Al, one should specify ['Al', 'Al'].
|
593
|
+
|
594
|
+
This function is based on the content of the DimerContinuityC1__VC_303890932454_002
|
595
|
+
Verification Check in OpenKIM [1-3].
|
596
|
+
|
597
|
+
[1] Tadmor E. Verification Check of Dimer C1 Continuity v002. OpenKIM; 2018.
|
598
|
+
doi:10.25950/43d2c6d5
|
599
|
+
|
600
|
+
[2] Tadmor EB, Elliott RS, Sethna JP, Miller RE, Becker CA. The potential of
|
601
|
+
atomistic simulations and the Knowledgebase of Interatomic Models. JOM.
|
602
|
+
2011;63(7):17. doi:10.1007/s11837-011-0102-6
|
603
|
+
|
604
|
+
[3] Elliott RS, Tadmor EB. Knowledgebase of Interatomic Models (KIM) Application
|
605
|
+
Programming Interface (API). OpenKIM; 2011. doi:10.25950/ff8f563a
|
606
|
+
"""
|
607
|
+
from scipy.optimize import bisect
|
608
|
+
|
609
|
+
def get_dimer_positions(a, large_cell_len):
|
610
|
+
"""
|
611
|
+
Generate positions for a dimer of length 'a' centered in a finite simulation box
|
612
|
+
with side length 'large_cell_len'
|
613
|
+
"""
|
614
|
+
half_cell = 0.5 * large_cell_len
|
615
|
+
positions = [
|
616
|
+
[half_cell - 0.5 * a, half_cell, half_cell],
|
617
|
+
[half_cell + 0.5 * a, half_cell, half_cell],
|
618
|
+
]
|
619
|
+
return positions
|
620
|
+
|
621
|
+
def energy(a, dimer, large_cell_len, einf):
|
622
|
+
dimer.set_positions(get_dimer_positions(a, large_cell_len))
|
623
|
+
return dimer.get_potential_energy() - einf
|
624
|
+
|
625
|
+
def energy_cheat(a, dimer, large_cell_len, offset, einf):
|
626
|
+
dimer.set_positions(get_dimer_positions(a, large_cell_len))
|
627
|
+
return (dimer.get_potential_energy() - einf) + offset
|
628
|
+
|
629
|
+
if not isinstance(symbols, (list, tuple)) or len(symbols) != 2:
|
630
|
+
raise ValueError(
|
631
|
+
"Argument 'symbols' passed to check_if_atoms_interacting_energy "
|
632
|
+
"must be a list of tuple of length 2 indicating the species pair to "
|
633
|
+
"check"
|
634
|
+
)
|
635
|
+
|
636
|
+
isolated_energy_per_atom = {}
|
637
|
+
isolated_energy_per_atom[symbols[0]] = get_isolated_energy_per_atom(
|
638
|
+
model, symbols[0]
|
639
|
+
)
|
640
|
+
isolated_energy_per_atom[symbols[1]] = get_isolated_energy_per_atom(
|
641
|
+
model, symbols[1]
|
642
|
+
)
|
643
|
+
einf = isolated_energy_per_atom[symbols[0]] + isolated_energy_per_atom[symbols[1]]
|
644
|
+
|
645
|
+
# First, establish the upper bracket cutoff by starting at 'b_init' Angstroms and
|
646
|
+
# incrementing by 'db' until
|
647
|
+
b_init = 4.0
|
648
|
+
|
649
|
+
# Create finite box of size large_cell_len
|
650
|
+
large_cell_len = 50
|
651
|
+
dimer = Atoms(
|
652
|
+
symbols,
|
653
|
+
positions=get_dimer_positions(b_init, large_cell_len),
|
654
|
+
cell=(large_cell_len, large_cell_len, large_cell_len),
|
655
|
+
pbc=(False, False, False),
|
656
|
+
)
|
657
|
+
calc = KIM(model)
|
658
|
+
dimer.calc = calc
|
659
|
+
|
660
|
+
db = 2.0
|
661
|
+
b = b_init - db
|
662
|
+
still_interacting = True
|
663
|
+
while still_interacting:
|
664
|
+
b += db
|
665
|
+
if b > max_upper_cutoff_bracket:
|
666
|
+
if hasattr(calc, "clean"):
|
667
|
+
calc.clean()
|
668
|
+
if hasattr(calc, "__del__"):
|
669
|
+
calc.__del__()
|
670
|
+
|
671
|
+
raise KIMASEError(
|
672
|
+
"Exceeded limit on upper bracket when determining cutoff "
|
673
|
+
"search range"
|
674
|
+
)
|
675
|
+
else:
|
676
|
+
eb = energy(b, dimer, large_cell_len, einf)
|
677
|
+
if abs(eb) < etol_fine:
|
678
|
+
still_interacting = False
|
679
|
+
|
680
|
+
a = b
|
681
|
+
da = 0.01
|
682
|
+
not_interacting = True
|
683
|
+
while not_interacting:
|
684
|
+
a -= da
|
685
|
+
if a < 0:
|
686
|
+
if hasattr(calc, "clean"):
|
687
|
+
calc.clean()
|
688
|
+
if hasattr(calc, "__del__"):
|
689
|
+
calc.__del__()
|
690
|
+
|
691
|
+
raise RuntimeError(
|
692
|
+
"Failed to determine lower bracket for cutoff search using etol_coarse "
|
693
|
+
"= {}. This may mean that the species pair provided ({}) does not "
|
694
|
+
"have a non-trivial energy interaction for the potential being "
|
695
|
+
"used.".format(etol_coarse, symbols)
|
696
|
+
)
|
697
|
+
else:
|
698
|
+
ea = energy(a, dimer, large_cell_len, einf)
|
699
|
+
if abs(ea) > etol_coarse:
|
700
|
+
not_interacting = False
|
701
|
+
|
702
|
+
# NOTE: Some Simulator Models have a history dependence due to them maintaining
|
703
|
+
# charges from the previous energy evaluation to use as an initial guess
|
704
|
+
# for the next charge equilibration. We therefore have to treat them not
|
705
|
+
# as single-valued functions but as distributions, i.e. for a given
|
706
|
+
# configuration you might get any of a range of energy values depending on
|
707
|
+
# the history of your previous energy evaluations. This is particularly
|
708
|
+
# problematic for this step, where we set up a bisection problem in order
|
709
|
+
# to determine the cutoff radius of the model. Our solution for this
|
710
|
+
# specific case is to make a very crude estimate of the variance of that
|
711
|
+
# distribution with a 10% factor of safety on it.
|
712
|
+
eb_new = energy(b, dimer, large_cell_len, einf)
|
713
|
+
eb_error = abs(eb_new - eb)
|
714
|
+
|
715
|
+
# compute offset to ensure that energy before and after cutoff have
|
716
|
+
# different signs
|
717
|
+
if ea < eb:
|
718
|
+
offset = -eb + 1.1 * eb_error + np.finfo(float).eps
|
719
|
+
else:
|
720
|
+
offset = -eb - 1.1 * eb_error - np.finfo(float).eps
|
721
|
+
|
722
|
+
rcut, results = bisect(
|
723
|
+
energy_cheat,
|
724
|
+
a,
|
725
|
+
b,
|
726
|
+
args=(dimer, large_cell_len, offset, einf),
|
727
|
+
full_output=True,
|
728
|
+
xtol=xtol,
|
729
|
+
maxiter=max_bisect_iters,
|
730
|
+
)
|
731
|
+
|
732
|
+
# General clean-up
|
733
|
+
if hasattr(calc, "clean"):
|
734
|
+
calc.clean()
|
735
|
+
if hasattr(calc, "__del__"):
|
736
|
+
calc.__del__()
|
737
|
+
|
738
|
+
if not results.converged:
|
739
|
+
raise RuntimeError(
|
740
|
+
"Bisection search to find cutoff distance did not converge "
|
741
|
+
"within {} iterations with xtol = {}".format(max_bisect_iters, xtol)
|
742
|
+
)
|
743
|
+
else:
|
744
|
+
return rcut
|
745
|
+
|
746
|
+
|
747
|
+
# If called directly, do nothing
|
748
|
+
if __name__ == "__main__":
|
749
|
+
pass
|