Hide keyboard shortcuts

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 FHI-aims. 

2 

3Felix Hanke hanke@liverpool.ac.uk 

4Jonas Bjork j.bjork@liverpool.ac.uk 

5Simon P. Rittmeyer simon.rittmeyer@tum.de 

6""" 

7 

8import os 

9import warnings 

10import time 

11from typing import Optional 

12import re 

13 

14import numpy as np 

15 

16from ase.units import Hartree 

17from ase.io.aims import write_aims, read_aims 

18from ase.data import atomic_numbers 

19from ase.calculators.calculator import FileIOCalculator, Parameters, kpts2mp, \ 

20 ReadError, PropertyNotImplementedError 

21 

22 

23def get_aims_version(string): 

24 match = re.search(r'\s*FHI-aims version\s*:\s*(\S+)', string, re.M) 

25 return match.group(1) 

26 

27 

28float_keys = [ 

29 'charge', 

30 'charge_mix_param', 

31 'default_initial_moment', 

32 'fixed_spin_moment', 

33 'hartree_convergence_parameter', 

34 'harmonic_length_scale', 

35 'ini_linear_mix_param', 

36 'ini_spin_mix_parma', 

37 'initial_moment', 

38 'MD_MB_init', 

39 'MD_time_step', 

40 'prec_mix_param', 

41 'set_vacuum_level', 

42 'spin_mix_param', 

43] 

44 

45exp_keys = [ 

46 'basis_threshold', 

47 'occupation_thr', 

48 'sc_accuracy_eev', 

49 'sc_accuracy_etot', 

50 'sc_accuracy_forces', 

51 'sc_accuracy_rho', 

52 'sc_accuracy_stress', 

53] 

54 

55string_keys = [ 

56 'communication_type', 

57 'density_update_method', 

58 'KS_method', 

59 'mixer', 

60 'output_level', 

61 'packed_matrix_format', 

62 'relax_unit_cell', 

63 'restart', 

64 'restart_read_only', 

65 'restart_write_only', 

66 'spin', 

67 'total_energy_method', 

68 'qpe_calc', 

69 'xc', 

70 'species_dir', 

71 'run_command', 

72 'plus_u', 

73] 

74 

75int_keys = [ 

76 'empty_states', 

77 'ini_linear_mixing', 

78 'max_relaxation_steps', 

79 'max_zeroin', 

80 'multiplicity', 

81 'n_max_pulay', 

82 'sc_iter_limit', 

83 'walltime', 

84] 

85 

86bool_keys = [ 

87 'collect_eigenvectors', 

88 'compute_forces', 

89 'compute_kinetic', 

90 'compute_numerical_stress', 

91 'compute_analytical_stress', 

92 'compute_heat_flux', 

93 'distributed_spline_storage', 

94 'evaluate_work_function', 

95 'final_forces_cleaned', 

96 'hessian_to_restart_geometry', 

97 'load_balancing', 

98 'MD_clean_rotations', 

99 'MD_restart', 

100 'override_illconditioning', 

101 'override_relativity', 

102 'restart_relaxations', 

103 'squeeze_memory', 

104 'symmetry_reduced_k_grid', 

105 'use_density_matrix', 

106 'use_dipole_correction', 

107 'use_local_index', 

108 'use_logsbt', 

109 'vdw_correction_hirshfeld', 

110] 

111 

112list_keys = [ 

113 'init_hess', 

114 'k_grid', 

115 'k_offset', 

116 'MD_run', 

117 'MD_schedule', 

118 'MD_segment', 

119 'mixer_threshold', 

120 'occupation_type', 

121 'output', 

122 'cube', 

123 'preconditioner', 

124 'relativistic', 

125 'relax_geometry', 

126] 

127 

128 

129class Aims(FileIOCalculator): 

130 # was "command" before the refactoring to dynamical commands 

131 __command_default = 'aims.version.serial.x > aims.out' 

132 __outfilename_default = 'aims.out' 

133 

134 implemented_properties = ['energy', 'forces', 'stress', 'stresses', 

135 'dipole', 'magmom'] 

136 

137 def __init__(self, restart=None, 

138 ignore_bad_restart_file=FileIOCalculator._deprecated, 

139 label=os.curdir, atoms=None, cubes=None, radmul=None, 

140 tier=None, aims_command=None, 

141 outfilename=None, **kwargs): 

142 """Construct the FHI-aims calculator. 

143 

144 The keyword arguments (kwargs) can be one of the ASE standard 

145 keywords: 'xc', 'kpts' and 'smearing' or any of FHI-aims' 

146 native keywords. 

147 

148 .. note:: The behavior of command/run_command has been refactored ase X.X.X 

149 It is now possible to independently specify the command to call 

150 FHI-aims and the outputfile into which stdout is directed. In 

