Coverage for /builds/ase/ase/ase/calculators/vasp/vasp.py: 41.42%

676 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

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

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

5 

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

7Kitchin. The path of the directory containing the pseudopotential 

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

9by the environmental flag $VASP_PP_PATH. 

10 

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

12to a python script looking something like:: 

13 

14 import os 

15 exitcode = os.system('vasp') 

16 

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

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

19 

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

21""" 

22 

23import os 

24import re 

25import subprocess 

26import sys 

27from contextlib import contextmanager 

28from pathlib import Path 

29from typing import Any, Dict, List, Tuple 

30from warnings import warn 

31from xml.etree import ElementTree 

32 

33import numpy as np 

34 

35import ase 

36from ase.calculators import calculator 

37from ase.calculators.calculator import Calculator 

38from ase.calculators.singlepoint import SinglePointDFTCalculator 

39from ase.calculators.vasp.create_input import GenerateVaspInput 

40from ase.config import cfg 

41from ase.io import jsonio, read 

42from ase.utils import PurePath, deprecated 

43from ase.vibrations.data import VibrationsData 

44 

45 

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

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

48 return True 

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

50 

51 

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

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

54 with the Calculator interface. 

55 

56 Parameters: 

57 

58 atoms: object 

59 Attach an atoms object to the calculator. 

60 

61 label: str 

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

63 Default is 'vasp'. 

64 

65 directory: str 

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

67 

68 restart: str or bool 

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

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

71 ``directory`` is used. 

72 

73 txt: bool, None, str or writable object 

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

75 

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

77 

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

79 and the output will be sent to that file. 

80 

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

82 which has a 'write' attribute. 

83 

84 Default is 'vasp.out' 

85 

86 - Examples: 

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

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

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

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

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

92 

93 command: str 

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

95 environment variables. 

96 """ 

97 name = 'vasp' 

98 ase_objtype = 'vasp_calculator' # For JSON storage 

99 

100 # Environment commands 

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

102 

103 implemented_properties = [ 

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

105 'magmom', 'magmoms' 

106 ] 

107 

108 # Can be used later to set some ASE defaults 

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

110 

111 @deprecated( 

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

113 'use "directory" instead.', 

114 category=FutureWarning, 

115 callback=_prohibit_directory_in_label, 

116 ) 

117 def __init__(self, 

118 atoms=None, 

119 restart=None, 

120 directory='.', 

121 label='vasp', 

122 ignore_bad_restart_file=Calculator._deprecated, 

123 command=None, 

124 txt='vasp.out', 

125 **kwargs): 

126 """ 

127 .. deprecated:: 3.19.2 

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

129 use ``directory`` instead. 

130 """ 

131 

132 self._atoms = None 

133 self.results = {} 

134 

135 # Initialize parameter dictionaries 

136 GenerateVaspInput.__init__(self) 

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

138 

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

140 self._xml_calc = None 

141 

142 # Set directory and label 

143 self.directory = directory 

144 if "/" in label: 

145 if self.directory != ".": 

146 msg = ( 

147 'Directory redundantly specified though directory=' 

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

149 '"/" in label.' 

150 ) 

151 raise ValueError(msg) 

152 self.label = label 

153 else: 

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

155 

156 if isinstance(restart, bool): 

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

158 

159 Calculator.__init__( 

160 self, 

161 restart=restart, 

162 ignore_bad_restart_file=ignore_bad_restart_file, 

163 # We already, manually, created the label 

164 label=self.label, 

165 atoms=atoms, 

166 **kwargs) 

167 

168 self.command = command 

169 

170 self._txt = None 

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

172 self.version = None 

173 

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

175 # Do we really still need to enfore this? 

176 

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

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

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

180 # pass 

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

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

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

184 # # used by explicitly setting the pseudopotential set and 

185 # # INCAR keys 

186 # else: 

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

188 

189 def make_command(self, command=None): 

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

191 ASE_VASP_COMMAND, VASP_COMMAND or VASP_SCRIPT. 

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

193 if command: 

