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

676 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-30 08:22 +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 

20https://www.vasp.at/ 

21""" 

22 

23import os 

24import re 

25import subprocess 

26import sys 

27from contextlib import contextmanager 

28from pathlib import Path 

29from typing import Any 

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 

59 atoms: object 

60 Attach an atoms object to the calculator. 

61 

62 label: str 

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

64 Default is 'vasp'. 

65 

66 directory: str 

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

68 

69 restart: str or bool 

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

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

72 ``directory`` is used. 

73 

74 txt: bool, None, str or writable object 

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

76 

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

78 

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

80 and the output will be sent to that file. 

81 

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

83 which has a 'write' attribute. 

84 

85 Default is 'vasp.out' 

86 

87 - Examples: 

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

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

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

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

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

93 

94 command: str 

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

96 environment variables. 

97 """ 

98 name = 'vasp' 

99 ase_objtype = 'vasp_calculator' # For JSON storage 

100 

101 # Environment commands 

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

103 

104 implemented_properties = [ 

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

106 'magmom', 'magmoms' 

107 ] 

108 

109 # Can be used later to set some ASE defaults 

110 default_parameters: dict[str, Any] = {} 

111 

112 @deprecated( 

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

114 'use "directory" instead.', 

115 category=FutureWarning, 

116 callback=_prohibit_directory_in_label, 

117 ) 

118 def __init__(self, 

119 atoms=None, 

120 restart=None, 

121 directory='.', 

122 label='vasp', 

123 ignore_bad_restart_file=Calculator._deprecated, 

124 command=None, 

125 txt='vasp.out', 

126 **kwargs): 

127 """ 

128 .. deprecated:: 3.19.2 

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

130 use ``directory`` instead. 

131 """ 

132 

133 self._atoms = None 

134 self.results = {} 

135 

136 # Initialize parameter dictionaries 

137 GenerateVaspInput.__init__(self) 

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

139 

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

141 self._xml_calc = None 

142 

143 # Set directory and label 

144 self.directory = directory 

145 if "/" in label: 

146 if self.directory != ".": 

147 msg = ( 

148 'Directory redundantly specified though directory=' 

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

150 '"/" in label.' 

151 ) 

152 raise ValueError(msg) 

153 self.label = label 

154 else: 

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

156 

157 if isinstance(restart, bool): 

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

159 

160 Calculator.__init__( 

161 self, 

162 restart=restart, 

163 ignore_bad_restart_file=ignore_bad_restart_file, 

164 # We already, manually, created the label 

165 label=self.label, 

166 atoms=atoms, 

167 **kwargs) 

168 

169 self.command = command 

170 

171 self._txt = None 

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

173 self.version = None 

174 

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

176 # Do we really still need to enfore this? 

177 

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

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

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

181 # pass 

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

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

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

185 # # used by explicitly setting the pseudopotential set and 

186 # # INCAR keys 

187 # else: 

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

189 

190 def make_command(self, command=None): 

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

192 ASE_VASP_COMMAND, VASP_COMMAND or VASP_SCRIPT. 

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

194 if command: 

195 cmd = command 

196 else: 

197 # Search for the environment commands 

198 for env in self.env_commands: 

199 if env in cfg: 

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

201 if env == 'VASP_SCRIPT': 

202 # Make the system python exe run $VASP_SCRIPT 

203 exe = sys.executable 

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

205 break 

206 else: 

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

208 ' or one of the following environment ' 

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

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

211 raise calculator.CalculatorSetupError(msg) 

212 return cmd 

213 

214 def set(self, **kwargs): 

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

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

217 on remaining inputs for VASP specific keys. 

218 

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

220 without resetting the results in the calculator. 

