Coverage for /builds/debichem-team/python-ase/ase/calculators/vasp/vasp.py: 41.42%

676 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-03-06 04:00 +0000

1# Copyright (C) 2008 CSC - Scientific Computing Ltd. 

2"""This module defines an ASE interface to VASP. 

3 

4Developed on the basis of modules by Jussi Enkovaara and John 

5Kitchin. The path of the directory containing the pseudopotential 

6directories (potpaw,potpaw_GGA, potpaw_PBE, ...) should be set 

7by the environmental flag $VASP_PP_PATH. 

8 

9The user should also set the environmental flag $VASP_SCRIPT pointing 

10to a python script looking something like:: 

11 

12 import os 

13 exitcode = os.system('vasp') 

14 

15Alternatively, user can set the environmental flag $VASP_COMMAND pointing 

16to the command use the launch vasp e.g. 'vasp' or 'mpirun -n 16 vasp' 

17 

18http://cms.mpi.univie.ac.at/vasp/ 

19""" 

20 

21import os 

22import re 

23import subprocess 

24import sys 

25from contextlib import contextmanager 

26from pathlib import Path 

27from typing import Any, Dict, List, Tuple 

28from warnings import warn 

29from xml.etree import ElementTree 

30 

31import numpy as np 

32 

33import ase 

34from ase.calculators import calculator 

35from ase.calculators.calculator import Calculator 

36from ase.calculators.singlepoint import SinglePointDFTCalculator 

37from ase.calculators.vasp.create_input import GenerateVaspInput 

38from ase.config import cfg 

39from ase.io import jsonio, read 

40from ase.utils import PurePath, deprecated 

41from ase.vibrations.data import VibrationsData 

42 

43 

44def _prohibit_directory_in_label(args: List, kwargs: Dict[str, Any]) -> bool: 

45 if len(args) >= 5 and "/" in args[4]: 

46 return True 

47 return "/" in kwargs.get("label", "") 

48 

49 

50class Vasp(GenerateVaspInput, Calculator): # type: ignore[misc] 

51 """ASE interface for the Vienna Ab initio Simulation Package (VASP), 

52 with the Calculator interface. 

53 

54 Parameters: 

55 

56 atoms: object 

57 Attach an atoms object to the calculator. 

58 

59 label: str 

60 Prefix for the output file, and sets the working directory. 

61 Default is 'vasp'. 

62 

63 directory: str 

64 Set the working directory. Is prepended to ``label``. 

65 

66 restart: str or bool 

67 Sets a label for the directory to load files from. 

68 if :code:`restart=True`, the working directory from 

69 ``directory`` is used. 

70 

71 txt: bool, None, str or writable object 

72 - If txt is None, output stream will be supressed 

73 

74 - If txt is '-' the output will be sent through stdout 

75 

76 - If txt is a string a file will be opened,\ 

77 and the output will be sent to that file. 

78 

79 - Finally, txt can also be a an output stream,\ 

80 which has a 'write' attribute. 

81 

82 Default is 'vasp.out' 

83 

84 - Examples: 

85 >>> from ase.calculators.vasp import Vasp 

86 >>> calc = Vasp(label='mylabel', txt='vasp.out') # Redirect stdout 

87 >>> calc = Vasp(txt='myfile.txt') # Redirect stdout 

88 >>> calc = Vasp(txt='-') # Print vasp output to stdout 

89 >>> calc = Vasp(txt=None) # Suppress txt output 

90 

91 command: str 

92 Custom instructions on how to execute VASP. Has priority over 

93 environment variables. 

94 """ 

95 name = 'vasp' 

96 ase_objtype = 'vasp_calculator' # For JSON storage 

97 

98 # Environment commands 

99 env_commands = ('ASE_VASP_COMMAND', 'VASP_COMMAND', 'VASP_SCRIPT') 

100 

101 implemented_properties = [ 

102 'energy', 'free_energy', 'forces', 'dipole', 'fermi', 'stress', 

103 'magmom', 'magmoms' 

104 ] 

105 

106 # Can be used later to set some ASE defaults 

107 default_parameters: Dict[str, Any] = {} 

108 

109 @deprecated( 

110 'Specifying directory in "label" is deprecated, ' 

111 'use "directory" instead.', 

112 category=FutureWarning, 

113 callback=_prohibit_directory_in_label, 

114 ) 

115 def __init__(self, 

116 atoms=None, 

117 restart=None, 

118 directory='.', 

119 label='vasp', 

120 ignore_bad_restart_file=Calculator._deprecated, 

121 command=None, 

122 txt='vasp.out', 

123 **kwargs): 

124 """ 

125 .. deprecated:: 3.19.2 

126 Specifying directory in ``label`` is deprecated, 

127 use ``directory`` instead. 

128 """ 

129 

130 self._atoms = None 

131 self.results = {} 

132 

133 # Initialize parameter dictionaries 

134 GenerateVaspInput.__init__(self) 

135 self._store_param_state() # Initialize an empty parameter state 

136 

137 # Store calculator from vasprun.xml here - None => uninitialized 

138 self._xml_calc = None 

139 

140 # Set directory and label 

141 self.directory = directory 

142 if "/" in label: 

143 if self.directory != ".": 

144 msg = ( 

145 'Directory redundantly specified though directory=' 

146 f'"{self.directory}" and label="{label}". Please omit ' 

147 '"/" in label.' 

148 ) 

149 raise ValueError(msg) 

150 self.label = label 

151 else: 

152 self.prefix = label # The label should only contain the prefix 

153 

154 if isinstance(restart, bool): 

155 restart = self.label if restart is True else None 

156 

157 Calculator.__init__( 

158 self, 

159 restart=restart, 

160 ignore_bad_restart_file=ignore_bad_restart_file, 

161 # We already, manually, created the label 

162 label=self.label, 

163 atoms=atoms, 

164 **kwargs) 

165 

166 self.command = command 

167 

168 self._txt = None 

169 self.txt = txt # Set the output txt stream 

170 self.version = None 

171 

172 # XXX: This seems to break restarting, unless we return first. 

173 # Do we really still need to enfore this? 

174 

175 # # If no XC combination, GGA functional or POTCAR type is specified, 

176 # # default to PW91. This is mostly chosen for backwards compatibility. 

177 # if kwargs.get('xc', None): 

