Statistiques
| Révision :

root / ase / calculators / abinit.py @ 5

Historique | Voir | Annoter | Télécharger (21,37 ko)

1
"""This module defines an ASE interface to ABINIT.
2

3
http://www.abinit.org/
4
"""
5

    
6
import os
7
from glob import glob
8
from os.path import join, isfile, islink
9

    
10
import numpy as np
11

    
12
from ase.data import chemical_symbols
13
from ase.data import atomic_numbers
14
from ase.units import Bohr, Hartree
15

    
16

    
17
class Abinit:
18
    """Class for doing ABINIT calculations.
19

20
    The default parameters are very close to those that the ABINIT
21
    Fortran code would use.  These are the exceptions::
22

23
      calc = Abinit(label='abinit', xc='LDA', pulay=5, mix=0.1)
24

25
    Use the set_inp method to set extra INPUT parameters::
26

27
      calc.set_inp('nstep', 30)
28

29
    """
30
    def __init__(self, label='abinit', xc='LDA', kpts=None, nbands=0,
31
                 width=0.04*Hartree, ecut=None, charge=0,
32
                 pulay=5, mix=0.1, pps='fhi', toldfe=1.0e-6
33
                 ):
34
        """Construct ABINIT-calculator object.
35

36
        Parameters
37
        ==========
38
        label: str
39
            Prefix to use for filenames (label.in, label.txt, ...).
40
            Default is 'abinit'.
41
        xc: str
42
            Exchange-correlation functional.  Must be one of LDA, PBE,
43
            revPBE, RPBE.
44
        kpts: list of three int
45
            Monkhost-Pack sampling.
46
        nbands: int
47
            Number of bands.
48
            Default is 0.
49
        width: float
50
            Fermi-distribution width in eV.
51
            Default is 0.04 Hartree.
52
        ecut: float
53
            Planewave cutoff energy in eV.
54
            No default.
55
        charge: float
56
            Total charge of the system.
57
            Default is 0.
58
        pulay: int
59
            Number of old densities to use for Pulay mixing.
60
        mix: float
61
            Mixing parameter between zero and one for density mixing.
62

63
        Examples
64
        ========
65
        Use default values:
66

67
        >>> h = Atoms('H', calculator=Abinit())
68
        >>> h.center(vacuum=3.0)
69
        >>> e = h.get_potential_energy()
70

71
        """
72

    
73
        if not nbands > 0:
74
            raise ValueError('Number of bands (nbands) not set')
75

    
76
        if ecut is None:
77
            raise ValueError('Planewave cutoff energy in eV (ecut) not set')
78

    
79
        self.label = label#################### != out
80
        self.xc = xc
81
        self.kpts = kpts
82
        self.nbands = nbands
83
        self.width = width
84
        self.ecut = ecut
85
        self.charge = charge
86
        self.pulay = pulay
87
        self.mix = mix
88
        self.pps = pps
89
        self.toldfe = toldfe
90
        if not pps in ['fhi', 'hgh', 'hgh.sc']:
91
            raise ValueError('Unexpected PP identifier %s' % pps)
92

    
93
        self.converged = False
94
        self.inp = {}
95
        self.n_entries_int = 20 # integer entries per line
96
        self.n_entries_float = 8 # float entries per line
97

    
98
    def update(self, atoms):
99
        if (not self.converged or
100
            len(self.numbers) != len(atoms) or
101
            (self.numbers != atoms.get_atomic_numbers()).any()):
102
            self.initialize(atoms)
103
            self.calculate(atoms)
104
        elif ((self.positions != atoms.get_positions()).any() or
105
              (self.pbc != atoms.get_pbc()).any() or
106
              (self.cell != atoms.get_cell()).any()):
107
            self.calculate(atoms)
108

    
109
    def initialize(self, atoms):
110
        self.numbers = atoms.get_atomic_numbers().copy()
111
        self.species = []
112
        for a, Z in enumerate(self.numbers):
113
            if Z not in self.species:
114
                self.species.append(Z)
115

    
116
        if 'ABINIT_PP_PATH' in os.environ:
117
            pppaths = os.environ['ABINIT_PP_PATH'].split(':')