194 cmd = command 

195 else: 

196 # Search for the environment commands 

197 for env in self.env_commands: 

198 if env in cfg: 

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

200 if env == 'VASP_SCRIPT': 

201 # Make the system python exe run $VASP_SCRIPT 

202 exe = sys.executable 

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

204 break 

205 else: 

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

207 ' or one of the following environment ' 

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

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

210 raise calculator.CalculatorSetupError(msg) 

211 return cmd 

212 

213 def set(self, **kwargs): 

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

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

216 on remaining inputs for VASP specific keys. 

217 

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

219 without resetting the results in the calculator. 

220 """ 

221 changed_parameters = {} 

222 

223 if 'label' in kwargs: 

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

225 

226 if 'directory' in kwargs: 

227 # str() call to deal with pathlib objects 

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

229 

230 if 'txt' in kwargs: 

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

232 

233 if 'atoms' in kwargs: 

234 atoms = kwargs.pop('atoms') 

235 self.atoms = atoms # Resets results 

236 

237 if 'command' in kwargs: 

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

239 

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

241 

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

243 if changed_parameters: 

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

245 if kwargs: 

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

247 GenerateVaspInput.set(self, **kwargs) 

248 self.results.clear() 

249 

250 def reset(self): 

251 self.atoms = None 

252 self.clear_results() 

253 

254 def clear_results(self): 

255 self.results.clear() 

256 self._xml_calc = None 

257 

258 @contextmanager 

259 def _txt_outstream(self): 

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

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

262 writable object. 

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

264 the new stream again when exiting. 

265 

266 Examples: 

267 # Pass a string 

268 calc.txt = 'vasp.out' 

269 with calc.txt_outstream() as out: 

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

271 

272 # Use an existing stream 

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

274 calc.txt = mystream 

275 with calc.txt_outstream() as out: 

276 calc.run(out=out) 

277 mystream.close() 

278 

279 # Print to stdout 

280 calc.txt = '-' 

281 with calc.txt_outstream() as out: 

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

283 """ 

284 

285 txt = self.txt 

286 open_and_close = False # Do we open the file? 

287 

288 if txt is None: 

289 # Suppress stdout 

290 out = subprocess.DEVNULL 

291 else: 

292 if isinstance(txt, str): 

293 if txt == '-': 

294 # subprocess.call redirects this to stdout 

295 out = None 

296 else: 

297 # Open the file in the work directory 

298 txt = self._indir(txt) 

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

300 # try/finally 

301 open_and_close = True 

302 elif hasattr(txt, 'write'): 

303 out = txt 

304 else: 

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

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

307 

308 try: 

309 if open_and_close: 

310 out = open(txt, 'w') 

311 yield out 

312 finally: 

313 if open_and_close: 

314 out.close() 

315 

316 def calculate(self, 

317 atoms=None, 

318 properties=('energy', ), 

319 system_changes=tuple(calculator.all_changes)): 

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

321 

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

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

324 from the VASP output files. 