221 """ 

222 changed_parameters = {} 

223 

224 if 'label' in kwargs: 

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

226 

227 if 'directory' in kwargs: 

228 # str() call to deal with pathlib objects 

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

230 

231 if 'txt' in kwargs: 

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

233 

234 if 'atoms' in kwargs: 

235 atoms = kwargs.pop('atoms') 

236 self.atoms = atoms # Resets results 

237 

238 if 'command' in kwargs: 

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

240 

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

242 

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

244 if changed_parameters: 

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

246 if kwargs: 

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

248 GenerateVaspInput.set(self, **kwargs) 

249 self.results.clear() 

250 

251 def reset(self): 

252 self.atoms = None 

253 self.clear_results() 

254 

255 def clear_results(self): 

256 self.results.clear() 

257 self._xml_calc = None 

258 

259 @contextmanager 

260 def _txt_outstream(self): 

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

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

263 writable object. 

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

265 the new stream again when exiting. 

266 

267 Examples 

268 -------- 

269 # Pass a string 

270 calc.txt = 'vasp.out' 

271 with calc.txt_outstream() as out: 

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

273 

274 # Use an existing stream 

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

276 calc.txt = mystream 

277 with calc.txt_outstream() as out: 

278 calc.run(out=out) 

279 mystream.close() 

280 

281 # Print to stdout 

282 calc.txt = '-' 

283 with calc.txt_outstream() as out: 

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

285 """ 

286 

287 txt = self.txt 

288 open_and_close = False # Do we open the file? 

289 

290 if txt is None: 

291 # Suppress stdout 

292 out = subprocess.DEVNULL 

293 else: 

294 if isinstance(txt, str): 

295 if txt == '-': 

296 # subprocess.call redirects this to stdout 

297 out = None 

298 else: 

299 # Open the file in the work directory 

300 txt = self._indir(txt) 

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

302 # try/finally 

303 open_and_close = True 

304 elif hasattr(txt, 'write'): 

305 out = txt 

306 else: 

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

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

309 

310 try: 

311 if open_and_close: 

312 out = open(txt, 'w') 

313 yield out 

314 finally: 

315 if open_and_close: 

316 out.close() 

317 

318 def calculate(self, 

319 atoms=None, 

320 properties=('energy', ), 

321 system_changes=tuple(calculator.all_changes)): 

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

323 

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

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

326 from the VASP output files. 

327 """ 

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

329 # Check for zero-length lattice vectors and PBC 

330 # and that we actually have an Atoms object. 

331 check_atoms(self.atoms) 

332 

333 self.clear_results() 

334 

335 command = self.make_command(self.command) 

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

337 

338 with self._txt_outstream() as out: 

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

340 out=out, 

341 directory=self.directory) 

342 

343 if errorcode: 

344 raise calculator.CalculationFailed( 

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

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

347 stderr)) 

348 

349 # Read results from calculation 

350 self.update_atoms(atoms) 

351 self.read_results() 

352 

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

354 """Method to explicitly execute VASP""" 

355 if command is None: 

356 command = self.command 

357 if directory is None: 

358 directory = self.directory 

359 result = subprocess.run(command, 

360 shell=True, 

361 cwd=directory, 

362 capture_output=True, 

363 text=True) 

364 if out is not None: 

365 out.write(result.stdout) 

366 return result.returncode, result.stderr 

367 

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

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

370 def compare_dict(d1, d2): 

371 """Helper function to compare dictionaries""" 

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

373 # for python 2.7 compatibility 

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

375 return False 

376 

377 # Check for differences in values 

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

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

380 return False 

381 return True 

382 

383 # First we check for default changes 

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

385 

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

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

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

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

390 if not compare_dict(param_dict, old_dict): 

391 system_changes.append(param_string) 

392 

393 return system_changes 

394 

395 def _store_param_state(self): 

396 """Store current parameter state""" 

397 self.param_state = dict( 

398 float_params=self.float_params.copy(), 

399 exp_params=self.exp_params.copy(), 

400 string_params=self.string_params.copy(), 

401 int_params=self.int_params.copy(), 

402 input_params=self.input_params.copy(), 

403 bool_params=self.bool_params.copy(), 

404 list_int_params=self.list_int_params.copy(), 

405 list_bool_params=self.list_bool_params.copy(), 

406 list_float_params=self.list_float_params.copy(), 

407 dict_params=self.dict_params.copy(), 

408 special_params=self.special_params.copy()) 

409 

410 def asdict(self): 

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

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

413 ``directory`` keywords. 

414 Contains the following keys: 

415 

416 - ``ase_version`` 

417 - ``vasp_version`` 

418 - ``inputs`` 

419 - ``results`` 

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

421 """ 