118
        else:
119
            pppaths = []
120

    
121
        self.ppp_list = []
122
        if self.xc != 'LDA':
123
            xcname = 'GGA'
124
        else:
125
            xcname = 'LDA'
126

    
127
        for Z in self.species:
128
            symbol = chemical_symbols[abs(Z)]
129
            number = atomic_numbers[symbol]
130

    
131
            pps = self.pps
132
            if pps == 'fhi':
133
                name = '%02d-%s.%s.fhi' % (number, symbol, xcname)
134
            elif pps in ('hgh', 'hgh.sc'):
135
                hghtemplate = '%d%s.%s.hgh' # E.g. "42mo.6.hgh"
136
                # There might be multiple files with different valence
137
                # electron counts, so we must choose between
138
                # the ordinary and the semicore versions for some elements.
139
                #
140
                # Therefore we first use glob to get all relevant files,
141
                # then pick the correct one afterwards.
142
                name = hghtemplate % (number, symbol.lower(), '*')
143

    
144
            found = False
145
            for path in pppaths:
146
                if pps.startswith('hgh'):
147
                    filenames = glob(join(path, name))
148
                    if not filenames:
149
                        continue
150
                    assert len(filenames) in [0, 1, 2]
151
                    if pps == 'hgh':
152
                        selector = min # Lowest possible valence electron count
153
                    else:
154
                        assert pps == 'hgh.sc'
155
                        selector = max # Semicore - highest electron count
156
                    Z = selector([int(os.path.split(name)[1].split('.')[1])
157
                                  for name in filenames])
158
                    name = hghtemplate % (number, symbol.lower(), str(Z))
159
                filename = join(path, name)
160
                if isfile(filename) or islink(filename):
161
                    found = True
162
                    self.ppp_list.append(filename)
163
                    break
164
            if not found:
165
                raise RuntimeError('No pseudopotential for %s!' % symbol)
166

    
167
        self.converged = False
168

    
169
    def get_potential_energy(self, atoms, force_consistent=False):
170
        self.update(atoms)
171

    
172
        if force_consistent:
173
            return self.efree
174
        else:
175
            # Energy extrapolated to zero Kelvin:
176
            return  (self.etotal + self.efree) / 2
177

    
178
    def get_number_of_bands(self):
179
        return self.nbands
180

    
181
    def get_kpts_info(self, kpt=0, spin=0, mode='eigenvalues'):
182
        return self.read_kpts_info(kpt, spin, mode)
183

    
184
    def get_k_point_weights(self):
185
        return self.get_kpts_info(kpt=0, spin=0, mode='k_point_weights')
186

    
187
    def get_bz_k_points(self):
188
        raise NotImplementedError
189

    
190
    def get_ibz_k_points(self):
191
        return self.get_kpts_info(kpt=0, spin=0, mode='ibz_k_points')
192

    
193
    def get_fermi_level(self):
194
        return self.read_fermi()
195

    
196
    def get_eigenvalues(self, kpt=0, spin=0):
197
        return self.get_kpts_info(kpt, spin, 'eigenvalues')
198

    
199
    def get_occupations(self, kpt=0, spin=0):
200
        return self.get_kpts_info(kpt, spin, 'occupations')
201

    
202
    def get_forces(self, atoms):
203
        self.update(atoms)
204
        return self.forces.copy()
205

    
206
    def get_stress(self, atoms):
207
        self.update(atoms)
208
        return self.stress.copy()
209

    
210
    def calculate(self, atoms):
211
        self.positions = atoms.get_positions().copy()
212
        self.cell = atoms.get_cell().copy()
213
        self.pbc = atoms.get_pbc().copy()
214

    
215
        self.write_files()
216

    
217
        self.write_inp(atoms)
218

    
219
        abinit = os.environ['ABINIT_SCRIPT']
220
        locals = {'label': self.label}
221

    
222
        # Now, because (stupidly) abinit when it finds a name it uses nameA
223
        # and when nameA exists it uses nameB, etc.
224
        # we need to rename our *.txt file to *.txt.bak
225
        filename = self.label + '.txt'