151 general, we replaced 

152 

153 <run_command> = <aims_command> + " > " + <outfilename 

154 

155 That is,what used to be, e.g., 

156 

157 >>> calc = Aims(run_command = "mpiexec -np 4 aims.x > aims.out") 

158 

159 can now be achieved with the two arguments 

160 

161 >>> calc = Aims(aims_command = "mpiexec -np 4 aims.x" 

162 >>> outfilename = "aims.out") 

163 

164 Backward compatibility, however, is provided. Also, the command 

165 actually used to run FHI-aims is dynamically updated (i.e., the 

166 "command" member variable). That is, e.g., 

167 

168 >>> calc = Aims() 

169 >>> print(calc.command) 

170 aims.version.serial.x > aims.out 

171 >>> calc.outfilename = "systemX.out" 

172 >>> print(calc.command) 

173 aims.version.serial.x > systemX.out 

174 >>> calc.aims_command = "mpiexec -np 4 aims.version.scalapack.mpi.x" 

175 >>> print(calc.command) 

176 mpiexec -np 4 aims.version.scalapack.mpi > systemX.out 

177 

178 

179 Arguments: 

180 

181 cubes: AimsCube object 

182 Cube file specification. 

183 

184 radmul: int 

185 Set radial multiplier for the basis set of all atomic species. 

186 

187 tier: int or array of ints 

188 Set basis set tier for all atomic species. 

189 

190 aims_command : str 

191 The full command as executed to run FHI-aims *without* the 

192 redirection to stdout. For instance "mpiexec -np 4 aims.x". Note 

193 that this is not the same as "command" or "run_command". 

194 .. note:: Added in ase X.X.X 

195 

196 outfilename : str 

197 The file (incl. path) to which stdout is redirected. Defaults to 

198 "aims.out" 

199 .. note:: Added in ase X.X.X 

200 

201 run_command : str, optional (default=None) 

202 Same as "command", see FileIOCalculator documentation. 

203 .. note:: Deprecated in ase X.X.X 

204 

205 outfilename : str, optional (default=aims.out) 

206 File into which the stdout of the FHI aims run is piped into. Note 

207 that this will be only of any effect, if the <run_command> does not 

208 yet contain a '>' directive. 

209 plus_u : dict 

210 For DFT+U. Adds a +U term to one specific shell of the species. 

211 

212 kwargs : dict 

213 Any of the base class arguments. 

214 

215 """ 

216 # yes, we pop the key and run it through our legacy filters 

217 command = kwargs.pop('command', None) 

218 

219 # Check for the "run_command" (deprecated keyword) 

220 # Consistently, the "command" argument should be used as suggested by the FileIO base class. 

221 # For legacy reasons, however, we here also accept "run_command" 

222 run_command = kwargs.pop('run_command', None) 

223 if run_command: 

224 # this warning is debatable... in my eyes it is more consistent to 

225 # use 'command' 

226 warnings.warn('Argument "run_command" is deprecated and will be replaced with "command". Alternatively, use "aims_command" and "outfile". See documentation for more details.') 

227 if command: 

228 warnings.warn('Caution! Argument "command" overwrites "run_command.') 

229 else: 

230 command = run_command 

231 

232 # this is the fallback to the default value for empty init 

233 if np.all([i is None for i in (command, aims_command, outfilename)]): 

234 # we go for the FileIOCalculator default way (env variable) with the former default as fallback 

235 command = os.environ.get('ASE_AIMS_COMMAND', Aims.__command_default) 

236 

237 # filter the command and set the member variables "aims_command" and "outfilename" 

238 self.__init_command(command=command, 

239 aims_command=aims_command, 

240 outfilename=outfilename) 

241 

242 FileIOCalculator.__init__(self, restart, ignore_bad_restart_file, 

243 label, atoms, 

244 # well, this is not nice, but cannot work around it... 

245 command=self.command, 

246 **kwargs) 

247 

248 self.cubes = cubes 

249 self.radmul = radmul 

250 self.tier = tier 

251 

252 # handling the filtering for dynamical commands with properties, 

253 @property # type: ignore 

254 def command(self) -> Optional[str]: # type: ignore 

255 return self.__command 

256 

257 @command.setter 

258 def command(self, x): 

259 self.__update_command(command=x) 

260 

261 @property 

262 def aims_command(self): 

263 return self.__aims_command 

264 

265 @aims_command.setter 

266 def aims_command(self, x): 

267 self.__update_command(aims_command=x) 

268 

269 @property 

270 def outfilename(self): 

271 return self.__outfilename 

272 

273 @outfilename.setter 

274 def outfilename(self, x): 

275 self.__update_command(outfilename=x) 

276 

277 def __init_command(self, command=None, aims_command=None, 

278 outfilename=None): 

279 """ 