422 # Get versions 

423 asevers = ase.__version__ 

424 vaspvers = self.get_version() 

425 

426 self._store_param_state() # Update param state 

427 # Store input parameters which have been set 

428 inputs = { 

429 key: value 

430 for param_dct in self.param_state.values() 

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

432 } 

433 

434 dct = { 

435 'ase_version': asevers, 

436 'vasp_version': vaspvers, 

437 # '__ase_objtype__': self.ase_objtype, 

438 'inputs': inputs, 

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

440 } 

441 

442 if self.atoms: 

443 # Encode atoms as dict 

444 from ase.db.row import atoms2dict 

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

446 

447 return dct 

448 

449 def fromdict(self, dct): 

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

451 dictionary. 

452 

453 Parameters 

454 ---------- 

455 

456 dct: Dictionary 

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

458 """ 

459 if 'vasp_version' in dct: 

460 self.version = dct['vasp_version'] 

461 if 'inputs' in dct: 

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

463 self._store_param_state() 

464 if 'atoms' in dct: 

465 from ase.db.row import AtomsRow 

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

467 self.atoms = atoms 

468 if 'results' in dct: 

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

470 

471 def write_json(self, filename): 

472 """Dump calculator state to JSON file. 

473 

474 Parameters 

475 ---------- 

476 

477 filename: string 

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

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

480 """ 

481 filename = self._indir(filename) 

482 dct = self.asdict() 

483 jsonio.write_json(filename, dct) 

484 

485 def read_json(self, filename): 

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

487 dct = jsonio.read_json(filename) 

488 self.fromdict(dct) 

489 

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

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

492 self.initialize(atoms) 

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

494 

495 def read(self, label=None): 

496 """Read results from VASP output files. 

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

498 Raises ReadError if they are not found""" 

499 if label is None: 

500 label = self.label 

501 Calculator.read(self, label) 

502 

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

504 if self.parameters is None: 

505 self.parameters = self.get_default_parameters() 

506 

507 # Check for existence of the necessary output files 

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

509 file = self._indir(f) 

510 if not file.is_file(): 

511 raise calculator.ReadError( 

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

513 

514 # Build sorting and resorting lists 

515 self.read_sort() 

516 

517 # Read atoms 

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

519 

520 # Read parameters 

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

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

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

524 

525 # Read the results from the calculation 

526 self.read_results() 

527 

528 def _indir(self, filename): 

529 """Prepend current directory to filename""" 

530 return Path(self.directory) / filename 

531 

532 def read_sort(self): 

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

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

535 """ 

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

537 if os.path.isfile(sortfile): 

538 self.sort = [] 

539 self.resort = [] 

540 with open(sortfile) as fd: 

541 for line in fd: 

542 sort, resort = line.split() 

543 self.sort.append(int(sort)) 

544 self.resort.append(int(resort)) 

545 else: 

546 # Redo the sorting 

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

548 self.initialize(atoms) 

549 

550 def read_atoms(self, filename): 

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