178 # pass 

179 # elif not (kwargs.get('gga', None) or kwargs.get('pp', None)): 

180 # self.input_params.update({'xc': 'PW91'}) 

181 # # A null value of xc is permitted; custom recipes can be 

182 # # used by explicitly setting the pseudopotential set and 

183 # # INCAR keys 

184 # else: 

185 # self.input_params.update({'xc': None}) 

186 

187 def make_command(self, command=None): 

188 """Return command if one is passed, otherwise try to find 

189 ASE_VASP_COMMAND, VASP_COMMAND or VASP_SCRIPT. 

190 If none are set, a CalculatorSetupError is raised""" 

191 if command: 

192 cmd = command 

193 else: 

194 # Search for the environment commands 

195 for env in self.env_commands: 

196 if env in cfg: 

197 cmd = cfg[env].replace('PREFIX', self.prefix) 

198 if env == 'VASP_SCRIPT': 

199 # Make the system python exe run $VASP_SCRIPT 

200 exe = sys.executable 

201 cmd = ' '.join([exe, cmd]) 

202 break 

203 else: 

204 msg = ('Please set either command in calculator' 

205 ' or one of the following environment ' 

206 'variables (prioritized as follows): {}').format( 

207 ', '.join(self.env_commands)) 

208 raise calculator.CalculatorSetupError(msg) 

209 return cmd 

210 

211 def set(self, **kwargs): 

212 """Override the set function, to test for changes in the 

213 Vasp Calculator, then call the create_input.set() 

214 on remaining inputs for VASP specific keys. 

215 

216 Allows for setting ``label``, ``directory`` and ``txt`` 

217 without resetting the results in the calculator. 

218 """ 

219 changed_parameters = {} 

220 

221 if 'label' in kwargs: 

222 self.label = kwargs.pop('label') 

223 

224 if 'directory' in kwargs: 

225 # str() call to deal with pathlib objects 

226 self.directory = str(kwargs.pop('directory')) 

227 

228 if 'txt' in kwargs: 

229 self.txt = kwargs.pop('txt') 

230 

231 if 'atoms' in kwargs: 

232 atoms = kwargs.pop('atoms') 

233 self.atoms = atoms # Resets results 

234 

235 if 'command' in kwargs: 

236 self.command = kwargs.pop('command') 

237 

238 changed_parameters.update(Calculator.set(self, **kwargs)) 

239 

240 # We might at some point add more to changed parameters, or use it 

241 if changed_parameters: 

242 self.clear_results() # We don't want to clear atoms 

243 if kwargs: 

244 # If we make any changes to Vasp input, we always reset 

245 GenerateVaspInput.set(self, **kwargs) 

246 self.results.clear() 

247 

248 def reset(self): 

249 self.atoms = None 

250 self.clear_results() 

251 

252 def clear_results(self): 

253 self.results.clear() 

254 self._xml_calc = None 

255 

256 @contextmanager 

257 def _txt_outstream(self): 

258 """Custom function for opening a text output stream. Uses self.txt 

259 to determine the output stream, and accepts a string or an open 

260 writable object. 

261 If a string is used, a new stream is opened, and automatically closes 

262 the new stream again when exiting. 

263 

264 Examples: 

265 # Pass a string 

266 calc.txt = 'vasp.out' 

267 with calc.txt_outstream() as out: 

268 calc.run(out=out) # Redirects the stdout to 'vasp.out' 

269 

270 # Use an existing stream 

271 mystream = open('vasp.out', 'w') 

272 calc.txt = mystream 

273 with calc.txt_outstream() as out: 

274 calc.run(out=out) 

275 mystream.close() 

276 

277 # Print to stdout 

278 calc.txt = '-' 

279 with calc.txt_outstream() as out: 

280 calc.run(out=out) # output is written to stdout 

281 """ 

282 

283 txt = self.txt 

284 open_and_close = False # Do we open the file? 

285 

286 if txt is None: 

287 # Suppress stdout 

288 out = subprocess.DEVNULL 

289 else: 

290 if isinstance(txt, str): 

291 if txt == '-': 

292 # subprocess.call redirects this to stdout 

293 out = None 

294 else: 

295 # Open the file in the work directory 

296 txt = self._indir(txt) 

297 # We wait with opening the file, until we are inside the 

298 # try/finally 

299 open_and_close = True 

300 elif hasattr(txt, 'write'): 

301 out = txt 

302 else: 

303 raise RuntimeError('txt should either be a string' 

304 'or an I/O stream, got {}'.format(txt)) 

305 

306 try: 

307 if open_and_close: 

308 out = open(txt, 'w') 

309 yield out 

310 finally: 

311 if open_and_close: 

312 out.close() 

313 

314 def calculate(self, 

315 atoms=None, 

316 properties=('energy', ), 

317 system_changes=tuple(calculator.all_changes)): 

318 """Do a VASP calculation in the specified directory. 

319 

320 This will generate the necessary VASP input files, and then 

321 execute VASP. After execution, the energy, forces. etc. are read 

322 from the VASP output files. 

323 """ 

324 Calculator.calculate(self, atoms, properties, system_changes) 

325 # Check for zero-length lattice vectors and PBC 

326 # and that we actually have an Atoms object. 

327 check_atoms(self.atoms) 

328 

329 self.clear_results() 

330 

331 command = self.make_command(self.command) 

332 self.write_input(self.atoms, properties, system_changes) 

333 

334 with self._txt_outstream() as out: 

335 errorcode, stderr = self._run(command=command, 

336 out=out, 

337 directory=self.directory) 

338 

339 if errorcode: 

340 raise calculator.CalculationFailed( 

341 '{} in {} returned an error: {:d} stderr {}'.format( 

342 self.name, Path(self.directory).resolve(), errorcode, 

343 stderr)) 

344 

345 # Read results from calculation 

346 self.update_atoms(atoms) 

347 self.read_results() 

348 

349 def _run(self, command=None, out=None, directory=None): 

350 """Method to explicitly execute VASP""" 

351 if command is None: 

352 command = self.command 

353 if directory is None: 

354 directory = self.directory 

355 result = subprocess.run(command, 

356 shell=True, 

357 cwd=directory, 

358 capture_output=True, 

359 text=True) 

360 if out is not None: 

361 out.write(result.stdout) 