280 Create the private variables for which properties are defines and set 

281 them accordingly. 

282 """ 

283 # new class variables due to dynamical command handling 

284 self.__aims_command = None 

285 self.__outfilename = None 

286 self.__command: Optional[str] = None 

287 

288 # filter the command and set the member variables "aims_command" and "outfilename" 

289 self.__update_command(command=command, 

290 aims_command=aims_command, 

291 outfilename=outfilename) 

292 

293 # legacy handling of the (run_)command behavior a.k.a. a universal setter routine 

294 def __update_command(self, command=None, aims_command=None, 

295 outfilename=None): 

296 """ 

297 Abstracted generic setter routine for a dynamic behavior of "command". 

298 

299 The command that is actually called on the command line and enters the 

300 base class, is <command> = <aims_command> > <outfilename>. 

301 

302 This new scheme has been introduced in order to conveniently change the 

303 outfile name from the outside while automatically updating the 

304 <command> member variable. 

305 

306 Obiously, changing <command> conflicts with changing <aims_command> 

307 and/or <outfilename>, which thus raises a <ValueError>. This should, 

308 however, not happen if this routine is not used outside the property 

309 definitions. 

310 

311 Parameters 

312 ---------- 

313 command : str 

314 The full command as executed to run FHI-aims. This includes 

315 any potential mpiexec call, as well as the redirection of stdout. 

316 For instance "mpiexec -np 4 aims.x > aims.out". 

317 

318 aims_command : str 

319 The full command as executed to run FHI-aims *without* the 

320 redirection to stdout. For instance "mpiexec -np 4 aims.x" 

321 

322 outfilename : str 

323 The file (incl. path) to which stdout is redirected. 

324 """ 

325 # disentangle the command if given 

326 if command: 

327 if aims_command: 

328 raise ValueError('Cannot specify "command" and "aims_command" simultaneously.') 

329 if outfilename: 

330 raise ValueError('Cannot specify "command" and "outfilename" simultaneously.') 

331 

332 # check if the redirection of stdout is included 

333 command_spl = command.split('>') 

334 if len(command_spl) > 1: 

335 self.__aims_command = command_spl[0].strip() 

336 self.__outfilename = command_spl[-1].strip() 

337 else: 

338 # this should not happen if used correctly 

339 # but just to ensure legacy behavior of how "run_command" was handled 

340 self.__aims_command = command.strip() 

341 self.__outfilename = Aims.__outfilename_default 

342 else: 

343 if aims_command is not None: 

344 self.__aims_command = aims_command 

345 elif outfilename is None: 

346 # nothing to do here, empty call with 3x None 

347 return 

348 if outfilename is not None: 

349 self.__outfilename = outfilename 

350 else: 

351 # default to 'aims.out' 

352 if not self.outfilename: 

353 self.__outfilename = Aims.__outfilename_default 

354 

355 self.__command = '{0:s} > {1:s}'.format(self.aims_command, 

356 self.outfilename) 

357 

358 def set_atoms(self, atoms): 

359 self.atoms = atoms 

360 

361 def set_label(self, label, update_outfilename=False): 

362 msg = "Aims.set_label is not supported anymore, please use `directory`" 

363 raise RuntimeError(msg) 

364 

365 @property 

366 def out(self): 

367 return os.path.join(self.label, self.outfilename) 

368 

369 def check_state(self, atoms): 

370 system_changes = FileIOCalculator.check_state(self, atoms) 

371 # Ignore unit cell for molecules: 

372 if not atoms.pbc.any() and 'cell' in system_changes: 

373 system_changes.remove('cell') 

374 return system_changes 

375 

376 def set(self, **kwargs): 

377 xc = kwargs.get('xc') 

378 if xc: 

379 kwargs['xc'] = {'LDA': 'pw-lda', 'PBE': 'pbe'}.get(xc, xc) 

380 

381 changed_parameters = FileIOCalculator.set(self, **kwargs) 

382 

383 if changed_parameters: 

384 self.reset() 

385 return changed_parameters 

386 

387 def write_input(self, atoms, properties=None, system_changes=None, 

388 ghosts=None, geo_constrain=None, scaled=None, velocities=None): 

389 FileIOCalculator.write_input(self, atoms, properties, system_changes) 

390 

391 if geo_constrain is None: 

392 geo_constrain = "relax_geometry" in self.parameters 

393 

394 if scaled is None: 

395 scaled = np.all(atoms.get_pbc()) 

396 if velocities is None: 

397 velocities = atoms.has('momenta') 

398 

399 have_lattice_vectors = atoms.pbc.any() 

400 have_k_grid = ('k_grid' in self.parameters or 

401 'kpts' in self.parameters) 

402 if have_lattice_vectors and not have_k_grid: 

403 raise RuntimeError('Found lattice vectors but no k-grid!') 

404 if not have_lattice_vectors and have_k_grid: 

405 raise RuntimeError('Found k-grid but no lattice vectors!') 

406 write_aims( 

407 os.path.join(self.directory, 'geometry.in'), 

408 atoms, 

409 scaled, 

410 geo_constrain, 

411 velocities=velocities, 

412 ghosts=ghosts 

413 ) 

414 self.write_control(atoms, os.path.join(self.directory, 'control.in')) 

415 self.write_species(atoms, os.path.join(self.directory, 'control.in')) 

416 self.parameters.write(os.path.join(self.directory, 'parameters.ase')) 

417 

418 def prepare_input_files(self): 

419 """ 