552 working directory. Normally called CONTCAR.""" 

553 return read(filename)[self.resort] 

554 

555 def update_atoms(self, atoms): 

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

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

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

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

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

561 # from CONTCAR. 

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

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

564 atoms.cell = atoms_sorted.cell 

565 

566 self.atoms = atoms # Creates a copy 

567 

568 def read_results(self): 

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

570 # Temporarily load OUTCAR into memory 

571 outcar = self.load_file('OUTCAR') 

572 

573 # Read the data we can from vasprun.xml 

574 calc_xml = self._read_xml() 

575 xml_results = calc_xml.results 

576 

577 # Fix sorting 

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

579 

580 self.results.update(xml_results) 

581 

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

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

584 # is relatively slow 

585 # Removed for now 

586 # self.read_outcar(lines=outcar) 

587 

588 # Update results dict with results from OUTCAR 

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

590 # the vasprun.xml file. 

591 

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

593 self.version = self.read_version() 

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

595 dipole = self.read_dipole(lines=outcar) 

596 nbands = self.read_nbands(lines=outcar) 

597 self.results.update( 

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

599 

600 # Stress is not always present. 

601 # Prevent calculation from going into a loop 

602 if 'stress' not in self.results: 

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

604 

605 self._set_old_keywords() 

606 

607 # Store the parameters used for this calculation 

608 self._store_param_state() 

609 

610 def _set_old_keywords(self): 

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

612 self.spinpol = self.get_spin_polarized() 

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

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

615 self.forces = self.get_forces() 

616 self.fermi = self.get_fermi_level() 

617 self.dipole = self.get_dipole_moment() 

618 # Prevent calculation from going into a loop 

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

620 self.nbands = self.get_number_of_bands() 

621 

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

623 @property 

624 def kpts(self): 

625 """Access the kpts from input_params dict""" 

626 return self.input_params['kpts'] 

627 

628 @kpts.setter 

629 def kpts(self, kpts): 

630 """Set kpts in input_params dict""" 

631 self.input_params['kpts'] = kpts 

632 

633 @property 

634 def encut(self): 

635 """Direct access to the encut parameter""" 

636 return self.float_params['encut'] 

637 

638 @encut.setter 

639 def encut(self, encut): 

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

641 self.set(encut=encut) 

642 

643 @property 

644 def xc(self): 

645 """Direct access to the xc parameter""" 

646 return self.get_xc_functional() 

647 

648 @xc.setter 

649 def xc(self, xc): 

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

651 self.set(xc=xc) 

652 

653 @property 

654 def atoms(self): 

655 return self._atoms 

656 

657 @atoms.setter 

658 def atoms(self, atoms): 

659 if atoms is None: 

660 self._atoms = None 

661 self.clear_results() 

662 else: 

663 if self.check_state(atoms): 

664 self.clear_results() 

665 self._atoms = atoms.copy() 

666 

667 def load_file(self, filename): 

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

669 

670 Example: 

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

672 >>> calc = Vasp() 

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

674 """ 

675 filename = self._indir(filename) 

676 with open(filename) as fd: 

677 return fd.readlines() 

678 

679 @contextmanager 

680 def load_file_iter(self, filename): 

681 """Return a file iterator""" 

682 

683 filename = self._indir(filename) 

684 with open(filename) as fd: 

685 yield fd 

686 

687 def read_outcar(self, lines=None): 

688 """Read results from the OUTCAR file. 

689 Deprecated, see read_results()""" 

690 if not lines: 

691 lines = self.load_file('OUTCAR') 

692 # Spin polarized calculation? 

693 self.spinpol = self.get_spin_polarized() 

694 

695 self.version = self.get_version() 

696 

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

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

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

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

701 

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

703 

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

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

706 

707 self.read_ldau() 

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

709 lines=lines) 

710 

711 def _read_xml(self) -> SinglePointDFTCalculator: 

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

713 Returns calculator from the xml file. 

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