325 """ 

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

327 # Check for zero-length lattice vectors and PBC 

328 # and that we actually have an Atoms object. 

329 check_atoms(self.atoms) 

330 

331 self.clear_results() 

332 

333 command = self.make_command(self.command) 

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

335 

336 with self._txt_outstream() as out: 

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

338 out=out, 

339 directory=self.directory) 

340 

341 if errorcode: 

342 raise calculator.CalculationFailed( 

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

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

345 stderr)) 

346 

347 # Read results from calculation 

348 self.update_atoms(atoms) 

349 self.read_results() 

350 

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

352 """Method to explicitly execute VASP""" 

353 if command is None: 

354 command = self.command 

355 if directory is None: 

356 directory = self.directory 

357 result = subprocess.run(command, 

358 shell=True, 

359 cwd=directory, 

360 capture_output=True, 

361 text=True) 

362 if out is not None: 

363 out.write(result.stdout) 

364 return result.returncode, result.stderr 

365 

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

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

368 def compare_dict(d1, d2): 

369 """Helper function to compare dictionaries""" 

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

371 # for python 2.7 compatibility 

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

373 return False 

374 

375 # Check for differences in values 

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

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

378 return False 

379 return True 

380 

381 # First we check for default changes 

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

383 

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

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

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

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

388 if not compare_dict(param_dict, old_dict): 

389 system_changes.append(param_string) 

390 

391 return system_changes 

392 

393 def _store_param_state(self): 

394 """Store current parameter state""" 

395 self.param_state = dict( 

396 float_params=self.float_params.copy(), 

397 exp_params=self.exp_params.copy(), 

398 string_params=self.string_params.copy(), 

399 int_params=self.int_params.copy(), 

400 input_params=self.input_params.copy(), 

401 bool_params=self.bool_params.copy(), 

402 list_int_params=self.list_int_params.copy(), 

403 list_bool_params=self.list_bool_params.copy(), 

404 list_float_params=self.list_float_params.copy(), 

405 dict_params=self.dict_params.copy(), 

406 special_params=self.special_params.copy()) 

407 

408 def asdict(self): 

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

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

411 ``directory`` keywords. 

412 Contains the following keys: 

413 

414 - ``ase_version`` 

415 - ``vasp_version`` 

416 - ``inputs`` 

417 - ``results`` 

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

419 """ 

420 # Get versions 

421 asevers = ase.__version__ 

422 vaspvers = self.get_version() 

423 

424 self._store_param_state() # Update param state 

425 # Store input parameters which have been set 

426 inputs = { 

427 key: value 

428 for param_dct in self.param_state.values() 

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

430 } 

431 

432 dct = { 

433 'ase_version': asevers, 

434 'vasp_version': vaspvers, 

435 # '__ase_objtype__': self.ase_objtype, 

436 'inputs': inputs, 

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

438 } 

439 

440 if self.atoms: 

441 # Encode atoms as dict 

442 from ase.db.row import atoms2dict 

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

444 

445 return dct 

446 

447 def fromdict(self, dct): 

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

449 dictionary. 

450 

451 Parameters: 

452 

453 dct: Dictionary 

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

455 """ 

456 if 'vasp_version' in dct: 

457 self.version = dct['vasp_version'] 

458 if 'inputs' in dct: 

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

460 self._store_param_state() 

461 if 'atoms' in dct: 

462 from ase.db.row import AtomsRow 

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

464 self.atoms = atoms 

465 if 'results' in dct: 

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

467 

468 def write_json(self, filename): 

469 """Dump calculator state to JSON file. 

470 

471 Parameters: 

472 

473 filename: string 

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

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

476 """ 

477 filename = self._indir(filename) 

478 dct = self.asdict() 

479 jsonio.write_json(filename, dct) 

480 

481 def read_json(self, filename): 

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

483 dct = jsonio.read_json(filename) 

484 self.fromdict(dct) 

485 

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

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

488 self.initialize(atoms) 

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

490 

491 def read(self, label=None): 

492 """Read results from VASP output files. 

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

494 Raises ReadError if they are not found""" 

495 if label is None: 

496 label = self.label 

497 Calculator.read(self, label) 

498 

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

500 if self.parameters is None: 

501 self.parameters = self.get_default_parameters() 

502 

503 # Check for existence of the necessary output files 

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

505 file = self._indir(f) 

506 if not file.is_file(): 

507 raise calculator.ReadError( 

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

509 

510 # Build sorting and resorting lists 

511 self.read_sort() 

512 

513 # Read atoms 

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

515 

516 # Read parameters 

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

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

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

520 

521 # Read the results from the calculation 

522 self.read_results() 

523 

524 def _indir(self, filename): 

525 """Prepend current directory to filename""" 

526 return Path(self.directory) / filename 

527 

528 def read_sort(self): 

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

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

531 """ 

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

533 if os.path.isfile(sortfile): 

534 self.sort = [] 

535 self.resort = [] 

536 with open(sortfile) as fd: 

537 for line in fd: 

538 sort, resort = line.split() 