362 return result.returncode, result.stderr 

363 

364 def check_state(self, atoms, tol=1e-15): 

365 """Check for system changes since last calculation.""" 

366 def compare_dict(d1, d2): 

367 """Helper function to compare dictionaries""" 

368 # Use symmetric difference to find keys which aren't shared 

369 # for python 2.7 compatibility 

370 if set(d1.keys()) ^ set(d2.keys()): 

371 return False 

372 

373 # Check for differences in values 

374 for key, value in d1.items(): 

375 if np.any(value != d2[key]): 

376 return False 

377 return True 

378 

379 # First we check for default changes 

380 system_changes = Calculator.check_state(self, atoms, tol=tol) 

381 

382 # We now check if we have made any changes to the input parameters 

383 # XXX: Should we add these parameters to all_changes? 

384 for param_string, old_dict in self.param_state.items(): 

385 param_dict = getattr(self, param_string) # Get current param dict 

386 if not compare_dict(param_dict, old_dict): 

387 system_changes.append(param_string) 

388 

389 return system_changes 

390 

391 def _store_param_state(self): 

392 """Store current parameter state""" 

393 self.param_state = dict( 

394 float_params=self.float_params.copy(), 

395 exp_params=self.exp_params.copy(), 

396 string_params=self.string_params.copy(), 

397 int_params=self.int_params.copy(), 

398 input_params=self.input_params.copy(), 

399 bool_params=self.bool_params.copy(), 

400 list_int_params=self.list_int_params.copy(), 

401 list_bool_params=self.list_bool_params.copy(), 

402 list_float_params=self.list_float_params.copy(), 

403 dict_params=self.dict_params.copy(), 

404 special_params=self.special_params.copy()) 

405 

406 def asdict(self): 

407 """Return a dictionary representation of the calculator state. 

408 Does NOT contain information on the ``command``, ``txt`` or 

409 ``directory`` keywords. 

410 Contains the following keys: 

411 

412 - ``ase_version`` 

413 - ``vasp_version`` 

414 - ``inputs`` 

415 - ``results`` 

416 - ``atoms`` (Only if the calculator has an ``Atoms`` object) 

417 """ 

418 # Get versions 

419 asevers = ase.__version__ 

420 vaspvers = self.get_version() 

421 

422 self._store_param_state() # Update param state 

423 # Store input parameters which have been set 

424 inputs = { 

425 key: value 

426 for param_dct in self.param_state.values() 

427 for key, value in param_dct.items() if value is not None 

428 } 

429 

430 dct = { 

431 'ase_version': asevers, 

432 'vasp_version': vaspvers, 

433 # '__ase_objtype__': self.ase_objtype, 

434 'inputs': inputs, 

435 'results': self.results.copy() 

436 } 

437 

438 if self.atoms: 

439 # Encode atoms as dict 

440 from ase.db.row import atoms2dict 

441 dct['atoms'] = atoms2dict(self.atoms) 

442 

443 return dct 

444 

445 def fromdict(self, dct): 

446 """Restore calculator from a :func:`~ase.calculators.vasp.Vasp.asdict` 

447 dictionary. 

448 

449 Parameters: 

450 

451 dct: Dictionary 

452 The dictionary which is used to restore the calculator state. 

453 """ 

454 if 'vasp_version' in dct: 

455 self.version = dct['vasp_version'] 

456 if 'inputs' in dct: 

457 self.set(**dct['inputs']) 

458 self._store_param_state() 

459 if 'atoms' in dct: 

460 from ase.db.row import AtomsRow 

461 atoms = AtomsRow(dct['atoms']).toatoms() 

462 self.atoms = atoms 

463 if 'results' in dct: 

464 self.results.update(dct['results']) 

465 

466 def write_json(self, filename): 

467 """Dump calculator state to JSON file. 

468 

469 Parameters: 

470 

471 filename: string 

472 The filename which the JSON file will be stored to. 

473 Prepends the ``directory`` path to the filename. 

474 """ 

475 filename = self._indir(filename) 

476 dct = self.asdict() 

477 jsonio.write_json(filename, dct) 

478 

479 def read_json(self, filename): 

480 """Load Calculator state from an exported JSON Vasp file.""" 

481 dct = jsonio.read_json(filename) 

482 self.fromdict(dct) 

483 

484 def write_input(self, atoms, properties=None, system_changes=None): 

485 """Write VASP inputfiles, INCAR, KPOINTS and POTCAR""" 

486 self.initialize(atoms) 

487 GenerateVaspInput.write_input(self, atoms, directory=self.directory) 

488 

489 def read(self, label=None): 

490 """Read results from VASP output files. 

491 Files which are read: OUTCAR, CONTCAR and vasprun.xml 

492 Raises ReadError if they are not found""" 

493 if label is None: 

494 label = self.label 

495 Calculator.read(self, label) 

496 

497 # If we restart, self.parameters isn't initialized 

498 if self.parameters is None: 

499 self.parameters = self.get_default_parameters() 

500 

501 # Check for existence of the necessary output files 

502 for f in ['OUTCAR', 'CONTCAR', 'vasprun.xml']: 

503 file = self._indir(f) 

504 if not file.is_file(): 

505 raise calculator.ReadError( 

506 f'VASP outputfile {file} was not found') 

507 

508 # Build sorting and resorting lists 

509 self.read_sort() 

510 

511 # Read atoms 

512 self.atoms = self.read_atoms(filename=self._indir('CONTCAR')) 

513 

514 # Read parameters 

515 self.read_incar(filename=self._indir('INCAR')) 

516 self.read_kpoints(filename=self._indir('KPOINTS')) 

517 self.read_potcar(filename=self._indir('POTCAR')) 

518 

519 # Read the results from the calculation 

520 self.read_results() 

521 

522 def _indir(self, filename): 

523 """Prepend current directory to filename""" 

524 return Path(self.directory) / filename 

525 

526 def read_sort(self): 

527 """Create the sorting and resorting list from ase-sort.dat. 

528 If the ase-sort.dat file does not exist, the sorting is redone. 

529 """ 

530 sortfile = self._indir('ase-sort.dat') 

531 if os.path.isfile(sortfile): 

532 self.sort = [] 

533 self.resort = [] 

534 with open(sortfile) as fd: 

535 for line in fd: 