226
        if islink(filename) or isfile(filename):
227
            os.rename(filename, filename+'.bak')
228

    
229
        execfile(abinit, {}, locals)
230
        exitcode = locals['exitcode']
231
        if exitcode != 0:
232
            raise RuntimeError(('Abinit exited with exit code: %d.  ' +
233
                                'Check %s.log for more information.') %
234
                               (exitcode, self.label))
235

    
236
        self.read()
237

    
238
        self.converged = True
239

    
240
    def write_files(self):
241
        """Write input parameters to files-file."""
242
        fh = open(self.label + '.files', 'w')
243

    
244
        import getpass
245
        #find a suitable default scratchdir (should be writeable!)
246
        username=getpass.getuser()
247

    
248
        if os.access("/scratch/"+username,os.W_OK):
249
                scratch = "/scratch/"+username
250
        elif os.access("/scratch/",os.W_OK):
251
                scratch = "/scratch/"
252
        else:
253
                if os.access(os.curdir,os.W_OK):
254
                        scratch = os.curdir #if no /scratch use curdir
255
                else:
256
                        raise IOError,"No suitable scratch directory and no write access to current dir"
257

    
258
        fh.write('%s\n' % (self.label+'.in')) # input
259
        fh.write('%s\n' % (self.label+'.txt')) # output
260
        fh.write('%s\n' % (self.label+'i')) # input
261
        fh.write('%s\n' % (self.label+'o')) # output
262
        # scratch files
263
        fh.write('%s\n' % (os.path.join(scratch, self.label+'.abinit')))
264
        # Provide the psp files
265
        for ppp in self.ppp_list:
266
            fh.write('%s\n' % (ppp)) # psp file path
267

    
268
        fh.close()
269

    
270
    def set_inp(self, key, value):
271
        """Set INPUT parameter."""
272
        self.inp[key] = value
273

    
274
    def write_inp(self, atoms):
275
        """Write input parameters to in-file."""
276
        fh = open(self.label + '.in', 'w')
277

    
278
        inp = {
279
            #'SystemLabel': self.label,
280
            #'LatticeConstant': 1.0,
281
            'natom': len(atoms),
282
            'charge': self.charge,
283
            'nband': self.nbands,
284
            #'DM.UseSaveDM': self.converged,
285
            #'SolutionMethod': 'diagon',
286
            'npulayit': self.pulay, # default 7
287
            'diemix': self.mix
288
            }
289

    
290
        if self.ecut is not None:
291
            inp['ecut'] = str(self.ecut)+' eV' # default Ha
292

    
293
        if self.width is not None:
294
            inp['tsmear'] = str(self.width)+' eV' # default Ha
295
            fh.write('occopt 3 # Fermi-Dirac smearing\n')
296

    
297
        inp['ixc'] = { # default 1
298
            'LDA':     7,
299
            'PBE':    11,
300
            'revPBE': 14,
301
            'RPBE':   15,
302
            'WC':     23
303
            }[self.xc]
304

    
305
        magmoms = atoms.get_initial_magnetic_moments()
306
        if magmoms.any():
307
            inp['nsppol'] = 2
308
            fh.write('spinat\n')
309
            for n, M in enumerate(magmoms):
310
                if M != 0:
311
                    fh.write('%.14f %.14f %.14f\n' % (0, 0, M))
312
        else:
313
            inp['nsppol'] = 1
314
            #fh.write('spinat\n')
315
            #for n, M in enumerate(magmoms):
316
            #    if M != 0:
317
            #        fh.write('%.14f\n' % (M))
318

    
319
        inp.update(self.inp)
320

    
321
        for key, value in inp.items():
322
            if value is None:
323
                continue
324

    
325
            if isinstance(value, list):
326
                fh.write('%block %s\n' % key)
327
                for line in value:
328
                    fh.write(' '.join(['%s' % x for x in line]) + '\n')
329
                fh.write('%endblock %s\n' % key)
330

    
331
            unit = keys_with_units.get(inpify(key))
332
            if unit is None:
333
                fh.write('%s %s\n' % (key, value))
334
            else:
335
                if 'fs**2' in unit:
336
                    value /= fs**2