715 """ 

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

717 incomplete_msg = ( 

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

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

720 try: 

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

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

723 assert isinstance(_xml_atoms, ase.Atoms) 

724 except ElementTree.ParseError as exc: 

725 raise calculator.ReadError(incomplete_msg) from exc 

726 

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

728 raise calculator.ReadError(incomplete_msg) 

729 

730 self._xml_calc = _xml_atoms.calc 

731 return self._xml_calc 

732 

733 @property 

734 def _xml_calc(self) -> SinglePointDFTCalculator: 

735 if self.__xml_calc is None: 

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

737 'Run read_results() first.') 

738 return self.__xml_calc 

739 

740 @_xml_calc.setter 

741 def _xml_calc(self, value): 

742 self.__xml_calc = value 

743 

744 def get_ibz_k_points(self): 

745 calc = self._xml_calc 

746 return calc.get_ibz_k_points() 

747 

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

749 calc = self._xml_calc 

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

751 

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

753 calc = self._xml_calc 

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

755 

756 def get_fermi_level(self): 

757 calc = self._xml_calc 

758 return calc.get_fermi_level() 

759 

760 def get_homo_lumo(self): 

761 calc = self._xml_calc 

762 return calc.get_homo_lumo() 

763 

764 def get_homo_lumo_by_spin(self, spin=0): 

765 calc = self._xml_calc 

766 return calc.get_homo_lumo_by_spin(spin=spin) 

767 

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

769 calc = self._xml_calc 

770 return calc.get_occupation_numbers(kpt, spin) 

771 

772 def get_spin_polarized(self): 

773 calc = self._xml_calc 

774 return calc.get_spin_polarized() 

775 

776 def get_number_of_spins(self): 

777 calc = self._xml_calc 

778 return calc.get_number_of_spins() 

779 

780 def get_number_of_bands(self): 

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

782 

783 def get_number_of_electrons(self, lines=None): 

784 if not lines: 

785 lines = self.load_file('OUTCAR') 

786 

787 nelect = None 

788 for line in lines: 

789 if 'total number of electrons' in line: 

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

791 break 

792 return nelect 

793 

794 def get_k_point_weights(self): 

795 filename = 'IBZKPT' 

796 return self.read_k_point_weights(filename) 

797 

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

799 """ 

800 The total DOS. 

801 

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

803 (energies, dos). 

804 """ 

805 from ase.dft.dos import DOS 

806 dos = DOS(self, **kwargs) 

807 e = dos.get_energies() 

808 d = dos.get_dos(spin=spin) 

809 return e, d 

810 

811 def get_version(self): 

812 if self.version is None: 

813 # Try if we can read the version number 

814 self.version = self.read_version() 

815 return self.version 

816 

817 def read_version(self): 

818 """Get the VASP version number""" 

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

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

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

822 return None 

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

824 for line in lines: 

825 if ' vasp.' in line: 

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

827 # We didn't find the version in VASP 

828 return None 

829 

830 def get_number_of_iterations(self): 

831 return self.read_number_of_iterations() 

832 

833 def read_number_of_iterations(self): 

834 niter = None 

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

836 for line in lines: 

837 # find the last iteration number 

838 if '- Iteration' in line: 

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

840 return niter 

841 

842 def read_number_of_ionic_steps(self): 

843 niter = None 

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

845 for line in lines: 

846 if '- Iteration' in line: 

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

848 return niter 

849 

850 def read_stress(self, lines=None): 

851 """Read stress from OUTCAR. 

852 

853 Depreciated: Use get_stress() instead. 

854 """ 

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

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

857 if not lines: 

858 lines = self.load_file('OUTCAR') 

859 

860 stress = None 

861 for line in lines: 

862 if ' in kB ' in line: 

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

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

865 return stress 

866 

867 def read_ldau(self, lines=None): 

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

869 if not lines: 

870 lines = self.load_file('OUTCAR') 

871 

872 ldau_luj = None 

873 ldauprint = None 

874 ldau = None 

875 ldautype = None 

876 atomtypes = [] 

877 # read ldau parameters from outcar 

878 for line in lines: 

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

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

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

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

883 ldau = True 

884 ldau_luj = {} 

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

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

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

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

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

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

891 # create dictionary 

892 if ldau: 

893 for i, symbol in enumerate(atomtypes): 

894 ldau_luj[symbol] = { 

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

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

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

898 } 

899 self.dict_params['ldau_luj'] = ldau_luj 

900 

901 self.ldau = ldau 

902 self.ldauprint = ldauprint 

903 self.ldautype = ldautype 

904 self.ldau_luj = ldau_luj 

905 return ldau, ldauprint, ldautype, ldau_luj 

906 

907 def get_xc_functional(self): 

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

909 

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

911 Otherwise, the XC functional associated with the 

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

913 The string is always cast to uppercase for consistency 

914 in checks.""" 

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

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

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

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

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