539 self.sort.append(int(sort)) 

540 self.resort.append(int(resort)) 

541 else: 

542 # Redo the sorting 

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

544 self.initialize(atoms) 

545 

546 def read_atoms(self, filename): 

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

548 working directory. Normally called CONTCAR.""" 

549 return read(filename)[self.resort] 

550 

551 def update_atoms(self, atoms): 

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

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

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

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

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

557 # from CONTCAR. 

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

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

560 atoms.cell = atoms_sorted.cell 

561 

562 self.atoms = atoms # Creates a copy 

563 

564 def read_results(self): 

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

566 # Temporarily load OUTCAR into memory 

567 outcar = self.load_file('OUTCAR') 

568 

569 # Read the data we can from vasprun.xml 

570 calc_xml = self._read_xml() 

571 xml_results = calc_xml.results 

572 

573 # Fix sorting 

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

575 

576 self.results.update(xml_results) 

577 

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

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

580 # is relatively slow 

581 # Removed for now 

582 # self.read_outcar(lines=outcar) 

583 

584 # Update results dict with results from OUTCAR 

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

586 # the vasprun.xml file. 

587 

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

589 self.version = self.read_version() 

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

591 dipole = self.read_dipole(lines=outcar) 

592 nbands = self.read_nbands(lines=outcar) 

593 self.results.update( 

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

595 

596 # Stress is not always present. 

597 # Prevent calculation from going into a loop 

598 if 'stress' not in self.results: 

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

600 

601 self._set_old_keywords() 

602 

603 # Store the parameters used for this calculation 

604 self._store_param_state() 

605 

606 def _set_old_keywords(self): 

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

608 self.spinpol = self.get_spin_polarized() 

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

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

611 self.forces = self.get_forces() 

612 self.fermi = self.get_fermi_level() 

613 self.dipole = self.get_dipole_moment() 

614 # Prevent calculation from going into a loop 

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

616 self.nbands = self.get_number_of_bands() 

617 

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

619 @property 

620 def kpts(self): 

621 """Access the kpts from input_params dict""" 

622 return self.input_params['kpts'] 

623 

624 @kpts.setter 

625 def kpts(self, kpts): 

626 """Set kpts in input_params dict""" 

627 self.input_params['kpts'] = kpts 

628 

629 @property 

630 def encut(self): 

631 """Direct access to the encut parameter""" 

632 return self.float_params['encut'] 

633 

634 @encut.setter 

635 def encut(self, encut): 

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

637 self.set(encut=encut) 

638 

639 @property 

640 def xc(self): 

641 """Direct access to the xc parameter""" 

642 return self.get_xc_functional() 

643 

644 @xc.setter 

645 def xc(self, xc): 

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

647 self.set(xc=xc) 

648 

649 @property 

650 def atoms(self): 

651 return self._atoms 

652 

653 @atoms.setter 

654 def atoms(self, atoms): 

655 if atoms is None: 

656 self._atoms = None 

657 self.clear_results() 

658 else: 

659 if self.check_state(atoms): 

660 self.clear_results() 

661 self._atoms = atoms.copy() 

662 

663 def load_file(self, filename): 

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

665 

666 Example: 

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

668 >>> calc = Vasp() 

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

670 """ 

671 filename = self._indir(filename) 

672 with open(filename) as fd: 

673 return fd.readlines() 

674 

675 @contextmanager 

676 def load_file_iter(self, filename): 

677 """Return a file iterator""" 

678 

679 filename = self._indir(filename) 

680 with open(filename) as fd: 

681 yield fd 

682 

683 def read_outcar(self, lines=None): 

684 """Read results from the OUTCAR file. 

685 Deprecated, see read_results()""" 

686 if not lines: 

687 lines = self.load_file('OUTCAR') 

688 # Spin polarized calculation? 

689 self.spinpol = self.get_spin_polarized() 

690 

691 self.version = self.get_version() 

692 

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

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

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

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

697 

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

699 

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

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

702 

703 self.read_ldau() 

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