420 Wrapper function to prepare input filesi, e.g., to a run on a remote 

421 machine 

422 """ 

423 if self.atoms is None: 

424 raise ValueError('No atoms object attached') 

425 self.write_input(self.atoms) 

426 

427 def write_control(self, atoms, filename, debug=False): 

428 lim = '#' + '='*79 

429 output = open(filename, 'w') 

430 output.write(lim + '\n') 

431 for line in ['FHI-aims file: ' + filename, 

432 'Created using the Atomic Simulation Environment (ASE)', 

433 time.asctime(), 

434 ]: 

435 output.write('# ' + line + '\n') 

436 if debug: 

437 output.write('# \n# List of parameters used to initialize the calculator:',) 

438 for p, v in self.parameters.items(): 

439 s = '# {} : {}\n'.format(p, v) 

440 output.write(s) 

441 output.write(lim + '\n') 

442 

443 assert not ('kpts' in self.parameters and 'k_grid' in self.parameters) 

444 assert not ('smearing' in self.parameters and 

445 'occupation_type' in self.parameters) 

446 

447 for key, value in self.parameters.items(): 

448 if key == 'kpts': 

449 mp = kpts2mp(atoms, self.parameters.kpts) 

450 output.write('%-35s%d %d %d\n' % (('k_grid',) + tuple(mp))) 

451 dk = 0.5 - 0.5 / np.array(mp) 

452 output.write('%-35s%f %f %f\n' % (('k_offset',) + tuple(dk))) 

453 elif key == 'species_dir' or key == 'run_command': 

454 continue 

455 elif key == 'plus_u': 

456 continue 

457 elif key == 'smearing': 

458 name = self.parameters.smearing[0].lower() 

459 if name == 'fermi-dirac': 

460 name = 'fermi' 

461 width = self.parameters.smearing[1] 

462 output.write('%-35s%s %f' % ('occupation_type', name, width)) 

463 if name == 'methfessel-paxton': 

464 order = self.parameters.smearing[2] 

465 output.write(' %d' % order) 

466 output.write('\n' % order) 

467 elif key == 'output': 

468 for output_type in value: 

469 output.write('%-35s%s\n' % (key, output_type)) 

470 elif key == 'vdw_correction_hirshfeld' and value: 

471 output.write('%-35s\n' % key) 

472 elif key in bool_keys: 

473 output.write('%-35s.%s.\n' % (key, repr(bool(value)).lower())) 

474 elif isinstance(value, (tuple, list)): 

475 output.write('%-35s%s\n' % 

476 (key, ' '.join(str(x) for x in value))) 

477 elif isinstance(value, str): 

478 output.write('%-35s%s\n' % (key, value)) 

479 else: 

480 output.write('%-35s%r\n' % (key, value)) 

481 if self.cubes: 

482 self.cubes.write(output) 

483 output.write(lim + '\n\n') 

484 output.close() 

485 

486 def read(self, label=None): 

487 if label is None: 

488 label = self.label 

489 FileIOCalculator.read(self, label) 

490 geometry = os.path.join(self.directory, 'geometry.in') 

491 control = os.path.join(self.directory, 'control.in') 

492 

493 for filename in [geometry, control, self.out]: 

494 if not os.path.isfile(filename): 

495 raise ReadError 

496 

497 self.atoms, symmetry_block = read_aims(geometry, True) 

498 self.parameters = Parameters.read(os.path.join(self.directory, 

499 'parameters.ase')) 

500 if symmetry_block: 

501 self.parameters["symmetry_block"] = symmetry_block 

502 self.read_results() 

503 

504 def read_results(self): 

505 converged = self.read_convergence() 

506 if not converged: 

507 os.system('tail -20 ' + self.out) 

508 raise RuntimeError('FHI-aims did not converge!\n' + 

509 'The last lines of output are printed above ' + 

510 'and should give an indication why.') 

511 self.read_energy() 

512 if ('compute_forces' in self.parameters or 

513 'sc_accuracy_forces' in self.parameters): 

514 self.read_forces() 

515 

516 if ('sc_accuracy_stress' in self.parameters or 

517 ('compute_numerical_stress' in self.parameters 

518 and self.parameters['compute_numerical_stress']) or 

519 ('compute_analytical_stress' in self.parameters 

520 and self.parameters['compute_analytical_stress']) or 

521 ('compute_heat_flux' in self.parameters 

522 and self.parameters['compute_heat_flux'])): 

523 self.read_stress() 

524 

525 if ('compute_heat_flux' in self.parameters 

526 and self.parameters['compute_heat_flux']): 

527 self.read_stresses() 

528 

529 if ('dipole' in self.parameters.get('output', []) and 

530 not self.atoms.pbc.any()): 

531 self.read_dipole() 

532 

533 def write_species(self, atoms, filename): 

534 self.ctrlname = filename 

535 species_path = self.parameters.get('species_dir') 

536 if species_path is None: 

537 species_path = os.environ.get('AIMS_SPECIES_DIR') 

538 if species_path is None: 

539 raise RuntimeError( 

540 'Missing species directory! Use species_dir ' + 

541 'parameter or set $AIMS_SPECIES_DIR environment variable.') 

542 control = open(filename, 'a') 

543 symbols = atoms.get_chemical_symbols() 

544 symbols2 = [] 

545 for n, symbol in enumerate(symbols): 

546 if symbol not in symbols2: 

547 symbols2.append(symbol) 

548 if self.tier is not None: 

549 if isinstance(self.tier, int): 

550 self.tierlist = np.ones(len(symbols2), 'int') * self.tier 

551 elif isinstance(self.tier, list): 

552 assert len(self.tier) == len(symbols2) 

553 self.tierlist = self.tier 

554 

555 for i, symbol in enumerate(symbols2): 

556 fd = os.path.join(species_path, '%02i_%s_default' % 

557 (atomic_numbers[symbol], symbol)) 

558 reached_tiers = False 

559 for line in open(fd, 'r'): 

560 if self.tier is not None: 

561 if 'First tier' in line: 

562 reached_tiers = True 

563 self.targettier = self.tierlist[i] 

564 self.foundtarget = False 

565 self.do_uncomment = True 

566 if reached_tiers: 

567 line = self.format_tiers(line) 

568 control.write(line) 

569 if self.tier is not None and not self.foundtarget: 

570 raise RuntimeError( 

571 "Basis tier %i not found for element %s" % 

572 (self.targettier, symbol)) 

573 if self.parameters.get('plus_u') is not None: 

574 if symbol in self.parameters.plus_u.keys(): 

575 control.write('plus_u %s \n' % 

576 self.parameters.plus_u[symbol]) 

577 control.close() 

578 

579 if self.radmul is not None: 

580 self.set_radial_multiplier() 

581 

582 def format_tiers(self, line): 

583 if 'meV' in line: 

584 assert line[0] == '#' 

585 if 'tier' in line and 'Further' not in line: 

586 tier = line.split(" tier")[0] 

587 tier = tier.split('"')[-1] 

588 current_tier = self.translate_tier(tier) 

589 if current_tier == self.targettier: 

590 self.foundtarget = True 

591 elif current_tier > self.targettier: 

592 self.do_uncomment = False 

593 else: 

594 self.do_uncomment = False 

595 return line 

596 elif self.do_uncomment and line[0] == '#': 

597 return line[1:] 

598 elif not self.do_uncomment and line[0] != '#': 

599 return '#' + line 

600 else: 

601 return line 

602 

603 def translate_tier(self, tier): 

604 if tier.lower() == 'first': 

605 return 1 

606 elif tier.lower() == 'second': 

607 return 2 

608 elif tier.lower() == 'third': 

609 return 3 

610 elif tier.lower() == 'fourth': 

611 return 4 

612 else: 

613 return -1 

614 

615 def set_radial_multiplier(self): 

616 assert isinstance(self.radmul, int) 

617 newctrl = self.ctrlname + '.new' 

618 fin = open(self.ctrlname, 'r') 

619 fout = open(newctrl, 'w') 

620 newline = " radial_multiplier %i\n" % self.radmul 

621 for line in fin: 

622 if ' radial_multiplier' in line: 

623 fout.write(newline) 

624 else: 

625 fout.write(line) 

626 fin.close() 

627 fout.close() 

628 os.rename(newctrl, self.ctrlname) 

629 

630 def get_dipole_moment(self, atoms): 

631 if ('dipole' not in self.parameters.get('output', []) or 

632 atoms.pbc.any()): 

633 raise PropertyNotImplementedError 

634 return FileIOCalculator.get_dipole_moment(self, atoms) 

635 

636 def get_stress(self, atoms): 

637 if ('compute_numerical_stress' not in self.parameters and 

638 'compute_analytical_stress' not in self.parameters): 

639 raise PropertyNotImplementedError 

640 return FileIOCalculator.get_stress(self, atoms) 

641 

642 def get_forces(self, atoms): 

643 if ('compute_forces' not in self.parameters and 

644 'sc_accuracy_forces' not in self.parameters): 

645 raise PropertyNotImplementedError 

646 return FileIOCalculator.get_forces(self, atoms) 

647 

648 def read_dipole(self): 

649 "Method that reads the electric dipole moment from the output file." 

650 for line in open(self.out, 'r'): 

651 if line.rfind('Total dipole moment [eAng]') > -1: 

652 dipolemoment = np.array([float(f) 

653 for f in line.split()[6:9]]) 

654 self.results['dipole'] = dipolemoment 

655 

656 def read_energy(self): 

657 for line in open(self.out, 'r'): 

658 if line.rfind('Total energy corrected') > -1: 

659 E0 = float(line.split()[5]) 

660 elif line.rfind('Total energy uncorrected') > -1: 

661 F = float(line.split()[5]) 

662 self.results['free_energy'] = F 

663 self.results['energy'] = E0 

664 

665 def read_forces(self): 

666 """Method that reads forces from the output file. 