920 

921 # Methods for reading information from OUTCAR files: 

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

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

924 Depreciated: use get_potential_energy() instead""" 

925 if not lines: 

926 lines = self.load_file('OUTCAR') 

927 

928 [energy_free, energy_zero] = [0, 0] 

929 if all: 

930 energy_free = [] 

931 energy_zero = [] 

932 for line in lines: 

933 # Free energy 

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

935 if all: 

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

937 else: 

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

939 # Extrapolated zero point energy 

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

941 if all: 

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

943 else: 

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

945 return [energy_free, energy_zero] 

946 

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

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

949 

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

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

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

953 

954 if not lines: 

955 lines = self.load_file('OUTCAR') 

956 

957 if all: 

958 all_forces = [] 

959 

960 for n, line in enumerate(lines): 

961 if 'TOTAL-FORCE' in line: 

962 forces = [] 

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

964 forces.append( 

965 np.array( 

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

967 

968 if all: 

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

970 

971 if all: 

972 return np.array(all_forces) 

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

974 

975 def read_fermi(self, lines=None): 

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

977 if not lines: 

978 lines = self.load_file('OUTCAR') 

979 

980 E_f = None 

981 for line in lines: 

982 if 'E-fermi' in line: 

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

984 return E_f 

985 

986 def read_dipole(self, lines=None): 

987 """Read dipole from OUTCAR""" 

988 if not lines: 

989 lines = self.load_file('OUTCAR') 

990 

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

992 for line in lines: 

993 if 'dipolmoment' in line: 

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

995 return dipolemoment 

996 

997 def read_mag(self, lines=None): 

998 if not lines: 

999 lines = self.load_file('OUTCAR') 

1000 p = self.int_params 

1001 q = self.list_float_params 

1002 if self.spinpol: 

1003 magnetic_moment = self._read_magnetic_moment(lines=lines) 

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

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

1006 magnetic_moments = self._read_magnetic_moments(lines=lines) 

1007 else: 

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

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

1010 ' to get information on magnetic moments') 

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

1012 else: 

1013 magnetic_moment = 0.0 

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

1015 return magnetic_moment, magnetic_moments 

1016 

1017 def _read_magnetic_moments(self, lines=None): 

1018 """Read magnetic moments from OUTCAR. 