536 sort, resort = line.split() 

537 self.sort.append(int(sort)) 

538 self.resort.append(int(resort)) 

539 else: 

540 # Redo the sorting 

541 atoms = read(self._indir('CONTCAR')) 

542 self.initialize(atoms) 

543 

544 def read_atoms(self, filename): 

545 """Read the atoms from file located in the VASP 

546 working directory. Normally called CONTCAR.""" 

547 return read(filename)[self.resort] 

548 

549 def update_atoms(self, atoms): 

550 """Update the atoms object with new positions and cell""" 

551 if (self.int_params['ibrion'] is not None 

552 and self.int_params['nsw'] is not None): 

553 if self.int_params['ibrion'] > -1 and self.int_params['nsw'] > 0: 

554 # Update atomic positions and unit cell with the ones read 

555 # from CONTCAR. 

556 atoms_sorted = read(self._indir('CONTCAR')) 

557 atoms.positions = atoms_sorted[self.resort].positions 

558 atoms.cell = atoms_sorted.cell 

559 

560 self.atoms = atoms # Creates a copy 

561 

562 def read_results(self): 

563 """Read the results from VASP output files""" 

564 # Temporarily load OUTCAR into memory 

565 outcar = self.load_file('OUTCAR') 

566 

567 # Read the data we can from vasprun.xml 

568 calc_xml = self._read_xml() 

569 xml_results = calc_xml.results 

570 

571 # Fix sorting 

572 xml_results['forces'] = xml_results['forces'][self.resort] 

573 

574 self.results.update(xml_results) 

575 

576 # Parse the outcar, as some properties are not loaded in vasprun.xml 

577 # We want to limit this as much as possible, as reading large OUTCAR's 

578 # is relatively slow 

579 # Removed for now 

580 # self.read_outcar(lines=outcar) 

581 

582 # Update results dict with results from OUTCAR 

583 # which aren't written to the atoms object we read from 

584 # the vasprun.xml file. 

585 

586 self.converged = self.read_convergence(lines=outcar) 

587 self.version = self.read_version() 

588 magmom, magmoms = self.read_mag(lines=outcar) 

589 dipole = self.read_dipole(lines=outcar) 

590 nbands = self.read_nbands(lines=outcar) 

591 self.results.update( 

592 dict(magmom=magmom, magmoms=magmoms, dipole=dipole, nbands=nbands)) 

593 

594 # Stress is not always present. 

595 # Prevent calculation from going into a loop 

596 if 'stress' not in self.results: 

597 self.results.update(dict(stress=None)) 

598 

599 self._set_old_keywords() 

600 

601 # Store the parameters used for this calculation 

602 self._store_param_state() 

603 

604 def _set_old_keywords(self): 

605 """Store keywords for backwards compatibility wd VASP calculator""" 

606 self.spinpol = self.get_spin_polarized() 

607 self.energy_free = self.get_potential_energy(force_consistent=True) 

608 self.energy_zero = self.get_potential_energy(force_consistent=False) 

609 self.forces = self.get_forces() 

610 self.fermi = self.get_fermi_level() 

611 self.dipole = self.get_dipole_moment() 

612 # Prevent calculation from going into a loop 

613 self.stress = self.get_property('stress', allow_calculation=False) 

614 self.nbands = self.get_number_of_bands() 

615 

616 # Below defines some functions for faster access to certain common keywords 

617 @property 

618 def kpts(self): 

619 """Access the kpts from input_params dict""" 

620 return self.input_params['kpts'] 

621 

622 @kpts.setter 

623 def kpts(self, kpts): 

624 """Set kpts in input_params dict""" 

625 self.input_params['kpts'] = kpts 

626 

627 @property 

628 def encut(self): 

629 """Direct access to the encut parameter""" 

630 return self.float_params['encut'] 

631 

632 @encut.setter 

633 def encut(self, encut): 

634 """Direct access for setting the encut parameter""" 

635 self.set(encut=encut) 

636 

637 @property 

638 def xc(self): 

639 """Direct access to the xc parameter""" 

640 return self.get_xc_functional() 

641 

642 @xc.setter 

643 def xc(self, xc): 

644 """Direct access for setting the xc parameter""" 

645 self.set(xc=xc) 

646 

647 @property 

648 def atoms(self): 

649 return self._atoms 

650 

651 @atoms.setter 

652 def atoms(self, atoms): 

653 if atoms is None: 

654 self._atoms = None 

655 self.clear_results() 

656 else: 

657 if self.check_state(atoms): 

658 self.clear_results() 

659 self._atoms = atoms.copy() 

660 

661 def load_file(self, filename): 

662 """Reads a file in the directory, and returns the lines 

663 

664 Example: 

665 >>> from ase.calculators.vasp import Vasp 

666 >>> calc = Vasp() 

667 >>> outcar = calc.load_file('OUTCAR') # doctest: +SKIP 

668 """ 

669 filename = self._indir(filename) 

670 with open(filename) as fd: 

671 return fd.readlines() 

672 

673 @contextmanager 

674 def load_file_iter(self, filename): 

675 """Return a file iterator""" 

676 

677 filename = self._indir(filename) 

678 with open(filename) as fd: 

679 yield fd 

680 

681 def read_outcar(self, lines=None): 

682 """Read results from the OUTCAR file. 

683 Deprecated, see read_results()""" 

684 if not lines: 

685 lines = self.load_file('OUTCAR') 

686 # Spin polarized calculation? 

687 self.spinpol = self.get_spin_polarized() 

688 

689 self.version = self.get_version() 

690 

691 # XXX: Do we want to read all of this again? 

692 self.energy_free, self.energy_zero = self.read_energy(lines=lines) 

693 self.forces = self.read_forces(lines=lines) 

694 self.fermi = self.read_fermi(lines=lines) 

695 

696 self.dipole = self.read_dipole(lines=lines) 

697 

698 self.stress = self.read_stress(lines=lines) 

699 self.nbands = self.read_nbands(lines=lines) 

700 

701 self.read_ldau() 

702 self.magnetic_moment, self.magnetic_moments = self.read_mag( 

703 lines=lines) 

704 

705 def _read_xml(self) -> SinglePointDFTCalculator: 

706 """Read vasprun.xml, and return the last calculator object. 

707 Returns calculator from the xml file. 

708 Raises a ReadError if the reader is not able to construct a calculator. 

709 """ 