667 

668 If 'all' is switched on, the forces for all ionic steps 

669 in the output file will be returned, in other case only the 

670 forces for the last ionic configuration are returned.""" 

671 lines = open(self.out, 'r').readlines() 

672 forces = np.zeros([len(self.atoms), 3]) 

673 for n, line in enumerate(lines): 

674 if line.rfind('Total atomic forces') > -1: 

675 for iatom in range(len(self.atoms)): 

676 data = lines[n + iatom + 1].split() 

677 for iforce in range(3): 

678 forces[iatom, iforce] = float(data[2 + iforce]) 

679 self.results['forces'] = forces 

680 

681 def read_stress(self): 

682 lines = open(self.out, 'r').readlines() 

683 stress = None 

684 for n, line in enumerate(lines): 

685 if (line.rfind('| Analytical stress tensor') > -1 or 

686 line.rfind('Numerical stress tensor') > -1): 

687 stress = [] 

688 for i in [n + 5, n + 6, n + 7]: 

689 data = lines[i].split() 

690 stress += [float(data[2]), float(data[3]), float(data[4])] 

691 # rearrange in 6-component form and return 

692 self.results['stress'] = np.array([stress[0], stress[4], stress[8], 

693 stress[5], stress[2], stress[1]]) 

694 

695 def read_stresses(self): 

696 """ Read stress per atom """ 

697 with open(self.out) as fd: 

698 next(l for l in fd if 

699 'Per atom stress (eV) used for heat flux calculation' in l) 

700 # scroll to boundary 

701 next(l for l in fd if '-------------' in l) 

702 

703 stresses = [] 

704 for l in [next(fd) for _ in range(len(self.atoms))]: 

705 # Read stresses and rearrange from 

706 # (xx, yy, zz, xy, xz, yz) to (xx, yy, zz, yz, xz, xy) 

707 xx, yy, zz, xy, xz, yz = [float(d) for d in l.split()[2:8]] 

708 stresses.append([xx, yy, zz, yz, xz, xy]) 

709 

710 self.results['stresses'] = np.array(stresses) 

711 

712 def get_stresses(self, voigt=False): 

713 """ Return stress per atom 