705 lines=lines) 

706 

707 def _read_xml(self) -> SinglePointDFTCalculator: 

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

709 Returns calculator from the xml file. 

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

711 """ 

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

713 incomplete_msg = ( 

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

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

716 try: 

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

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

719 assert isinstance(_xml_atoms, ase.Atoms) 

720 except ElementTree.ParseError as exc: 

721 raise calculator.ReadError(incomplete_msg) from exc 

722 

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

724 raise calculator.ReadError(incomplete_msg) 

725 

726 self._xml_calc = _xml_atoms.calc 

727 return self._xml_calc 

728 

729 @property 

730 def _xml_calc(self) -> SinglePointDFTCalculator: 

731 if self.__xml_calc is None: 

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

733 'Run read_results() first.') 

734 return self.__xml_calc 

735 

736 @_xml_calc.setter 

737 def _xml_calc(self, value): 

738 self.__xml_calc = value 

739 

740 def get_ibz_k_points(self): 

741 calc = self._xml_calc 

742 return calc.get_ibz_k_points() 

743 

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

745 calc = self._xml_calc 

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

747 

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

749 calc = self._xml_calc 

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

751 

752 def get_fermi_level(self): 

753 calc = self._xml_calc 

754 return calc.get_fermi_level() 

755 

756 def get_homo_lumo(self): 

757 calc = self._xml_calc 

758 return calc.get_homo_lumo() 

759 

760 def get_homo_lumo_by_spin(self, spin=0): 

761 calc = self._xml_calc 

762 return calc.get_homo_lumo_by_spin(spin=spin) 

763 

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

765 calc = self._xml_calc 

766 return calc.get_occupation_numbers(kpt, spin) 

767 

768 def get_spin_polarized(self): 

769 calc = self._xml_calc 

770 return calc.get_spin_polarized() 

771 

772 def get_number_of_spins(self): 

773 calc = self._xml_calc 

774 return calc.get_number_of_spins() 

775 

776 def get_number_of_bands(self): 

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

778 

779 def get_number_of_electrons(self, lines=None): 

780 if not lines: 

781 lines = self.load_file('OUTCAR') 

782 

783 nelect = None 

784 for line in lines: 

785 if 'total number of electrons' in line: 

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

787 break 

788 return nelect 

789 

790 def get_k_point_weights(self): 

791 filename = 'IBZKPT' 

792 return self.read_k_point_weights(filename) 

793 

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

795 """ 

796 The total DOS. 

797 

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

799 (energies, dos). 

800 """ 

801 from ase.dft.dos import DOS 

802 dos = DOS(self, **kwargs) 

803 e = dos.get_energies() 

804 d = dos.get_dos(spin=spin) 

805 return e, d 

806 

807 def get_version(self): 

808 if self.version is None: 

809 # Try if we can read the version number 

810 self.version = self.read_version() 

811 return self.version 

812 

813 def read_version(self): 

814 """Get the VASP version number""" 

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

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

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

818 return None 

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

820 for line in lines: 

821 if ' vasp.' in line: 

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

823 # We didn't find the version in VASP 

824 return None 

825 

826 def get_number_of_iterations(self): 

827 return self.read_number_of_iterations() 

828 

829 def read_number_of_iterations(self): 

830 niter = None 

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

832 for line in lines: 

833 # find the last iteration number 

834 if '- Iteration' in line: 

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

836 return niter 

837 

838 def read_number_of_ionic_steps(self): 

839 niter = None 

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

841 for line in lines: 

842 if '- Iteration' in line: 

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

844 return niter 

845 

846 def read_stress(self, lines=None): 

847 """Read stress from OUTCAR. 

848 

849 Depreciated: Use get_stress() instead. 