710 file = self._indir('vasprun.xml') 

711 incomplete_msg = ( 

712 f'The file "{file}" is incomplete, and no DFT data was available. ' 

713 'This is likely due to an incomplete calculation.') 

714 try: 

715 _xml_atoms = read(file, index=-1, format='vasp-xml') 

716 # Silence mypy, we should only ever get a single atoms object 

717 assert isinstance(_xml_atoms, ase.Atoms) 

718 except ElementTree.ParseError as exc: 

719 raise calculator.ReadError(incomplete_msg) from exc 

720 

721 if _xml_atoms is None or _xml_atoms.calc is None: 

722 raise calculator.ReadError(incomplete_msg) 

723 

724 self._xml_calc = _xml_atoms.calc 

725 return self._xml_calc 

726 

727 @property 

728 def _xml_calc(self) -> SinglePointDFTCalculator: 

729 if self.__xml_calc is None: 

730 raise RuntimeError('vasprun.xml data has not yet been loaded. ' 

731 'Run read_results() first.') 

732 return self.__xml_calc 

733 

734 @_xml_calc.setter 

735 def _xml_calc(self, value): 

736 self.__xml_calc = value 

737 

738 def get_ibz_k_points(self): 

739 calc = self._xml_calc 

740 return calc.get_ibz_k_points() 

741 

742 def get_kpt(self, kpt=0, spin=0): 

743 calc = self._xml_calc 

744 return calc.get_kpt(kpt=kpt, spin=spin) 

745 

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

747 calc = self._xml_calc 

748 return calc.get_eigenvalues(kpt=kpt, spin=spin) 

749 

750 def get_fermi_level(self): 

751 calc = self._xml_calc 

752 return calc.get_fermi_level() 

753 

754 def get_homo_lumo(self): 

755 calc = self._xml_calc 

756 return calc.get_homo_lumo() 

757 

758 def get_homo_lumo_by_spin(self, spin=0): 

759 calc = self._xml_calc 

760 return calc.get_homo_lumo_by_spin(spin=spin) 

761 

762 def get_occupation_numbers(self, kpt=0, spin=0): 

763 calc = self._xml_calc 

764 return calc.get_occupation_numbers(kpt, spin) 

765 

766 def get_spin_polarized(self): 

767 calc = self._xml_calc 

768 return calc.get_spin_polarized() 

769 

770 def get_number_of_spins(self): 

771 calc = self._xml_calc 

772 return calc.get_number_of_spins() 

773 

774 def get_number_of_bands(self): 

775 return self.results.get('nbands', None) 

776 

777 def get_number_of_electrons(self, lines=None): 

778 if not lines: 

779 lines = self.load_file('OUTCAR') 

780 

781 nelect = None 

782 for line in lines: 

783 if 'total number of electrons' in line: 

784 nelect = float(line.split('=')[1].split()[0].strip()) 

785 break 

786 return nelect 

787 

788 def get_k_point_weights(self): 

789 filename = 'IBZKPT' 

790 return self.read_k_point_weights(filename) 

791 

792 def get_dos(self, spin=None, **kwargs): 

793 """ 

794 The total DOS. 

795 

796 Uses the ASE DOS module, and returns a tuple with 

797 (energies, dos). 

798 """ 

799 from ase.dft.dos import DOS 

800 dos = DOS(self, **kwargs) 

801 e = dos.get_energies() 

802 d = dos.get_dos(spin=spin) 

803 return e, d 

804 

805 def get_version(self): 

806 if self.version is None: 

807 # Try if we can read the version number 

808 self.version = self.read_version() 

809 return self.version 

810 

811 def read_version(self): 

812 """Get the VASP version number""" 

813 # The version number is the first occurrence, so we can just 

814 # load the OUTCAR, as we will return soon anyway 

815 if not os.path.isfile(self._indir('OUTCAR')): 

816 return None 

817 with self.load_file_iter('OUTCAR') as lines: 

818 for line in lines: 

819 if ' vasp.' in line: 

820 return line[len(' vasp.'):].split()[0] 

821 # We didn't find the version in VASP 

822 return None 

823 

824 def get_number_of_iterations(self): 

825 return self.read_number_of_iterations() 

826 

827 def read_number_of_iterations(self): 

828 niter = None 

829 with self.load_file_iter('OUTCAR') as lines: 

830 for line in lines: 

831 # find the last iteration number 

832 if '- Iteration' in line: 

833 niter = list(map(int, re.findall(r'\d+', line)))[1] 

834 return niter 

835 

836 def read_number_of_ionic_steps(self): 

837 niter = None 

838 with self.load_file_iter('OUTCAR') as lines: 

839 for line in lines: 

840 if '- Iteration' in line: 

841 niter = list(map(int, re.findall(r'\d+', line)))[0] 

842 return niter 

843 

844 def read_stress(self, lines=None): 

845 """Read stress from OUTCAR. 

846 

847 Depreciated: Use get_stress() instead. 

848 """ 

849 # We don't really need this, as we read this from vasprun.xml 

850 # keeping it around "just in case" for now 

851 if not lines: 

852 lines = self.load_file('OUTCAR') 

853 

854 stress = None 

855 for line in lines: 

856 if ' in kB ' in line: 

857 stress = -np.array([float(a) for a in line.split()[2:]]) 

858 stress = stress[[0, 1, 2, 4, 5, 3]] * 1e-1 * ase.units.GPa 

859 return stress 

860 

861 def read_ldau(self, lines=None): 

862 """Read the LDA+U values from OUTCAR""" 

863 if not lines: 

864 lines = self.load_file('OUTCAR') 

865 

866 ldau_luj = None 

867 ldauprint = None 

868 ldau = None 

869 ldautype = None 

870 atomtypes = [] 

871 # read ldau parameters from outcar 

872 for line in lines: 

873 if line.find('TITEL') != -1: # What atoms are present 

874 atomtypes.append(line.split()[3].split('_')[0].split('.')[0]) 

875 if line.find('LDAUTYPE') != -1: # Is this a DFT+U calculation 

876 ldautype = int(line.split('=')[-1]) 

877 ldau = True 

878 ldau_luj = {} 

879 if line.find('LDAUL') != -1: 

