root / ase / utils / molecule_test.py @ 16
Historique | Voir | Annoter | Télécharger (9,49 ko)
1 |
#!/usr/bin/env python
|
---|---|
2 |
|
3 |
"""This module defines extensible classes for running tests on molecules.
|
4 |
|
5 |
Use this to compare different calculators, XC functionals and so on by
|
6 |
calculating e.g. atomization energies and bond lengths across the
|
7 |
g2 database of small molecules.
|
8 |
"""
|
9 |
|
10 |
import os |
11 |
import sys |
12 |
import traceback |
13 |
|
14 |
import numpy as np |
15 |
|
16 |
from ase import PickleTrajectory, read |
17 |
from ase.calculators.emt import EMT |
18 |
from ase.data.molecules import molecule, atoms as g2_atoms, g1 |
19 |
|
20 |
|
21 |
class BatchTest: |
22 |
"""Contains logic for looping over tests and file management."""
|
23 |
def __init__(self, test): |
24 |
self.test = test
|
25 |
self.txt = sys.stdout # ? |
26 |
|
27 |
def run_single_test(self, formula): |
28 |
print >> self.txt, self.test.name, formula, '...', |
29 |
self.txt.flush()
|
30 |
filename = self.test.get_filename(formula)
|
31 |
if os.path.exists(filename):
|
32 |
print >> self.txt, 'Skipped.' |
33 |
return
|
34 |
try:
|
35 |
open(filename, 'w').close() # Empty file |
36 |
system, calc = self.test.setup(formula)
|
37 |
self.test.run(formula, system, filename)
|
38 |
print >> self.txt, 'OK!' |
39 |
self.txt.flush()
|
40 |
except self.test.exceptions: |
41 |
print >> self.txt, 'Failed!' |
42 |
traceback.print_exc(file=self.txt)
|
43 |
print >> self.txt |
44 |
self.txt.flush()
|
45 |
|
46 |
def run(self, formulas): |
47 |
"""Run a batch of tests.
|
48 |
|
49 |
This will invoke the test method on each formula, printing
|
50 |
status to stdout.
|
51 |
|
52 |
Those formulas that already have test result files will
|
53 |
be skipped."""
|
54 |
|
55 |
# Create directories if necessary
|
56 |
if self.test.dir and not os.path.isdir(self.test.dir): |
57 |
os.mkdir(self.test.dir) # Won't work on 'dir1/dir2', but oh well |
58 |
|
59 |
for formula in formulas: |
60 |
self.run_single_test(formula)
|
61 |
|
62 |
def collect(self, formulas, verbose=False): |
63 |
"""Yield results of previous calculations."""
|
64 |
for formula in formulas: |
65 |
try:
|
66 |
filename = self.test.get_filename(formula)
|
67 |
results = self.test.retrieve(formula, filename)
|
68 |
if verbose:
|
69 |
print >> self.txt, 'Loaded:', formula, filename |
70 |
yield formula, results
|
71 |
except (IOError, RuntimeError, TypeError): |
72 |
# XXX which errors should we actually catch?
|
73 |
if verbose:
|
74 |
print >> self.txt, 'Error:', formula, '[%s]' % filename |
75 |
traceback.print_exc(file=self.txt)
|
76 |
|
77 |
|
78 |
class MoleculeTest: |
79 |
"""Generic class for runnings various tests on the g2 dataset.
|
80 |
|
81 |
Usage: instantiate MoleculeTest with desired test settings and
|
82 |
invoke its run() method on the desired formulas.
|
83 |
|
84 |
This class will use the ASE EMT calculator by default. You can
|
85 |
create a subclass using an arbitrary calculator by overriding the
|
86 |
setup_calculator method. Most methods can be overridden to
|
87 |
provide highly customized behaviour. """
|
88 |
|
89 |
def __init__(self, name, vacuum=6.0, exceptions=None): |
90 |
"""Create a molecule test.
|
91 |
|
92 |
The name parameter will be part of all output files generated
|
93 |
by this molecule test. If name contains a '/' character, the
|
94 |
preceding part will be interpreted as a directory in which to
|
95 |
put files.
|
96 |
|
97 |
The vacuum parameter is used to set the cell size.
|
98 |
|
99 |
A tuple of exception types can be provided which will be
|
100 |
caught during a batch of calculations. Types not specified
|
101 |
will be considered fatal."""
|
102 |
|
103 |
dir, path = os.path.split(name)
|
104 |
self.dir = dir |
105 |
self.name = name
|
106 |
self.vacuum = vacuum
|
107 |
if exceptions is None: |
108 |
exceptions = () |
109 |
self.exceptions = exceptions
|
110 |
|
111 |
def setup_calculator(self, system, formula): |
112 |
"""Create a new calculator.
|
113 |
|
114 |
Default is an EMT calculator. Most implementations will want to
|
115 |
override this method."""
|
116 |
raise NotImplementedError |
117 |
|
118 |
def setup_system(self, formula): |
119 |
"""Create an Atoms object from the given formula.
|
120 |
|
121 |
By default this will be loaded from the g2 database, setting
|
122 |
the cell size by means of the molecule test's vacuum parameter."""
|
123 |
system = molecule(formula) |
124 |
system.center(vacuum=self.vacuum)
|
125 |
return system
|
126 |
|
127 |
def setup(self, formula): |
128 |
"""Build calculator and atoms objects.
|
129 |
|
130 |
This will invoke the setup_calculator and setup_system methods."""
|
131 |
system = self.setup_system(formula)
|
132 |
calc = self.setup_calculator(system, formula)
|
133 |
system.set_calculator(calc) |
134 |
return system, calc
|
135 |
|
136 |
def get_filename(self, formula, extension='traj'): |
137 |
"""Returns the filename for a test result file.
|
138 |
|
139 |
Default format is <name>.<formula>.traj
|
140 |
|
141 |
The test may write other files, but this filename is used as a
|
142 |
flag denoting whether the calculation has been done
|
143 |
already."""
|
144 |
return '.'.join([self.name, formula, extension]) |
145 |
|
146 |
def run(self, formula, system, filename): |
147 |
raise NotImplementedError |
148 |
|
149 |
def retrieve(self, formula, filename): |
150 |
"""Retrieve results of previous calculation from file.
|
151 |
|
152 |
Default implementation returns the total energy.
|
153 |
|
154 |
This method should be overridden whenever the test method is
|
155 |
overridden to calculate something else than the total energy."""
|
156 |
raise NotImplementedError |
157 |
|
158 |
|
159 |
class EnergyTest: |
160 |
def run(self, formula, system, filename): |
161 |
"""Calculate energy of specified system and save to file."""
|
162 |
system.get_potential_energy() |
163 |
# Won't create .bak file:
|
164 |
traj = PickleTrajectory(open(filename, 'w'), 'w') |
165 |
traj.write(system) |
166 |
traj.close() |
167 |
|
168 |
def retrieve(self, formula, filename): |
169 |
system = read(filename) |
170 |
energy = system.get_potential_energy() |
171 |
return energy
|
172 |
|
173 |
def calculate_atomization_energies(self, molecular_energies, |
174 |
atomic_energies): |
175 |
atomic_energy_dict = dict(atomic_energies)
|
176 |
for formula, molecular_energy in molecular_energies: |
177 |
try:
|
178 |
system = molecule(formula) |
179 |
atomic = [atomic_energy_dict[s] |
180 |
for s in system.get_chemical_symbols()] |
181 |
atomization_energy = molecular_energy - sum(atomic)
|
182 |
yield formula, atomization_energy
|
183 |
except KeyError: |
184 |
pass
|
185 |
|
186 |
|
187 |
class BondLengthTest: |
188 |
def run(self, formula, system, filename): |
189 |
"""Calculate bond length of a dimer.
|
190 |
|
191 |
This will calculate total energies for varying atomic
|
192 |
separations close to the g2 bond length, allowing
|
193 |
determination of bond length by fitting.
|
194 |
"""
|
195 |
if len(system) != 2: |
196 |
raise ValueError('Not a dimer') |
197 |
traj = PickleTrajectory(open(filename, 'w'), 'w') |
198 |
pos = system.positions |
199 |
d = np.linalg.norm(pos[1] - pos[0]) |
200 |
for x in range(-2, 3): |
201 |
system.set_distance(0, 1, d * (1.0 + x * 0.02)) |
202 |
traj.write(system) |
203 |
traj.close() |
204 |
|
205 |
def retrieve(self, formula, filename): |
206 |
traj = PickleTrajectory(filename, 'r')
|
207 |
distances = np.array([np.linalg.norm(a.positions[1] - a.positions[0]) |
208 |
for a in traj]) |
209 |
energies = np.array([a.get_potential_energy() for a in traj]) |
210 |
polynomial = np.polyfit(distances, energies, 2) # or maybe 3rd order? |
211 |
# With 3rd order it is not always obvious which root is right
|
212 |
pderiv = np.polyder(polynomial, 1)
|
213 |
d0 = np.roots(pderiv) |
214 |
e0 = np.polyval(energies, d0) |
215 |
return distances, energies, d0, e0, polynomial
|
216 |
|
217 |
|
218 |
class EMTTest(MoleculeTest): |
219 |
def setup_calculator(self, system, calculator): |
220 |
return EMT()
|
221 |
|
222 |
|
223 |
class EMTEnergyTest(EnergyTest, EMTTest): |
224 |
pass
|
225 |
|
226 |
|
227 |
class EMTBondLengthTest(BondLengthTest, EMTTest): |
228 |
pass
|
229 |
|
230 |
|
231 |
def main(): |
232 |
supported_elements = 'Ni, C, Pt, Ag, H, Al, O, N, Au, Pd, Cu'.split(', ') |
233 |
formulas = [formula for formula in g1 |
234 |
if np.all([symbol in supported_elements |
235 |
for symbol
|
236 |
in molecule(formula).get_chemical_symbols()])]
|
237 |
|
238 |
atoms = [symbol for symbol in g2_atoms if symbol in supported_elements] |
239 |
dimers = [formula for formula in formulas if len(molecule(formula)) == 2] |
240 |
|
241 |
|
242 |
name1 = 'testfiles/energy'
|
243 |
name2 = 'testfiles/bond'
|
244 |
test1 = BatchTest(EMTEnergyTest(name1, vacuum=3.0))
|
245 |
test2 = BatchTest(EMTBondLengthTest(name2, vacuum=3.0))
|
246 |
|
247 |
print 'Energy test' |
248 |
print '-----------' |
249 |
test1.run(formulas + atoms) |
250 |
|
251 |
print
|
252 |
print 'Bond length test' |
253 |
print '----------------' |
254 |
test2.run(dimers) |
255 |
|
256 |
print
|
257 |
print 'Atomization energies' |
258 |
print '--------------------' |
259 |
atomic_energies = dict(test1.collect(atoms))
|
260 |
molecular_energies = dict(test1.collect(formulas))
|
261 |
atomization_energies = {} |
262 |
for formula, energy in molecular_energies.iteritems(): |
263 |
system = molecule(formula) |
264 |
atomic = [atomic_energies[s] for s in system.get_chemical_symbols()] |
265 |
atomization_energy = energy - sum(atomic)
|
266 |
atomization_energies[formula] = atomization_energy |
267 |
print formula.rjust(10), '%.02f' % atomization_energy |
268 |
|
269 |
print
|
270 |
print 'Bond lengths' |
271 |
print '------------' |
272 |
for formula, (d_i, e_i, d0, e0, poly) in test2.collect(dimers): |
273 |
system = molecule(formula) |
274 |
bref = np.linalg.norm(system.positions[1] - system.positions[0]) |
275 |
print formula.rjust(10), '%6.3f' % d0, ' g2ref =', '%2.3f' % bref |
276 |
|
277 |
|
278 |
if __name__ == '__main__': |
279 |
main() |