850 """ 

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

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

853 if not lines: 

854 lines = self.load_file('OUTCAR') 

855 

856 stress = None 

857 for line in lines: 

858 if ' in kB ' in line: 

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

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

861 return stress 

862 

863 def read_ldau(self, lines=None): 

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

865 if not lines: 

866 lines = self.load_file('OUTCAR') 

867 

868 ldau_luj = None 

869 ldauprint = None 

870 ldau = None 

871 ldautype = None 

872 atomtypes = [] 

873 # read ldau parameters from outcar 

874 for line in lines: 

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

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

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

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

879 ldau = True 

880 ldau_luj = {} 

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

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

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

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

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

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

887 # create dictionary 

888 if ldau: 

889 for i, symbol in enumerate(atomtypes): 

890 ldau_luj[symbol] = { 

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

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

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

894 } 

895 self.dict_params['ldau_luj'] = ldau_luj 

896 

897 self.ldau = ldau 

898 self.ldauprint = ldauprint 

899 self.ldautype = ldautype 

900 self.ldau_luj = ldau_luj 

901 return ldau, ldauprint, ldautype, ldau_luj 

902 

903 def get_xc_functional(self): 

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

905 

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

907 Otherwise, the XC functional associated with the 

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

909 The string is always cast to uppercase for consistency 

910 in checks.""" 

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

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

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

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

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

916 

917 # Methods for reading information from OUTCAR files: 

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

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

920 Depreciated: use get_potential_energy() instead""" 

921 if not lines: 

922 lines = self.load_file('OUTCAR') 

923 

924 [energy_free, energy_zero] = [0, 0] 

925 if all: 

926 energy_free = [] 

927 energy_zero = [] 

928 for line in lines: 

929 # Free energy 

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

931 if all: 

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

933 else: 

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

935 # Extrapolated zero point energy 

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

937 if all: 

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

939 else: 

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

941 return [energy_free, energy_zero] 

942 

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

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

945 

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

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

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

949 

950 if not lines: 

951 lines = self.load_file('OUTCAR') 

952 

953 if all: 

954 all_forces = [] 

955 

956 for n, line in enumerate(lines): 

957 if 'TOTAL-FORCE' in line: 

958 forces = [] 

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

960 forces.append( 

961 np.array( 

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

963 

964 if all: 

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

966 

967 if all: 

968 return np.array(all_forces) 

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

970 

971 def read_fermi(self, lines=None): 

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

973 if not lines: 

974 lines = self.load_file('OUTCAR') 

975 

976 E_f = None 

977 for line in lines: 

978 if 'E-fermi' in line: 

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

980 return E_f 

981 

982 def read_dipole(self, lines=None): 

983 """Read dipole from OUTCAR""" 

984 if not lines: 

985 lines = self.load_file('OUTCAR') 

986 

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

988 for line in lines: 

989 if 'dipolmoment' in line: 

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

991 return dipolemoment 

992 

993 def read_mag(self, lines=None): 

994 if not lines: 

995 lines = self.load_file('OUTCAR') 

996 p = self.int_params 

997 q = self.list_float_params 

998 if self.spinpol: 

999 magnetic_moment = self._read_magnetic_moment(lines=lines) 

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

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

1002 magnetic_moments = self._read_magnetic_moments(lines=lines) 

1003 else: 

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

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

1006 ' to get information on magnetic moments') 

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

1008 else: 

1009 magnetic_moment = 0.0 

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

1011 return magnetic_moment, magnetic_moments 

1012 

1013 def _read_magnetic_moments(self, lines=None): 

1014 """Read magnetic moments from OUTCAR. 

