Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""This module defines an ASE interface to FLAPW code FLEUR.
3http://www.flapw.de
4"""
6import os
8from subprocess import Popen, PIPE
10import re
12import numpy as np
14from ase.units import Hartree, Bohr
15from ase.calculators.calculator import PropertyNotImplementedError
18class FLEUR:
19 """Class for doing FLEUR calculations.
21 In order to use fleur one has to define the following environment
22 variables:
24 FLEUR_INPGEN path to the input generator (inpgen.x) of fleur
26 FLEUR path to the fleur executable. Note that fleur uses different
27 executable for real and complex cases (systems with/without inversion
28 symmetry), so FLEUR must point to the correct executable.
30 The initialize_density step can be performed in parallel
31 only if run on one compute node. FLEUR_SERIAL is used for this step.
33 It is probable that user needs to tune manually the input file before
34 the actual calculation, so in addition to the standard
35 get_potential_energy function this class defines the following utility
36 functions:
38 write_inp
39 generate the input file *inp*
40 initialize_density
41 creates the initial density after possible manual edits of *inp*
42 calculate
43 convergence the total energy. With fleur, one specifies always
44 only the number of SCF-iterations so this function launches
45 the executable several times and monitors the convergence.
46 relax
47 Uses fleur's internal algorithm for structure
48 optimization. Requires that the proper optimization parameters
49 (atoms to optimize etc.) are specified by hand in *inp*
51 """
52 def __init__(self, xc='LDA', kpts=None, nbands=None, convergence=None,
53 width=None, kmax=None, mixer=None, maxiter=None,
54 maxrelax=20, workdir=None, equivatoms=True, rmt=None,
55 lenergy=None):
56 """Construct FLEUR-calculator object.
58 Parameters
59 ==========
60 xc: str
61 Exchange-correlation functional. Must be one of LDA, PBE,
62 RPBE.
63 kpts: list of three int
64 Monkhost-Pack sampling.
65 nbands: int
66 Number of bands. (not used at the moment)
67 convergence: dictionary
68 Convergence parameters (currently only energy in eV)
69 {'energy' : float}
70 width: float
71 Fermi-distribution width in eV.
72 kmax: float
73 Plane wave cutoff in a.u. If kmax is set then:
74 gmax = 3.0 * kmax
75 gmaxxc = int(2.5 * kmax * 10)/10. (from set_inp.f)
76 mixer: dictionary
77 Mixing parameters imix, alpha, spinf
78 {'imix' : int, 'alpha' : float, 'spinf' : float}
79 maxiter: int
80 Maximum number of SCF iterations (name in the code: itmax)
81 maxrelax: int
82 Maximum number of relaxation steps
83 workdir: str
84 Working directory for the calculation
85 equivatoms: bool
86 If False: generate inequivalent atoms (default is True).
87 Setting to False allows one for example to calculate spin-polarized dimers.
88 See http://www.flapw.de/pm/index.php?n=User-Documentation.InputFileForTheInputGenerator.
89 rmt: dictionary
90 rmt values in Angstrom., e.g: {'O': 1.1 * Bohr, 'N': -0.1}
91 Negative number with respect to the rmt set by FLEUR.
92 lenergy: float
93 Lower energy in eV. Default -1.8 * Hartree.
94 """
96 self.xc = xc
97 self.kpts = kpts
98 self.nbands = nbands
99 self.width = width
100 self.kmax = kmax
101 self.itmax_step_default = 9 # SCF steps per run (default)
102 self.itmax_step = 5 # SCF steps per run
103 assert self.itmax_step_default <= 9
104 assert self.itmax_step <= self.itmax_step_default
105 self.itmax_default = 40
106 if maxiter is None:
107 self.itmax = self.itmax_default
108 else:
109 self.itmax = maxiter
110 self.maxrelax = maxrelax
111 self.mixer = mixer
113 if convergence:
114 self.convergence = convergence
115 self.convergence['energy'] /= Hartree
116 else:
117 self.convergence = {'energy': 0.0001}
119 self.start_dir = None
120 self.workdir = workdir
121 if self.workdir:
122 self.start_dir = os.getcwd()
123 if not os.path.isdir(workdir):
124 os.mkdir(workdir)
125 else:
126 self.workdir = '.'
127 self.start_dir = '.'
129 self.equivatoms = equivatoms
131 self.rmt = rmt
132 self.lenergy = lenergy
134 self.converged = False
136 def run_executable(self, mode='fleur', executable='FLEUR'):
138 assert executable in ['FLEUR', 'FLEUR_SERIAL']
140 executable_use = executable
141 if executable == 'FLEUR_SERIAL' and not os.environ.get(executable, ''):
142 executable_use = 'FLEUR' # use FLEUR if FLEUR_SERIAL not set
143 try:
144 code_exe = os.environ[executable_use]
145 except KeyError:
146 raise RuntimeError('Please set ' + executable_use)
147 p = Popen(code_exe, shell=True, stdin=PIPE, stdout=PIPE,
148 stderr=PIPE)
149 stat = p.wait()
150 out = p.stdout.read()
151 err = p.stderr.read()
152 print(mode, ': stat= ', stat, ' out= ', out, ' err=', err)
153 # special handling of exit status from density generation and regular fleur.x
154 if mode in ['density']:
155 if '!' in err:
156 os.chdir(self.start_dir)
157 raise RuntimeError(executable_use + ' exited with a code %s' % err)
158 else:
159 if stat != 0:
160 os.chdir(self.start_dir)
161 raise RuntimeError(executable_use + ' exited with a code %d' % stat)
163 def update(self, atoms):
164 """Update a FLEUR calculation."""
166 if (not self.converged or
167 len(self.numbers) != len(atoms) or
168 (self.numbers != atoms.get_atomic_numbers()).any()):
169 self.initialize(atoms)
170 self.calculate(atoms)
171 elif ((self.positions != atoms.get_positions()).any() or
172 (self.pbc != atoms.get_pbc()).any() or
173 (self.cell != atoms.get_cell()).any()):
174 self.converged = False
175 self.initialize(atoms)
176 self.calculate(atoms)
178 def initialize(self, atoms):
179 """Create an input file inp and generate starting density."""
181 self.converged = False
182 self.initialize_inp(atoms)
183 self.initialize_density(atoms)
185 def initialize_inp(self, atoms):
186 """Create a inp file"""
187 os.chdir(self.workdir)
189 self.numbers = atoms.get_atomic_numbers().copy()
190 self.positions = atoms.get_positions().copy()
191 self.cell = atoms.get_cell().copy()
192 self.pbc = atoms.get_pbc().copy()
194 # create the input
195 self.write_inp(atoms)
197 os.chdir(self.start_dir)
199 def initialize_density(self, atoms):
200 """Creates a new starting density."""
202 os.chdir(self.workdir)
203 # remove possible conflicting files
204 files2remove = ['cdn1', 'fl7para', 'stars', 'wkf2', 'enpara',
205 'kpts', 'broyd', 'broyd.7', 'tmat', 'tmas']
206 if 0:
207 # avoid STOP bzone3 error by keeping the kpts file
208 files2remove.remove('kpts')
210 for f in files2remove:
211 if os.path.isfile(f):
212 os.remove(f)
214 # generate the starting density
215 os.system("sed -i -e 's/strho=./strho=T/' inp")
216 self.run_executable(mode='density', executable='FLEUR_SERIAL')
217 os.system("sed -i -e 's/strho=./strho=F/' inp")
219 os.chdir(self.start_dir)
220 # generate spin-polarized density
221 # http://www.flapw.de/pm/index.php?n=User-Documentation.Magnetism
222 if atoms.get_initial_magnetic_moments().sum() > 0.0:
223 os.chdir(self.workdir)
224 # generate cdnc file (1 SCF step: swsp=F - non-magnetic)
225 os.system("sed -i -e 's/itmax=.*,maxiter/itmax= 1,maxiter/' inp")
226 self.run_executable(mode='cdnc', executable='FLEUR')
227 sedline = "'s/itmax=.*,maxiter/itmax= '"
228 sedline += str(self.itmax_step_default) + "',maxiter/'"
229 os.system("sed -i -e " + sedline + " inp")
230 # generate spin polarized density (swsp=T)
231 os.system("sed -i -e 's/swsp=./swsp=T/' inp")
232 self.run_executable(mode='swsp', executable='FLEUR_SERIAL')
233 # restore swsp=F
234 os.system("sed -i -e 's/swsp=./swsp=F/' inp")
235 os.chdir(self.start_dir)
237 def get_potential_energy(self, atoms, force_consistent=False):
238 self.update(atoms)
240 if force_consistent:
241 return self.efree * Hartree
242 else:
243 # Energy extrapolated to zero Kelvin:
244 return (self.etotal + self.efree) / 2 * Hartree
246 def get_number_of_iterations(self, atoms):
247 self.update(atoms)
248 return self.niter
250 def get_forces(self, atoms):
251 self.update(atoms)
252 # electronic structure is converged, so let's calculate forces:
253 # TODO
254 return np.array((0.0, 0.0, 0.0))
256 def get_stress(self, atoms):
257 raise PropertyNotImplementedError
259 def get_dipole_moment(self, atoms):
260 """Returns total dipole moment of the system."""
261 raise PropertyNotImplementedError
263 def calculate(self, atoms):
264 """Converge a FLEUR calculation to self-consistency.
266 Input files should be generated before calling this function
267 FLEUR performs always fixed number of SCF steps. This function
268 reduces the number of iterations gradually, however, a minimum
269 of five SCF steps is always performed.
270 """
272 os.chdir(self.workdir)
274 self.niter = 0
275 out = ''
276 err = ''
277 while not self.converged:
278 if self.niter > self.itmax:
279 os.chdir(self.start_dir)
280 raise RuntimeError('FLEUR failed to convergence in %d iterations' % self.itmax)
282 self.run_executable(mode='fleur', executable='FLEUR')
284 # catenate new output with the old one
285 os.system('cat out >> out.old')
286 self.read()
287 self.check_convergence()
289 if os.path.exists('out.old'):
290 os.rename('out.old', 'out')
291 # After convergence clean up broyd* files
292 os.system('rm -f broyd*')
293 os.chdir(self.start_dir)
294 return out, err
296 def relax(self, atoms):
297 """Currently, user has to manually define relaxation parameters
298 (atoms to relax, relaxation directions, etc.) in inp file
299 before calling this function."""
301 nrelax = 0
302 relaxed = False
303 while not relaxed:
304 # Calculate electronic structure
305 self.calculate(atoms)
306 # Calculate the Pulay forces
307 os.system("sed -i -e 's/l_f=./l_f=T/' inp")
308 while True:
309 self.converged = False
310 out, err = self.calculate(atoms)
311 if 'GEO new' in err:
312 os.chdir(self.workdir)
313 os.rename('inp_new', 'inp')
314 os.chdir(self.start_dir)
315 break
316 if 'GEO: Des woas' in err:
317 relaxed = True
318 break
319 nrelax += 1
320 # save the out and cdn1 files
321 os.system('cp out out_%d' % nrelax)
322 os.system('cp cdn1 cdn1_%d' % nrelax)
323 if nrelax > self.maxrelax:
324 os.chdir(self.start_dir)
325 raise RuntimeError('Failed to relax in %d iterations' % self.maxrelax)
326 self.converged = False
328 def write_inp(self, atoms):
329 """Write the *inp* input file of FLEUR.
331 First, the information from Atoms is written to the simple input
332 file and the actual input file *inp* is then generated with the
333 FLEUR input generator. The location of input generator is specified
334 in the environment variable FLEUR_INPGEN.
336 Finally, the *inp* file is modified according to the arguments of
337 the FLEUR calculator object.
338 """
340 with open('inp_simple', 'w') as fh:
341 self._write_inp(atoms, fh)
343 def _write_inp(self, atoms, fh):
344 fh.write('FLEUR input generated with ASE\n')
345 fh.write('\n')
347 if atoms.pbc[2]:
348 film = 'f'
349 else:
350 film = 't'
351 fh.write('&input film=%s /' % film)
352 fh.write('\n')
354 for vec in atoms.get_cell():
355 fh.write(' ')
356 for el in vec:
357 fh.write(' %21.16f' % (el/Bohr))
358 fh.write('\n')
359 fh.write(' %21.16f\n' % 1.0)
360 fh.write(' %21.16f %21.16f %21.16f\n' % (1.0, 1.0, 1.0))
361 fh.write('\n')
363 natoms = len(atoms)
364 fh.write(' %6d\n' % natoms)
365 positions = atoms.get_scaled_positions()
366 if not atoms.pbc[2]:
367 # in film calculations z position has to be in absolute
368 # coordinates and symmetrical
369 cart_pos = atoms.get_positions()
370 cart_pos[:, 2] -= atoms.get_cell()[2, 2]/2.0
371 positions[:, 2] = cart_pos[:, 2] / Bohr
372 atomic_numbers = atoms.get_atomic_numbers()
373 for n, (Z, pos) in enumerate(zip(atomic_numbers, positions)):
374 if self.equivatoms:
375 fh.write('%3d' % Z)
376 else:
377 # generate inequivalent atoms, by using non-integer Z
378 # (only the integer part will be used as Z of the atom)
379 # see http://www.flapw.de/pm/index.php?n=User-Documentation.InputFileForTheInputGenerator
380 fh.write('%3d.%04d' % (Z, n)) # MDTMP don't think one can calculate more that 10**4 atoms
381 for el in pos:
382 fh.write(' %21.16f' % el)
383 fh.write('\n')
385 # avoid "STOP read_record: ERROR reading input"
386 fh.write('&end /')
388 try:
389 inpgen = os.environ['FLEUR_INPGEN']
390 except KeyError:
391 raise RuntimeError('Please set FLEUR_INPGEN')
393 # rename the previous inp if it exists
394 if os.path.isfile('inp'):
395 os.rename('inp', 'inp.bak')
396 os.system('%s -old < inp_simple' % inpgen)
398 # read the whole inp-file for possible modifications
399 with open('inp', 'r') as fh:
400 lines = fh.readlines()
402 window_ln = -1
403 for ln, line in enumerate(lines):
404 # XC potential
405 if line.startswith('pbe'):
406 if self.xc == 'PBE':
407 pass
408 elif self.xc == 'RPBE':
409 lines[ln] = 'rpbe non-relativi\n'
410 elif self.xc == 'LDA':
411 lines[ln] = 'mjw non-relativic\n'
412 del lines[ln+1]
413 else:
414 raise RuntimeError('XC-functional %s is not supported' % self.xc)
415 if line.startswith('Window'):
416 # few things are set around this line
417 window_ln = ln
418 # kmax
419 if self.kmax and ln == window_ln:
420 line = '%10.5f\n' % self.kmax
421 lines[ln+2] = line
422 # lower energy
423 if self.lenergy is not None and ln == window_ln:
424 l0 = lines[ln+1].split()[0]
425 l = lines[ln+1].replace(l0, '%8.5f' % (self.lenergy / Hartree))
426 lines[ln+1] = l
427 # gmax cutoff for PW-expansion of potential & density ( > 2*kmax)
428 # gmaxxc cutoff for PW-expansion of XC-potential ( > 2*kmax, < gmax)
429 if self.kmax and line.startswith('vchk'):
430 gmax = 3. * self.kmax
431 line = ' %10.6f %10.6f\n' % (gmax, int(2.5 * self.kmax * 10)/10.)
432 lines[ln-1] = line
433 # Fermi width
434 if self.width and line.startswith('gauss'):
435 line = 'gauss=F %7.5ftria=F\n' % (self.width / Hartree)
436 lines[ln] = line
437 # kpts
438 if self.kpts and line.startswith('nkpt'):
439 line = 'nkpt= nx=%2d,ny=%2d,nz=%2d\n' % (self.kpts[0],
440 self.kpts[1],
441 self.kpts[2])
442 lines[ln] = line
443 # itmax
444 if self.itmax < self.itmax_step_default and line.startswith('itmax'):
445 # decrease number of SCF steps; increasing is done by 'while not self.converged:'
446 lsplit = line.split(',')
447 if lsplit[0].find('itmax') != -1:
448 lsplit[0] = 'itmax=' + ('%2d' % self.itmax)
449 lines[ln] = ",".join(lsplit)
450 # Mixing
451 if self.mixer and line.startswith('itmax'):
452 imix = self.mixer['imix']
453 alpha = self.mixer['alpha']
454 spinf = self.mixer['spinf']
455 line_end = 'imix=%2d,alpha=%6.2f,spinf=%6.2f\n' % (imix,
456 alpha,
457 spinf)
458 line = line[:21] + line_end
459 lines[ln] = line
460 # jspins and swsp
461 if atoms.get_initial_magnetic_moments().sum() > 0.0:
462 assert not self.equivatoms, 'equivatoms currently not allowed in magnetic systems'
463 if line.find('jspins=1') != -1:
464 lines[ln] = line.replace('jspins=1', 'jspins=2')
465 if line.startswith('swsp=F'):
466 # setting initial magnetic moments for all atom types
467 lines[ln] = 'swsp=F'
468 for m in atoms.get_initial_magnetic_moments():
469 lines[ln] += (' %5.2f' % m)
470 lines[ln] += '\n'
471 # inpgen produces incorrect symbol 'J' for Iodine
472 if line.startswith(' J 53'):
473 lines[ln] = lines[ln].replace(' J 53', ' I 53')
474 # rmt
475 if self.rmt is not None:
476 for s in list(set(atoms.get_chemical_symbols())): # unique
477 if s in self.rmt:
478 # set the requested rmt
479 for ln, line in enumerate(lines):
480 ls = line.split()
481 if len(ls) == 7 and ls[0].strip() == s:
482 rorig = ls[5].strip()
483 if self.rmt[s] < 0.0:
484 r = float(rorig) + self.rmt[s] / Bohr
485 else:
486 r = self.rmt[s] / Bohr
487 print(s, rorig, r)
488 lines[ln] = lines[ln].replace(rorig, ("%.6f" % r))
490 # write everything back to inp
491 with open('inp', 'w') as fh:
492 for line in lines:
493 fh.write(line)
495 def read(self):
496 """Read results from FLEUR's text-output file `out`."""
498 with open('out', 'r') as fd:
499 lines = fd.readlines()
501 # total energies
502 self.total_energies = []
503 pat = re.compile(r'(.*total energy=)(\s)*([-0-9.]*)')
504 for line in lines:
505 m = pat.match(line)
506 if m:
507 self.total_energies.append(float(m.group(3)))
508 self.etotal = self.total_energies[-1]
510 # free_energies
511 self.free_energies = []
512 pat = re.compile(r'(.*free energy=)(\s)*([-0-9.]*)')
513 for line in lines:
514 m = pat.match(line)
515 if m:
516 self.free_energies.append(float(m.group(3)))
517 self.efree = self.free_energies[-1]
519 # TODO forces, charge density difference...
521 def check_convergence(self):
522 """Check the convergence of calculation"""
523 energy_error = np.ptp(self.total_energies[-3:])
524 self.converged = energy_error < self.convergence['energy']
526 # TODO check charge convergence
528 # reduce the itmax in inp
529 with open('inp', 'r') as fh:
530 lines = fh.readlines()
531 pat = re.compile('(itmax=)([ 0-9]*)')
532 with open('inp', 'w') as fh:
533 for line in lines:
534 m = pat.match(line)
535 if m:
536 itmax = int(m.group(2))
537 self.niter += itmax
538 itmax_new = itmax // 2
539 itmax = max(self.itmax_step, itmax_new)
540 line = 'itmax=%2d' % itmax + line[8:]
541 fh.write(line)