1019 Only reads the last occurrence. """ 

1020 if not lines: 

1021 lines = self.load_file('OUTCAR') 

1022 

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

1024 magstr = 'magnetization (x)' 

1025 

1026 # Search for the last occurrence 

1027 nidx = -1 

1028 for n, line in enumerate(lines): 

1029 if magstr in line: 

1030 nidx = n 

1031 

1032 # Read that occurrence 

1033 if nidx > -1: 

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

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

1036 return magnetic_moments[self.resort] 

1037 

1038 def _read_magnetic_moment(self, lines=None): 

1039 """Read magnetic moment from OUTCAR""" 

1040 if not lines: 

1041 lines = self.load_file('OUTCAR') 

1042 

1043 for line in lines: 

1044 if 'number of electron ' in line: 

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

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

1047 return magnetic_moment 

1048 

1049 def read_nbands(self, lines=None): 

1050 """Read number of bands from OUTCAR""" 

1051 if not lines: 

1052 lines = self.load_file('OUTCAR') 

1053 

1054 for line in lines: 

1055 line = self.strip_warnings(line) 

1056 if 'NBANDS' in line: 

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

1058 return None 

1059 

1060 def read_convergence(self, lines=None): 

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

1062 if not lines: 

1063 lines = self.load_file('OUTCAR') 

1064 

1065 converged = None 

1066 # First check electronic convergence 

1067 for line in lines: 

1068 # VASP 6 actually labels loop exit reason 

1069 if 'aborting loop' in line: 

1070 converged = 'because EDIFF is reached' in line 

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

1072 # and 

1073 # 'because hard stop was set' 

1074 # will result in unconverged 

1075 break 

1076 # determine convergence by attempting to reproduce VASP's 

1077 # internal logic 

1078 if 'EDIFF ' in line: 

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

1080 if 'total energy-change' in line: 

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

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

1083 if 'MIXING' in line: 

1084 continue 

1085 split = line.split(':') 

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

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

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

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

1090 # we are checking still the first number so 

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

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

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

1094 bsplit = b.split('-') 

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

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

1097 b = float(b) 

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

1099 converged = True 

1100 else: 

1101 converged = False 

1102 continue 

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

1104 # condition been fulfilled 

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

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

1107 if not self.read_relaxed(): 

1108 converged = False 

1109 else: 

1110 converged = True 

1111 return converged 

1112 

1113 def read_k_point_weights(self, filename): 

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

1115 

1116 lines = self.load_file(filename) 

1117 

1118 if 'Tetrahedra\n' in lines: 

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

1120 else: 

1121 N = len(lines) 

1122 kpt_weights = [] 

1123 for n in range(3, N): 

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

1125 kpt_weights = np.array(kpt_weights) 

1126 kpt_weights /= np.sum(kpt_weights) 

1127 

1128 return kpt_weights 

1129 

1130 def read_relaxed(self, lines=None): 

1131 """Check if ionic relaxation completed""" 

1132 if not lines: 

1133 lines = self.load_file('OUTCAR') 

1134 for line in lines: 

1135 if 'reached required accuracy' in line: 

1136 return True 

1137 return False 

1138 

1139 def read_spinpol(self, lines=None): 

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

1141 

1142 Depreciated: Use get_spin_polarized() instead. 

1143 """ 

1144 if not lines: 

1145 lines = self.load_file('OUTCAR') 

1146 

1147 for line in lines: 

1148 if 'ISPIN' in line: 

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

1150 self.spinpol = True 

1151 else: 

1152 self.spinpol = False 

1153 return self.spinpol 

1154 

1155 def strip_warnings(self, line): 

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

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

1158 return "" 

1159 return line 

1160 

1161 @property 

1162 def txt(self): 

1163 return self._txt 

1164 

1165 @txt.setter 

1166 def txt(self, txt): 

1167 if isinstance(txt, PurePath): 

1168 txt = str(txt) 

1169 self._txt = txt 

1170 

1171 def get_number_of_grid_points(self): 

1172 raise NotImplementedError 

1173 

1174 def get_pseudo_density(self): 

1175 raise NotImplementedError 

1176 

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

1178 raise NotImplementedError 

1179 

1180 def get_bz_k_points(self): 

1181 raise NotImplementedError 

1182 

1183 def read_vib_freq(self, lines=None) -> tuple[list[float], list[float]]: 

1184 """Read vibrational frequencies. 

1185 

1186 Returns 

1187 ------- 

1188 List of real and list of imaginary frequencies 

1189 (imaginary number as real number). 

1190 """ 

1191 freq = [] 

1192 i_freq = [] 

1193 

1194 if not lines: 

1195 lines = self.load_file('OUTCAR') 

1196 

1197 for line in lines: 

1198 data = line.split() 

1199 if 'THz' in data: 

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

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

1202 else: 

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

1204 return freq, i_freq 

1205 

1206 def _read_massweighted_hessian_xml(self): 

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