337
                elif 'fs' in unit:
338
                    value /= fs
339
                fh.write('%s %f %s\n' % (key, value, unit))
340

    
341
        fh.write('#Definition of the unit cell\n')
342
        fh.write('acell\n')
343
        fh.write('%.14f %.14f %.14f Angstrom\n' %  (1.0, 1.0, 1.0))
344
        fh.write('rprim\n')
345
        for v in self.cell:
346
            fh.write('%.14f %.14f %.14f\n' %  tuple(v))
347

    
348
        fh.write('chkprim 0 # Allow non-primitive cells\n')
349

    
350
        fh.write('#Definition of the atom types\n')
351
        fh.write('ntypat %d\n' % (len(self.species)))
352
        fh.write('znucl')
353
        for n, Z in enumerate(self.species):
354
            fh.write(' %d' % (Z))
355
        fh.write('\n')
356
        fh.write('#Enumerate different atomic species\n')
357
        fh.write('typat')
358
        fh.write('\n')
359
        self.types = []
360
        for Z in self.numbers:
361
            for n, Zs in enumerate(self.species):
362
                if Z == Zs:
363
                    self.types.append(n+1)
364
        for n, type in enumerate(self.types):
365
            fh.write(' %d' % (type))
366
            if n > 1 and ((n % self.n_entries_int) == 1):
367
                fh.write('\n')
368
        fh.write('\n')
369

    
370
        fh.write('#Definition of the atoms\n')
371
        fh.write('xangst\n')
372
        a = 0
373
        for pos, Z in zip(self.positions, self.numbers):
374
            a += 1
375
            fh.write('%.14f %.14f %.14f\n' %  tuple(pos))
376

    
377
        if self.kpts is not None:
378
            fh.write('kptopt %d\n' % (1))
379
            fh.write('ngkpt ')
380
            fh.write('%d %d %d\n' %  tuple(self.kpts))
381

    
382
        fh.write('#Definition of the SCF procedure\n')
383
        fh.write('toldfe %.1g\n' %  self.toldfe)
384
        fh.write('chkexit 1 # abinit.exit file in the running directory terminates after the current SCF\n')
385

    
386
        fh.close()
387

    
388
    def read_fermi(self):
389
        """Method that reads Fermi energy in Hartree from the output file
390
        and returns it in eV"""
391
        E_f=None
392
        filename = self.label + '.txt'
393
        text = open(filename).read().lower()
394
        assert 'error' not in text
395
        for line in iter(text.split('\n')):
396
            if line.rfind('fermi (or homo) energy (hartree) =') > -1:
397
                E_f = float(line.split('=')[1].strip().split()[0])
398
        return E_f*Hartree
399

    
400
    def read_kpts_info(self, kpt=0, spin=0, mode='eigenvalues'):
401
        """ Returns list of eigenvalues, occupations, kpts weights, or
402
        kpts coordinates for given kpt and spin"""
403
        # output may look like this (or without occupation entries); 8 entries per line:
404
        #
405
        #  Eigenvalues (hartree) for nkpt=  20  k points:
406
        # kpt#   1, nband=  3, wtk=  0.01563, kpt=  0.0625  0.0625  0.0625 (reduced coord)
407
        #  -0.09911   0.15393   0.15393
408
        #      occupation numbers for kpt#   1
409
        #   2.00000   0.00000   0.00000
410
        # kpt#   2, nband=  3, wtk=  0.04688, kpt=  0.1875  0.0625  0.0625 (reduced coord)
411
        # ...
412
        #
413
        assert mode in ['eigenvalues' , 'occupations', 'ibz_k_points', 'k_point_weights'], 'mode not in [\'eigenvalues\' , \'occupations\', \'ibz_k_points\', \'k_point_weights\']'
414
        # number of lines of eigenvalues/occupations for a kpt
415
        n_entry_lines = max(1, int(self.nbands/self.n_entries_float))
416
        #
417
        filename = self.label + '.txt'
418
        text = open(filename).read().lower()
419
        assert 'error' not in text
420
        lines = iter(text.split('\n'))
421
        text_list = []
422
        # find the begining line of eigenvalues