880 L = line.split('=')[-1].split() 

881 if line.find('LDAUU') != -1: 

882 U = line.split('=')[-1].split() 

883 if line.find('LDAUJ') != -1: 

884 J = line.split('=')[-1].split() 

885 # create dictionary 

886 if ldau: 

887 for i, symbol in enumerate(atomtypes): 

888 ldau_luj[symbol] = { 

889 'L': int(L[i]), 

890 'U': float(U[i]), 

891 'J': float(J[i]) 

892 } 

893 self.dict_params['ldau_luj'] = ldau_luj 

894 

895 self.ldau = ldau 

896 self.ldauprint = ldauprint 

897 self.ldautype = ldautype 

898 self.ldau_luj = ldau_luj 

899 return ldau, ldauprint, ldautype, ldau_luj 

900 

901 def get_xc_functional(self): 

902 """Returns the XC functional or the pseudopotential type 

903 

904 If a XC recipe is set explicitly with 'xc', this is returned. 

905 Otherwise, the XC functional associated with the 

906 pseudopotentials (LDA, PW91 or PBE) is returned. 

907 The string is always cast to uppercase for consistency 

908 in checks.""" 

909 if self.input_params.get('xc', None): 

910 return self.input_params['xc'].upper() 

911 if self.input_params.get('pp', None): 

912 return self.input_params['pp'].upper() 

913 raise ValueError('No xc or pp found.') 

914 

915 # Methods for reading information from OUTCAR files: 

916 def read_energy(self, all=None, lines=None): 

917 """Method to read energy from OUTCAR file. 

918 Depreciated: use get_potential_energy() instead""" 

919 if not lines: 

920 lines = self.load_file('OUTCAR') 

921 

922 [energy_free, energy_zero] = [0, 0] 

923 if all: 

924 energy_free = [] 

925 energy_zero = [] 

926 for line in lines: 

927 # Free energy 

928 if line.lower().startswith(' free energy toten'): 

929 if all: 

930 energy_free.append(float(line.split()[-2])) 

931 else: 

932 energy_free = float(line.split()[-2]) 

933 # Extrapolated zero point energy 

934 if line.startswith(' energy without entropy'): 

935 if all: 

936 energy_zero.append(float(line.split()[-1])) 

937 else: 

938 energy_zero = float(line.split()[-1]) 

939 return [energy_free, energy_zero] 

940 

941 def read_forces(self, all=False, lines=None): 

942 """Method that reads forces from OUTCAR file. 

943 

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

945 in the OUTCAR file be returned, in other case only the 

946 forces for the last ionic configuration is returned.""" 

947 

948 if not lines: 

949 lines = self.load_file('OUTCAR') 

950 

951 if all: 

952 all_forces = [] 

953 

954 for n, line in enumerate(lines): 

955 if 'TOTAL-FORCE' in line: 

956 forces = [] 

957 for i in range(len(self.atoms)): 

958 forces.append( 

959 np.array( 

960 [float(f) for f in lines[n + 2 + i].split()[3:6]])) 

961 

962 if all: 

963 all_forces.append(np.array(forces)[self.resort]) 

964 

965 if all: 

966 return np.array(all_forces) 

967 return np.array(forces)[self.resort] 

968 

969 def read_fermi(self, lines=None): 

970 """Method that reads Fermi energy from OUTCAR file""" 

971 if not lines: 

972 lines = self.load_file('OUTCAR') 

973 

974 E_f = None 

975 for line in lines: 

976 if 'E-fermi' in line: 

977 E_f = float(line.split()[2]) 

978 return E_f 

979 

980 def read_dipole(self, lines=None): 

981 """Read dipole from OUTCAR""" 

982 if not lines: 

983 lines = self.load_file('OUTCAR') 

984 

985 dipolemoment = np.zeros([1, 3]) 

986 for line in lines: 

987 if 'dipolmoment' in line: 

988 dipolemoment = np.array([float(f) for f in line.split()[1:4]]) 

989 return dipolemoment 

990 

991 def read_mag(self, lines=None): 

992 if not lines: 

993 lines = self.load_file('OUTCAR') 

994 p = self.int_params 

995 q = self.list_float_params 

996 if self.spinpol: 

997 magnetic_moment = self._read_magnetic_moment(lines=lines) 

998 if ((p['lorbit'] is not None and p['lorbit'] >= 10) 

999 or (p['lorbit'] is None and q['rwigs'])): 

1000 magnetic_moments = self._read_magnetic_moments(lines=lines) 

1001 else: 

1002 warn('Magnetic moment data not written in OUTCAR (LORBIT<10),' 

1003 ' setting magnetic_moments to zero.\nSet LORBIT>=10' 

1004 ' to get information on magnetic moments') 

1005 magnetic_moments = np.zeros(len(self.atoms)) 

1006 else: 

1007 magnetic_moment = 0.0 

1008 magnetic_moments = np.zeros(len(self.atoms)) 

1009 return magnetic_moment, magnetic_moments 

1010 

1011 def _read_magnetic_moments(self, lines=None): 

1012 """Read magnetic moments from OUTCAR. 

1013 Only reads the last occurrence. """ 

1014 if not lines: 

1015 lines = self.load_file('OUTCAR') 

1016 

1017 magnetic_moments = np.zeros(len(self.atoms)) 

1018 magstr = 'magnetization (x)' 

1019 

1020 # Search for the last occurrence 

1021 nidx = -1 

1022 for n, line in enumerate(lines): 

1023 if magstr in line: 

1024 nidx = n 

1025 

1026 # Read that occurrence 

1027 if nidx > -1: 

1028 for m in range(len(self.atoms)): 

1029 magnetic_moments[m] = float(lines[nidx + m + 4].split()[-1]) 

1030 return magnetic_moments[self.resort] 

1031 

1032 def _read_magnetic_moment(self, lines=None): 

1033 """Read magnetic moment from OUTCAR""" 

1034 if not lines: 

1035 lines = self.load_file('OUTCAR') 

1036 

1037 for line in lines: 

1038 if 'number of electron ' in line: 

1039 _ = line.split()[-1] 

1040 magnetic_moment = 0.0 if _ == "magnetization" else float(_) 

1041 return magnetic_moment 

1042 

1043 def read_nbands(self, lines=None): 

1044 """Read number of bands from OUTCAR""" 

