Statistiques
| Révision :

root / ase / utils / molecule_test.py @ 13

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()