1015 Only reads the last occurrence. """ 

1016 if not lines: 

1017 lines = self.load_file('OUTCAR') 

1018 

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

1020 magstr = 'magnetization (x)' 

1021 

1022 # Search for the last occurrence 

1023 nidx = -1 

1024 for n, line in enumerate(lines): 

1025 if magstr in line: 

1026 nidx = n 

1027 

1028 # Read that occurrence 

1029 if nidx > -1: 

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

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

1032 return magnetic_moments[self.resort] 

1033 

1034 def _read_magnetic_moment(self, lines=None): 

1035 """Read magnetic moment from OUTCAR""" 

1036 if not lines: 

1037 lines = self.load_file('OUTCAR') 

1038 

1039 for line in lines: 

1040 if 'number of electron ' in line: 

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

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

1043 return magnetic_moment 

1044 

1045 def read_nbands(self, lines=None): 

1046 """Read number of bands from OUTCAR""" 

1047 if not lines: 

1048 lines = self.load_file('OUTCAR') 

1049 

1050 for line in lines: 

1051 line = self.strip_warnings(line) 

1052 if 'NBANDS' in line: 

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

1054 return None 

1055 

1056 def read_convergence(self, lines=None): 

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

1058 if not lines: 

1059 lines = self.load_file('OUTCAR') 

1060 

1061 converged = None 

1062 # First check electronic convergence 

1063 for line in lines: 

1064 # VASP 6 actually labels loop exit reason 

1065 if 'aborting loop' in line: 

1066 converged = 'because EDIFF is reached' in line 

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

1068 # and 

1069 # 'because hard stop was set' 

1070 # will result in unconverged 

1071 break 

1072 # determine convergence by attempting to reproduce VASP's 

1073 # internal logic 

1074 if 'EDIFF ' in line: 

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

1076 if 'total energy-change' in line: 

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

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

1079 if 'MIXING' in line: 

1080 continue 

1081 split = line.split(':') 

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

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

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

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

1086 # we are checking still the first number so 

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

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

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

1090 bsplit = b.split('-') 

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

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

1093 b = float(b) 

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

1095 converged = True 

1096 else: 

1097 converged = False 

1098 continue 

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

1100 # condition been fulfilled 

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

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

1103 if not self.read_relaxed(): 

1104 converged = False 

1105 else: 

1106 converged = True 

1107 return converged 

1108 

1109 def read_k_point_weights(self, filename): 

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

1111 

1112 lines = self.load_file(filename) 

1113 

1114 if 'Tetrahedra\n' in lines: 

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

1116 else: 

1117 N = len(lines) 

1118 kpt_weights = [] 

1119 for n in range(3, N): 

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

1121 kpt_weights = np.array(kpt_weights) 

1122 kpt_weights /= np.sum(kpt_weights) 

1123 

1124 return kpt_weights 

1125 

1126 def read_relaxed(self, lines=None): 

1127 """Check if ionic relaxation completed""" 

1128 if not lines: 

1129 lines = self.load_file('OUTCAR') 

1130 for line in lines: 

1131 if 'reached required accuracy' in line: 

1132 return True 

1133 return False 

1134 

1135 def read_spinpol(self, lines=None): 

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

1137 

1138 Depreciated: Use get_spin_polarized() instead. 

1139 """ 

1140 if not lines: 

1141 lines = self.load_file('OUTCAR') 

1142 

1143 for line in lines: 

1144 if 'ISPIN' in line: 

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

1146 self.spinpol = True 

1147 else: 

1148 self.spinpol = False 

1149 return self.spinpol 

1150 

1151 def strip_warnings(self, line): 

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

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

1154 return "" 

1155 return line 

1156 

1157 @property 

1158 def txt(self): 

1159 return self._txt 

1160 

1161 @txt.setter 

1162 def txt(self, txt): 

1163 if isinstance(txt, PurePath): 

1164 txt = str(txt) 

1165 self._txt = txt 

1166 

1167 def get_number_of_grid_points(self): 

1168 raise NotImplementedError 

1169 

1170 def get_pseudo_density(self): 

1171 raise NotImplementedError 

1172 

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

1174 raise NotImplementedError 

1175 

1176 def get_bz_k_points(self): 

1177 raise NotImplementedError 

1178 

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

1180 """Read vibrational frequencies. 

1181 

1182 Returns: 

1183 List of real and list of imaginary frequencies 

1184 (imaginary number as real number). 

1185 """ 

1186 freq = [] 

1187 i_freq = [] 

1188 

1189 if not lines: 

1190 lines = self.load_file('OUTCAR') 

1191 

1192 for line in lines: 

1193 data = line.split() 

1194 if 'THz' in data: 

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

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