714 

715 Returns an array of the six independent components of the 

716 symmetric stress tensor per atom, in the traditional Voigt order 

717 (xx, yy, zz, yz, xz, xy) or as a 3x3 matrix. Default is 3x3 matrix. 

718 """ 

719 

720 voigt_stresses = self.results['stresses'] 

721 

722 if voigt: 

723 return voigt_stresses 

724 else: 

725 stresses = np.zeros((len(self.atoms), 3, 3)) 

726 for ii, stress in enumerate(voigt_stresses): 

727 xx, yy, zz, yz, xz, xy = stress 

728 stresses[ii] = np.array([(xx, xy, xz), 

729 (xy, yy, yz), 

730 (xz, yz, zz)]) 

731 return stresses 

732 

733 def read_convergence(self): 

734 converged = False 

735 lines = open(self.out, 'r').readlines() 

736 for n, line in enumerate(lines): 

737 if line.rfind('Have a nice day') > -1: 

738 converged = True 

739 return converged 

740 

741 def get_number_of_iterations(self): 

742 return self.read_number_of_iterations() 

743 

744 def read_number_of_iterations(self): 

745 niter = None 

746 lines = open(self.out, 'r').readlines() 

747 for n, line in enumerate(lines): 

748 if line.rfind('| Number of self-consistency cycles') > -1: 

749 niter = int(line.split(':')[-1].strip()) 

750 return niter 

751 

752 def get_electronic_temperature(self): 

753 return self.read_electronic_temperature() 

754 

755 def read_electronic_temperature(self): 

756 width = None 

757 lines = open(self.out, 'r').readlines() 

758 for n, line in enumerate(lines): 

759 if line.rfind('Occupation type:') > -1: 

760 width = float(line.split('=')[-1].strip().split()[0]) 

761 return width 

762 

763 def get_number_of_electrons(self): 

764 return self.read_number_of_electrons() 

765 

766 def read_number_of_electrons(self): 

767 nelect = None 

768 lines = open(self.out, 'r').readlines() 

769 for n, line in enumerate(lines): 

770 if line.rfind('The structure contains') > -1: 

771 nelect = float(line.split()[-2].strip()) 

772 return nelect 

773 

774 def get_number_of_bands(self): 

775 return self.read_number_of_bands() 

776 

777 def read_number_of_bands(self): 

778 nband = None 

779 lines = open(self.out, 'r').readlines() 

780 for n, line in enumerate(lines): 

781 if line.rfind('Number of Kohn-Sham states') > -1: 

782 nband = int(line.split(':')[-1].strip()) 

783 return nband 

784 

785 def get_k_point_weights(self): 

786 return self.read_kpts(mode='k_point_weights') 

787 

788 def get_bz_k_points(self): 

789 raise NotImplementedError 

790 

791 def get_ibz_k_points(self): 

792 return self.read_kpts(mode='ibz_k_points') 

793 

794 def get_spin_polarized(self): 

795 return self.read_number_of_spins() 

796 

797 def get_number_of_spins(self): 

798 return 1 + self.get_spin_polarized() 

799 

800 def get_magnetic_moment(self, atoms=None): 

801 return self.read_magnetic_moment() 

802 

803 def read_number_of_spins(self): 

804 spinpol = None 

805 lines = open(self.out, 'r').readlines() 

806 for n, line in enumerate(lines): 

807 if line.rfind('| Number of spin channels') > -1: 

808 spinpol = int(line.split(':')[-1].strip()) - 1 

809 return spinpol 

810 

811 def read_magnetic_moment(self): 

812 magmom = None 

813 if not self.get_spin_polarized(): 

814 magmom = 0.0 

815 else: # only for spinpolarized system Magnetisation is printed 

816 for line in open(self.out, 'r').readlines(): 

817 if line.find('N_up - N_down') != -1: # last one 

818 magmom = float(line.split(':')[-1].strip()) 

819 return magmom 

820 

821 def get_fermi_level(self): 

822 return self.read_fermi() 

823 

824 def get_eigenvalues(self, kpt=0, spin=0): 

825 return self.read_eigenvalues(kpt, spin, 'eigenvalues') 

826 

827 def get_occupations(self, kpt=0, spin=0): 

828 return self.read_eigenvalues(kpt, spin, 'occupations') 

829 

830 def read_fermi(self): 

831 E_f = None 

832 lines = open(self.out, 'r').readlines() 

833 for n, line in enumerate(lines): 

834 if line.rfind('| Chemical potential (Fermi level) in eV') > -1: 

835 E_f = float(line.split(':')[-1].strip()) 

836 return E_f 

837 

838 def read_kpts(self, mode='ibz_k_points'): 

839 """ Returns list of kpts weights or kpts coordinates. """ 

840 values = [] 

841 assert mode in ['ibz_k_points', 'k_point_weights'] 

842 lines = open(self.out, 'r').readlines() 

843 kpts = None 

844 kptsstart = None 

845 for n, line in enumerate(lines): 

846 if line.rfind('| Number of k-points') > -1: 

847 kpts = int(line.split(':')[-1].strip()) 

848 for n, line in enumerate(lines): 

849 if line.rfind('K-points in task') > -1: 

850 kptsstart = n # last occurrence of ( 

851 assert kpts is not None 

852 assert kptsstart is not None 

853 text = lines[kptsstart + 1:] 

854 values = [] 

855 for line in text[:kpts]: 

856 if mode == 'ibz_k_points': 

857 b = [float(c.strip()) for c in line.split()[4:7]] 

858 else: 

859 b = float(line.split()[-1]) 

860 values.append(b) 

861 if len(values) == 0: 

862 values = None 

863 return np.array(values) 

864 

865 def read_eigenvalues(self, kpt=0, spin=0, mode='eigenvalues'): 

866 """ Returns list of last eigenvalues, occupations 