1208 

1209 Returns 

1210 ------- 

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

1212 

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

1214 

1215 Converts to ASE units for VASP version 6. 

1216 """ 

1217 

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

1219 try: 

1220 tree = ElementTree.iterparse(file) 

1221 hessian = None 

1222 for event, elem in tree: 

1223 if elem.tag == 'dynmat': 

1224 for i, entry in enumerate( 

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

1226 text_split = entry.text.split() 

1227 if not text_split: 

1228 raise ElementTree.ParseError( 

1229 "Could not find varray hessian!") 

1230 if i == 0: 

1231 n_items = len(text_split) 

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

1233 assert isinstance(hessian, np.ndarray) 

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

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

1236 if i != n_items - 1: 

1237 raise ElementTree.ParseError( 

1238 "Hessian is not quadratic!") 

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

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

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

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

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

1244 # VASP6 uses factor 2pi 

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

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

1247 break 

1248 else: # Catch changes in VASP 

1249 vasp_version_error_msg = ( 

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

1251 'non-supported VASP version. ' 

1252 'Not sure what unit the Hessian ' 

1253 'is in, aborting.') 

1254 raise calculator.ReadError(vasp_version_error_msg) 

1255 

1256 else: 

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

1258 assert isinstance(hessian, np.ndarray) 

1259 hessian *= conv 

1260 if hessian is None: 

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

1262 

1263 except ElementTree.ParseError as exc: 

1264 incomplete_msg = ( 

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

1266 'and no DFT data was available. ' 

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

1268 raise calculator.ReadError(incomplete_msg) from exc 

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

1270 return -hessian 

1271 

1272 def get_vibrations(self) -> VibrationsData: 

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

1274 

1275 Returns 

1276 ------- 

1277 VibrationsData object. 

1278 

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

1280 

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

1282 in the POTCAR can therefore result in different results. 

1283 

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

1285 the VASP-Wiki. 

1286 """ 

1287 

1288 mass_weighted_hessian = self._read_massweighted_hessian_xml() 

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

1290 indices = VibrationsData.indices_from_constraints(self.atoms) 

1291 # save the corresponding sorted atom numbers 

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

1293 # mass weights = 1/sqrt(mass) 

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

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

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

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

1298 # opposite in Vibrations.read 

1299 hessian = mass_weighted_hessian / \ 

1300 mass_weights / mass_weights[:, np.newaxis] 

1301 

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

1303 

1304 def get_nonselfconsistent_energies(self, bee_type): 

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

1306 written in OUTCAR file. 

1307 """ 

1308 assert bee_type == 'beefvdw' 

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

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

1311 s = p.readlines() 

1312 p.close() 

1313 xc = np.array([]) 

1314 for line in s: 

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

1316 xc = np.append(xc, l_) 

1317 assert len(xc) == 32 

1318 return xc 

1319 

1320 

1321##################################### 

1322# Below defines helper functions 

1323# for the VASP calculator 

1324##################################### 

1325 

1326 

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

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

1329 it can be run by VASP. 

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

1331 """ 

1332 

1333 # Loop through all check functions 

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

1335 check(atoms) 

1336 

1337 

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

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

1340 Raises CalculatorSetupError if the cell is wrong. 

1341 """ 

1342 if atoms.cell.rank < 3: 

1343 raise calculator.CalculatorSetupError( 

1344 "The lattice vectors are zero! " 

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

1346 "unit cell.") 

1347 

1348 

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

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

1351 cannot handle non-PBC. 

1352 Raises CalculatorSetupError. 

1353 """ 

1354 if not atoms.pbc.all(): 

1355 raise calculator.CalculatorSetupError( 

1356 "Vasp cannot handle non-periodic boundaries. " 

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

1358 

1359 

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

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

1362 Raises CalculatorSetupError. 

1363 """ 

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

1365 raise calculator.CalculatorSetupError( 

1366 'Expected an Atoms object, ' 

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