423
        contains_eigenvalues = False
424
        for line in lines:
425
            #if line.rfind('eigenvalues (hartree) for nkpt') > -1: #MDTMP
426
            if line.rfind('eigenvalues (   ev  ) for nkpt') > -1:
427
                n_kpts = int(line.split('nkpt=')[1].strip().split()[0])
428
                contains_eigenvalues = True
429
                break
430
        # find the end line of eigenvalues starting from linenum
431
        for line in lines:
432
            text_list.append(line)
433
            if not line.strip(): # find a blank line
434
                break
435
        # remove last (blank) line
436
        text_list = text_list[:-1]
437

    
438
        assert contains_eigenvalues, 'No eigenvalues found in the output'
439

    
440
        # join text eigenvalues description with eigenvalues
441
        # or occupation numbers for kpt# with occupations
442
        contains_occupations = False
443
        for line in text_list:
444
            if line.rfind('occupation numbers') > -1:
445
                contains_occupations = True
446
                break
447
        if mode == 'occupations':
448
            assert contains_occupations, 'No occupations found in the output'
449

    
450
        if contains_occupations:
451
            range_kpts = 2*n_kpts
452
        else:
453
            range_kpts = n_kpts
454
        #
455
        values_list = []
456
        offset = 0
457
        for kpt_entry in range(range_kpts):
458
            full_line = ''
459
            for entry_line in range(n_entry_lines+1):
460
                full_line = full_line+str(text_list[offset+entry_line])
461
            first_line = text_list[offset]
462
            if mode == 'occupations':
463
                if first_line.rfind('occupation numbers') > -1:
464
                    # extract numbers
465
                    full_line = [float(v) for v in full_line.split('#')[1].strip().split()[1:]]
466
                    values_list.append(full_line)
467
            elif mode in ['eigenvalues', 'ibz_k_points', 'k_point_weights']:
468
                if first_line.rfind('reduced coord') > -1:
469
                    # extract numbers
470
                    if mode == 'eigenvalues':
471
                        #full_line = [Hartree*float(v) for v in full_line.split(')')[1].strip().split()[:]] # MDTMP
472
                        full_line = [float(v) for v in full_line.split(')')[1].strip().split()[:]]
473
                    elif mode == 'ibz_k_points':
474
                        full_line = [float(v) for v in full_line.split('kpt=')[1].strip().split('(')[0].split()]
475
                    else:
476
                        full_line = float(full_line.split('wtk=')[1].strip().split(',')[0].split()[0])
477
                    values_list.append(full_line)
478
            offset = offset+n_entry_lines+1
479
        #
480
        if mode in ['occupations', 'eigenvalues']:
481
            return np.array(values_list[kpt])
482
        else:
483
            return np.array(values_list)
484

    
485
    def read(self):
486
        """Read results from ABINIT's text-output file."""
487
        filename = self.label + '.txt'
488
        text = open(filename).read().lower()
489
        assert 'error' not in text
490
        assert 'was not enough scf cycles to converge' not in text
491
        # some consistency ckecks
492
        for line in iter(text.split('\n')):
493
            if line.rfind('natom  ') > -1:
494
                natom = int(line.split()[-1])
495
                assert natom == len(self.numbers)
496
        for line in iter(text.split('\n')):
497
            if line.rfind('znucl  ') > -1:
498
                znucl = [float(Z) for Z in line.split()[1:]]
499
                for n, Z in enumerate(self.species):
500
                    assert Z == znucl[n]
501
        lines = text.split('\n')
502
        for n, line in enumerate(lines):
503
            if line.rfind(' typat  ') > -1:
504
                nlines = len(self.numbers) / self.n_entries_int
505
                typat = [int(t) for t in line.split()[1:]] # first line
506
                for nline in range(nlines): # remaining lines
507
                    for t in lines[1 + n + nline].split()[:]:
508
                        typat.append(int(t))
509
        for n, t in enumerate(self.types):
510
            assert t == typat[n]
511

    
512
        lines = iter(text.split('\n'))
513
        # Stress:
514
        # Printed in the output in the following format [Hartree/Bohr^3]:
515
        # sigma(1 1)=  4.02063464E-04  sigma(3 2)=  0.00000000E+00
516
        # sigma(2 2)=  4.02063464E-04  sigma(3 1)=  0.00000000E+00
517
        # sigma(3 3)=  4.02063464E-04  sigma(2 1)=  0.00000000E+00
518
        for line in lines:
519
            if line.rfind('cartesian components of stress tensor (hartree/bohr^3)') > -1:
520
                self.stress = np.empty((3, 3))
521
                for i in range(3):
522
                    entries = lines.next().split()
523
                    self.stress[i,i] = float(entries[2])
524
                    self.stress[min(3, 4-i)-1, max(1, 2-i)-1] = float(entries[5])
525
                    self.stress[max(1, 2-i)-1, min(3, 4-i)-1] = float(entries[5])
526
                self.stress = self.stress*Hartree/Bohr**3
527
                break
528
        else:
529
            raise RuntimeError
530

    
531
        # Energy [Hartree]:
532
        # Warning: Etotal could mean both electronic energy and free energy!
533
        for line in iter(text.split('\n')):
534
            if line.rfind('>>>>> internal e=') > -1:
535
                self.etotal = float(line.split('=')[-1])*Hartree
536
                for line1 in iter(text.split('\n')):
537
                    if line1.rfind('>>>>>>>>> etotal=') > -1:
538
                        self.efree = float(line1.split('=')[-1])*Hartree
539
                        break
540
                else:
541
                    raise RuntimeError
542
                break
543
        else:
544
            for line2 in iter(text.split('\n')):
545
                if line2.rfind('>>>>>>>>> etotal=') > -1:
546
                    self.etotal = float(line2.split('=')[-1])*Hartree
547
                    self.efree = self.etotal
548
                    break
549
            else:
550
                raise RuntimeError
551

    
552
        # Forces:
553
        for line in lines:
554
            if line.rfind('cartesian forces (ev/angstrom) at end:') > -1:
555
                forces = []
556
                for i in range(len(self.numbers)):
557
                    forces.append(np.array([float(f) for f in lines.next().split()[1:]]))
558
                self.forces = np.array(forces)
559
                break
560
        else:
561
            raise RuntimeError
562

    
563
def inpify(key):
564
    return key.lower().replace('_', '').replace('.', '').replace('-', '')
565

    
566

    
567
keys_with_units = {
568
    }
569
#keys_with_units = {
570
#    'paoenergyshift': 'eV',
571
#    'zmunitslength': 'Bohr',
572
#    'zmunitsangle': 'rad',
573
#    'zmforcetollength': 'eV/Ang',
574
#    'zmforcetolangle': 'eV/rad',
575
#    'zmmaxdispllength': 'Ang',
576
#    'zmmaxdisplangle': 'rad',
577
#    'ecut': 'eV',
578
#    'dmenergytolerance': 'eV',
579
#    'electronictemperature': 'eV',
580
#    'oneta': 'eV',
581
#    'onetaalpha': 'eV',
582
#    'onetabeta': 'eV',
583
#    'onrclwf': 'Ang',
584
#    'onchemicalpotentialrc': 'Ang',
585
#    'onchemicalpotentialtemperature': 'eV',
586
#    'mdmaxcgdispl': 'Ang',
587
#    'mdmaxforcetol': 'eV/Ang',
588
#    'mdmaxstresstol': 'eV/Ang**3',
589
#    'mdlengthtimestep': 'fs',
590
#    'mdinitialtemperature': 'eV',
591
#    'mdtargettemperature': 'eV',
592
#    'mdtargetpressure': 'eV/Ang**3',
593
#    'mdnosemass': 'eV*fs**2',
594
#    'mdparrinellorahmanmass': 'eV*fs**2',
595
#    'mdtaurelax': 'fs',
596
#    'mdbulkmodulus': 'eV/Ang**3',
597
#    'mdfcdispl': 'Ang',
598
#    'warningminimumatomicdistance': 'Ang',
599
#    'rcspatial': 'Ang',
600
#    'kgridcutoff': 'Ang',
601
#    'latticeconstant': 'Ang'}