1197 else: 

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

1199 return freq, i_freq 

1200 

1201 def _read_massweighted_hessian_xml(self): 

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

1203 

1204 Returns: 

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

1206 

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

1208 

1209 Converts to ASE units for VASP version 6. 

1210 """ 

1211 

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

1213 try: 

1214 tree = ElementTree.iterparse(file) 

1215 hessian = None 

1216 for event, elem in tree: 

1217 if elem.tag == 'dynmat': 

1218 for i, entry in enumerate( 

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

1220 text_split = entry.text.split() 

1221 if not text_split: 

1222 raise ElementTree.ParseError( 

1223 "Could not find varray hessian!") 

1224 if i == 0: 

1225 n_items = len(text_split) 

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

1227 assert isinstance(hessian, np.ndarray) 

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

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

1230 if i != n_items - 1: 

1231 raise ElementTree.ParseError( 

1232 "Hessian is not quadratic!") 

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

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

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

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

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

1238 # VASP6 uses factor 2pi 

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

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

1241 break 

1242 else: # Catch changes in VASP 

1243 vasp_version_error_msg = ( 

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

1245 'non-supported VASP version. ' 

1246 'Not sure what unit the Hessian ' 

1247 'is in, aborting.') 

1248 raise calculator.ReadError(vasp_version_error_msg) 

1249 

1250 else: 

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

1252 assert isinstance(hessian, np.ndarray) 

1253 hessian *= conv 

1254 if hessian is None: 

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

1256 

1257 except ElementTree.ParseError as exc: 

1258 incomplete_msg = ( 

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

1260 'and no DFT data was available. ' 

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

1262 raise calculator.ReadError(incomplete_msg) from exc 

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

1264 return -hessian 

1265 

1266 def get_vibrations(self) -> VibrationsData: 

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

1268 

1269 Returns: 

1270 VibrationsData object. 

1271 

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

1273 

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

1275 in the POTCAR can therefore result in different results. 

1276 

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

1278 the VASP-Wiki. 

1279 """ 

1280 

1281 mass_weighted_hessian = self._read_massweighted_hessian_xml() 

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

1283 indices = VibrationsData.indices_from_constraints(self.atoms) 

1284 # save the corresponding sorted atom numbers 

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

1286 # mass weights = 1/sqrt(mass) 

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

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

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

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

1291 # opposite in Vibrations.read 

1292 hessian = mass_weighted_hessian / \ 

1293 mass_weights / mass_weights[:, np.newaxis] 

1294 

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

1296 

1297 def get_nonselfconsistent_energies(self, bee_type): 

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

1299 written in OUTCAR file. 

1300 """ 

1301 assert bee_type == 'beefvdw' 

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

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

1304 s = p.readlines() 

1305 p.close() 

1306 xc = np.array([]) 

1307 for line in s: 

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

1309 xc = np.append(xc, l_) 

1310 assert len(xc) == 32 

1311 return xc 

1312 

1313 

1314##################################### 

1315# Below defines helper functions 

1316# for the VASP calculator 

1317##################################### 

1318 

1319 

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

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

1322 it can be run by VASP. 

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

1324 """ 

1325 

1326 # Loop through all check functions 

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

1328 check(atoms) 

1329 

1330 

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

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

1333 Raises CalculatorSetupError if the cell is wrong. 

1334 """ 

1335 if atoms.cell.rank < 3: 

1336 raise calculator.CalculatorSetupError( 

1337 "The lattice vectors are zero! " 

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

1339 "unit cell.") 

1340 

1341 

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

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

1344 cannot handle non-PBC. 

1345 Raises CalculatorSetupError. 

1346 """ 

1347 if not atoms.pbc.all(): 

1348 raise calculator.CalculatorSetupError( 

1349 "Vasp cannot handle non-periodic boundaries. " 

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

1351 

1352 

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

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

1355 Raises CalculatorSetupError. 

1356 """ 

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

1358 raise calculator.CalculatorSetupError( 

1359 'Expected an Atoms object, ' 

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