1045 if not lines: 

1046 lines = self.load_file('OUTCAR') 

1047 

1048 for line in lines: 

1049 line = self.strip_warnings(line) 

1050 if 'NBANDS' in line: 

1051 return int(line.split()[-1]) 

1052 return None 

1053 

1054 def read_convergence(self, lines=None): 

1055 """Method that checks whether a calculation has converged.""" 

1056 if not lines: 

1057 lines = self.load_file('OUTCAR') 

1058 

1059 converged = None 

1060 # First check electronic convergence 

1061 for line in lines: 

1062 # VASP 6 actually labels loop exit reason 

1063 if 'aborting loop' in line: 

1064 converged = 'because EDIFF is reached' in line 

1065 # NOTE: 'EDIFF was not reached (unconverged)' 

1066 # and 

1067 # 'because hard stop was set' 

1068 # will result in unconverged 

1069 break 

1070 # determine convergence by attempting to reproduce VASP's 

1071 # internal logic 

1072 if 'EDIFF ' in line: 

1073 ediff = float(line.split()[2]) 

1074 if 'total energy-change' in line: 

1075 # I saw this in an atomic oxygen calculation. it 

1076 # breaks this code, so I am checking for it here. 

1077 if 'MIXING' in line: 

1078 continue 

1079 split = line.split(':') 

1080 a = float(split[1].split('(')[0]) 

1081 b = split[1].split('(')[1][0:-2] 

1082 # sometimes this line looks like (second number wrong format!): 

1083 # energy-change (2. order) :-0.2141803E-08 ( 0.2737684-111) 

1084 # we are checking still the first number so 

1085 # let's "fix" the format for the second one 

1086 if 'e' not in b.lower(): 

1087 # replace last occurrence of - (assumed exponent) with -e 

1088 bsplit = b.split('-') 

1089 bsplit[-1] = 'e' + bsplit[-1] 

1090 b = '-'.join(bsplit).replace('-e', 'e-') 

1091 b = float(b) 

1092 if [abs(a), abs(b)] < [ediff, ediff]: 

1093 converged = True 

1094 else: 

1095 converged = False 

1096 continue 

1097 # Then if ibrion in [1,2,3] check whether ionic relaxation 

1098 # condition been fulfilled 

1099 if (self.int_params['ibrion'] in [1, 2, 3] 

1100 and self.int_params['nsw'] not in [0]): 

1101 if not self.read_relaxed(): 

1102 converged = False 

1103 else: 

1104 converged = True 

1105 return converged 

1106 

1107 def read_k_point_weights(self, filename): 

1108 """Read k-point weighting. Normally named IBZKPT.""" 

1109 

1110 lines = self.load_file(filename) 

1111 

1112 if 'Tetrahedra\n' in lines: 

1113 N = lines.index('Tetrahedra\n') 

1114 else: 

1115 N = len(lines) 

1116 kpt_weights = [] 

1117 for n in range(3, N): 

1118 kpt_weights.append(float(lines[n].split()[3])) 

1119 kpt_weights = np.array(kpt_weights) 

1120 kpt_weights /= np.sum(kpt_weights) 

1121 

1122 return kpt_weights 

1123 

1124 def read_relaxed(self, lines=None): 

1125 """Check if ionic relaxation completed""" 

1126 if not lines: 

1127 lines = self.load_file('OUTCAR') 

1128 for line in lines: 

1129 if 'reached required accuracy' in line: 

1130 return True 

1131 return False 

1132 

1133 def read_spinpol(self, lines=None): 

1134 """Method which reads if a calculation from spinpolarized using OUTCAR. 

1135 

1136 Depreciated: Use get_spin_polarized() instead. 

1137 """ 

1138 if not lines: 

1139 lines = self.load_file('OUTCAR') 

1140 

1141 for line in lines: 

1142 if 'ISPIN' in line: 

1143 if int(line.split()[2]) == 2: 

1144 self.spinpol = True 

1145 else: 

1146 self.spinpol = False 

1147 return self.spinpol 

1148 

1149 def strip_warnings(self, line): 

1150 """Returns empty string instead of line from warnings in OUTCAR.""" 

1151 if line[0] == "|": 

1152 return "" 

1153 return line 

1154 

1155 @property 

1156 def txt(self): 

1157 return self._txt 

1158 

1159 @txt.setter 

1160 def txt(self, txt): 

1161 if isinstance(txt, PurePath): 

1162 txt = str(txt) 

1163 self._txt = txt 

1164 

1165 def get_number_of_grid_points(self): 

1166 raise NotImplementedError 

1167 

1168 def get_pseudo_density(self): 

1169 raise NotImplementedError 

1170 

1171 def get_pseudo_wavefunction(self, n=0, k=0, s=0, pad=True): 

1172 raise NotImplementedError 

1173 

1174 def get_bz_k_points(self): 

1175 raise NotImplementedError 

1176 

1177 def read_vib_freq(self, lines=None) -> Tuple[List[float], List[float]]: 

1178 """Read vibrational frequencies. 

1179 

1180 Returns: 

1181 List of real and list of imaginary frequencies 

1182 (imaginary number as real number). 

1183 """ 

1184 freq = [] 

1185 i_freq = [] 

1186 

1187 if not lines: 

1188 lines = self.load_file('OUTCAR') 

1189 

1190 for line in lines: 

1191 data = line.split() 

1192 if 'THz' in data: 

1193 if 'f/i=' not in data: 

1194 freq.append(float(data[-2])) 

1195 else: 

1196 i_freq.append(float(data[-2])) 

1197 return freq, i_freq 

1198 

1199 def _read_massweighted_hessian_xml(self) -> np.ndarray: 

1200 """Read the Mass Weighted Hessian from vasprun.xml. 

1201 

1202 Returns: 

1203 The Mass Weighted Hessian as np.ndarray from the xml file. 

1204 

1205 Raises a ReadError if the reader is not able to read the Hessian. 

1206 

1207 Converts to ASE units for VASP version 6. 

1208 """ 

1209 

1210 file = self._indir('vasprun.xml') 

1211 try: 

1212 tree = ElementTree.iterparse(file) 

1213 hessian = None 

1214 for event, elem in tree: 

1215 if elem.tag == 'dynmat': 