867 for given kpt and spin. """ 

868 values = [] 

869 assert mode in ['eigenvalues', 'occupations'] 

870 lines = open(self.out, 'r').readlines() 

871 # number of kpts 

872 kpts = None 

873 for n, line in enumerate(lines): 

874 if line.rfind('| Number of k-points') > -1: 

875 kpts = int(line.split(':')[-1].strip()) 

876 break 

877 assert kpts is not None 

878 assert kpt + 1 <= kpts 

879 # find last (eigenvalues) 

880 eigvalstart = None 

881 for n, line in enumerate(lines): 

882 # eigenvalues come after Preliminary charge convergence reached 

883 if line.rfind('Preliminary charge convergence reached') > -1: 

884 eigvalstart = n 

885 break 

886 assert eigvalstart is not None 

887 lines = lines[eigvalstart:] 

888 for n, line in enumerate(lines): 

889 if line.rfind('Writing Kohn-Sham eigenvalues') > -1: 

890 eigvalstart = n 

891 break 

892 assert eigvalstart is not None 

893 text = lines[eigvalstart + 1:] # remove first 1 line 

894 # find the requested k-point 

895 nbands = self.read_number_of_bands() 

896 sppol = self.get_spin_polarized() 

897 beg = ((nbands + 4 + int(sppol) * 1) * kpt * (sppol + 1) + 

898 3 + sppol * 2 + kpt * sppol) 

899 if self.get_spin_polarized(): 

900 if spin == 0: 

901 beg = beg 

902 end = beg + nbands 

903 else: 

904 beg = beg + nbands + 5 

905 end = beg + nbands 

906 else: 

907 end = beg + nbands 

908 values = [] 

909 for line in text[beg:end]: 

910 # aims prints stars for large values ... 

911 line = line.replace('**************', ' 10000') 

912 line = line.replace('***************', ' 10000') 

913 line = line.replace('****************', ' 10000') 

914 b = [float(c.strip()) for c in line.split()[1:]] 

915 values.append(b) 

916 if mode == 'eigenvalues': 

917 values = [Hartree * v[1] for v in values] 

918 else: 

919 values = [v[0] for v in values] 

920 if len(values) == 0: 

921 values = None 

922 return np.array(values) 

923 

924 

925class AimsCube: 

926 "Object to ensure the output of cube files, can be attached to Aims object" 

927 def __init__(self, origin=(0, 0, 0), 

928 edges=[(0.1, 0.0, 0.0), (0.0, 0.1, 0.0), (0.0, 0.0, 0.1)], 

929 points=(50, 50, 50), plots=None): 

930 """parameters: 

931 

932 origin, edges, points: 

933 Same as in the FHI-aims output 

934 plots: 

935 what to print, same names as in FHI-aims """ 

936 

937 self.name = 'AimsCube' 

938 self.origin = origin 

939 self.edges = edges 

940 self.points = points 

941 self.plots = plots 

942 

943 def ncubes(self): 

944 """returns the number of cube files to output """ 

945 if self.plots: 

946 number = len(self.plots) 

947 else: 

948 number = 0 

949 return number 

950 

951 def set(self, **kwargs): 

952 """ set any of the parameters ... """ 

953 # NOT IMPLEMENTED AT THE MOMENT! 

954 

955 def move_to_base_name(self, basename): 

956 """ when output tracking is on or the base namem is not standard, 

957 this routine will rename add the base to the cube file output for 

958 easier tracking """ 

959 for plot in self.plots: 

960 found = False 

961 cube = plot.split() 

962 if (cube[0] == 'total_density' or 

963 cube[0] == 'spin_density' or 

964 cube[0] == 'delta_density'): 

965 found = True 

966 old_name = cube[0] + '.cube' 

967 new_name = basename + '.' + old_name 

968 if cube[0] == 'eigenstate' or cube[0] == 'eigenstate_density': 

969 found = True 

970 state = int(cube[1]) 

971 s_state = cube[1] 

972 for i in [10, 100, 1000, 10000]: 

973 if state < i: 

974 s_state = '0' + s_state 

975 old_name = cube[0] + '_' + s_state + '_spin_1.cube' 

976 new_name = basename + '.' + old_name 

977 if found: 

978 os.system('mv ' + old_name + ' ' + new_name) 

979 

980 def add_plot(self, name): 

981 """ in case you forgot one ... """ 

982 self.plots += [name] 

983 

984 def write(self, file): 

985 """ write the necessary output to the already opened control.in """ 

986 file.write('output cube ' + self.plots[0] + '\n') 

987 file.write(' cube origin ') 

988 for ival in self.origin: 

989 file.write(str(ival) + ' ') 

990 file.write('\n') 

991 for i in range(3): 

992 file.write(' cube edge ' + str(self.points[i]) + ' ') 

993 for ival in self.edges[i]: 

994 file.write(str(ival) + ' ') 

995 file.write('\n') 

996 if self.ncubes() > 1: 

997 for i in range(self.ncubes() - 1): 

998 file.write('output cube ' + self.plots[i + 1] + '\n')