1216 for i, entry in enumerate( 

1217 elem.findall('varray[@name="hessian"]/v')): 

1218 text_split = entry.text.split() 

1219 if not text_split: 

1220 raise ElementTree.ParseError( 

1221 "Could not find varray hessian!") 

1222 if i == 0: 

1223 n_items = len(text_split) 

1224 hessian = np.zeros((n_items, n_items)) 

1225 assert isinstance(hessian, np.ndarray) 

1226 hessian[i, :] = np.array( 

1227 [float(val) for val in text_split]) 

1228 if i != n_items - 1: 

1229 raise ElementTree.ParseError( 

1230 "Hessian is not quadratic!") 

1231 # VASP6+ uses THz**2 as unit, not mEV**2 as before 

1232 for entry in elem.findall('i[@name="unit"]'): 

1233 if entry.text.strip() == 'THz^2': 

1234 conv = ase.units._amu / ase.units._e / \ 

1235 1e-4 * (2 * np.pi)**2 # THz**2 to eV**2 

1236 # VASP6 uses factor 2pi 

1237 # 1e-4 = (angstrom to meter times Hz to THz) squared 

1238 # = (1e10 times 1e-12)**2 

1239 break 

1240 else: # Catch changes in VASP 

1241 vasp_version_error_msg = ( 

1242 f'The file "{file}" is from a ' 

1243 'non-supported VASP version. ' 

1244 'Not sure what unit the Hessian ' 

1245 'is in, aborting.') 

1246 raise calculator.ReadError(vasp_version_error_msg) 

1247 

1248 else: 

1249 conv = 1.0 # VASP version <6 unit is meV**2 

1250 assert isinstance(hessian, np.ndarray) 

1251 hessian *= conv 

1252 if hessian is None: 

1253 raise ElementTree.ParseError("Hessian is None!") 

1254 

1255 except ElementTree.ParseError as exc: 

1256 incomplete_msg = ( 

1257 f'The file "{file}" is incomplete, ' 

1258 'and no DFT data was available. ' 

1259 'This is likely due to an incomplete calculation.') 

1260 raise calculator.ReadError(incomplete_msg) from exc 

1261 # VASP uses the negative definition of the hessian compared to ASE 

1262 return -hessian 

1263 

1264 def get_vibrations(self) -> VibrationsData: 

1265 """Get a VibrationsData Object from a VASP Calculation. 

1266 

1267 Returns: 

1268 VibrationsData object. 

1269 

1270 Note that the atoms in the VibrationsData object can be resorted. 

1271 

1272 Uses the (mass weighted) Hessian from vasprun.xml, different masses 

1273 in the POTCAR can therefore result in different results. 

1274 

1275 Note the limitations concerning k-points and symmetry mentioned in 

1276 the VASP-Wiki. 

1277 """ 

1278 

1279 mass_weighted_hessian = self._read_massweighted_hessian_xml() 

1280 # get indices of freely moving atoms, i.e. respect constraints. 

1281 indices = VibrationsData.indices_from_constraints(self.atoms) 

1282 # save the corresponding sorted atom numbers 

1283 sort_indices = np.array(self.sort)[indices] 

1284 # mass weights = 1/sqrt(mass) 

1285 mass_weights = np.repeat(self.atoms.get_masses()[sort_indices]**-0.5, 3) 

1286 # get the unweighted hessian = H_w / m_w / m_w^T 

1287 # ugly and twice the work, but needed since vasprun.xml does 

1288 # not have the unweighted ase.vibrations.vibration will do the 

1289 # opposite in Vibrations.read 

1290 hessian = mass_weighted_hessian / \ 

1291 mass_weights / mass_weights[:, np.newaxis] 

1292 

1293 return VibrationsData.from_2d(self.atoms[self.sort], hessian, indices) 

1294 

1295 def get_nonselfconsistent_energies(self, bee_type): 

1296 """ Method that reads and returns BEE energy contributions 

1297 written in OUTCAR file. 

1298 """ 

1299 assert bee_type == 'beefvdw' 

1300 cmd = 'grep -32 "BEEF xc energy contributions" OUTCAR | tail -32' 

1301 p = os.popen(cmd, 'r') 

1302 s = p.readlines() 

1303 p.close() 

1304 xc = np.array([]) 

1305 for line in s: 

1306 l_ = float(line.split(":")[-1]) 

1307 xc = np.append(xc, l_) 

1308 assert len(xc) == 32 

1309 return xc 

1310 

1311 

1312##################################### 

1313# Below defines helper functions 

1314# for the VASP calculator 

1315##################################### 

1316 

1317 

1318def check_atoms(atoms: ase.Atoms) -> None: 

1319 """Perform checks on the atoms object, to verify that 

1320 it can be run by VASP. 

1321 A CalculatorSetupError error is raised if the atoms are not supported. 

1322 """ 

1323 

1324 # Loop through all check functions 

1325 for check in (check_atoms_type, check_cell, check_pbc): 

1326 check(atoms) 

1327 

1328 

1329def check_cell(atoms: ase.Atoms) -> None: 

1330 """Check if there is a zero unit cell. 

1331 Raises CalculatorSetupError if the cell is wrong. 

1332 """ 

1333 if atoms.cell.rank < 3: 

1334 raise calculator.CalculatorSetupError( 

1335 "The lattice vectors are zero! " 

1336 "This is the default value - please specify a " 

1337 "unit cell.") 

1338 

1339 

1340def check_pbc(atoms: ase.Atoms) -> None: 

1341 """Check if any boundaries are not PBC, as VASP 

1342 cannot handle non-PBC. 

1343 Raises CalculatorSetupError. 

1344 """ 

1345 if not atoms.pbc.all(): 

1346 raise calculator.CalculatorSetupError( 

1347 "Vasp cannot handle non-periodic boundaries. " 

1348 "Please enable all PBC, e.g. atoms.pbc=True") 

1349 

1350 

1351def check_atoms_type(atoms: ase.Atoms) -> None: 

1352 """Check that the passed atoms object is in fact an Atoms object. 

1353 Raises CalculatorSetupError. 

1354 """ 

1355 if not isinstance(atoms, ase.Atoms): 

1356 raise calculator.CalculatorSetupError( 

1357 'Expected an Atoms object, ' 

1358 'instead got object of type {}'.format(type(atoms)))