Coverage for /builds/ase/ase/ase/io/vasp_parsers/vasp_outcar_parsers.py: 95.24%

462 statements  

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

1# fmt: off 

2 

3""" 

4Module for parsing OUTCAR files. 

5""" 

6import re 

7from abc import ABC, abstractmethod 

8from pathlib import Path, PurePath 

9from typing import Any, Dict, Iterator, List, Optional, Sequence, TextIO, Union 

10from warnings import warn 

11 

12import numpy as np 

13 

14import ase 

15from ase import Atoms 

16from ase.calculators.singlepoint import ( 

17 SinglePointDFTCalculator, 

18 SinglePointKPoint, 

19) 

20from ase.data import atomic_numbers 

21from ase.io import ParseError, read 

22from ase.io.utils import ImageChunk 

23 

24# Denotes end of Ionic step for OUTCAR reading 

25_OUTCAR_SCF_DELIM = 'FREE ENERGIE OF THE ION-ELECTRON SYSTEM' 

26 

27# Some type aliases 

28_HEADER = Dict[str, Any] 

29_CURSOR = int 

30_CHUNK = Sequence[str] 

31_RESULT = Dict[str, Any] 

32 

33 

34class NoNonEmptyLines(Exception): 

35 """No more non-empty lines were left in the provided chunck""" 

36 

37 

38class UnableToLocateDelimiter(Exception): 

39 """Did not find the provided delimiter""" 

40 

41 def __init__(self, delimiter, msg): 

42 self.delimiter = delimiter 

43 super().__init__(msg) 

44 

45 

46def _check_line(line: str) -> str: 

47 """Auxiliary check line function for OUTCAR numeric formatting. 

48 See issue #179, https://gitlab.com/ase/ase/issues/179 

49 Only call in cases we need the numeric values 

50 """ 

51 if re.search('[0-9]-[0-9]', line): 

52 line = re.sub('([0-9])-([0-9])', r'\1 -\2', line) 

53 return line 

54 

55 

56def find_next_non_empty_line(cursor: _CURSOR, lines: _CHUNK) -> _CURSOR: 

57 """Fast-forward the cursor from the current position to the next 

58 line which is non-empty. 

59 Returns the new cursor position on the next non-empty line. 

60 """ 

61 for line in lines[cursor:]: 

62 if line.strip(): 

63 # Line was non-empty 

64 return cursor 

65 # Empty line, increment the cursor position 

66 cursor += 1 

67 # There was no non-empty line 

68 raise NoNonEmptyLines("Did not find a next line which was not empty") 

69 

70 

71def search_lines(delim: str, cursor: _CURSOR, lines: _CHUNK) -> _CURSOR: 

72 """Search through a chunk of lines starting at the cursor position for 

73 a given delimiter. The new position of the cursor is returned.""" 

74 for line in lines[cursor:]: 

75 if delim in line: 

76 # The cursor should be on the line with the delimiter now 

77 assert delim in lines[cursor] 

78 return cursor 

79 # We didn't find the delimiter 

80 cursor += 1 

81 raise UnableToLocateDelimiter( 

82 delim, f'Did not find starting point for delimiter {delim}') 

83 

84 

85def convert_vasp_outcar_stress(stress: Sequence): 

86 """Helper function to convert the stress line in an OUTCAR to the 

87 expected units in ASE """ 

88 stress_arr = -np.array(stress) 

89 shape = stress_arr.shape 

90 if shape != (6, ): 

91 raise ValueError( 

92 f'Stress has the wrong shape. Expected (6,), got {shape}') 

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

94 return stress_arr 

95 

96 

97def read_constraints_from_file(directory): 

98 directory = Path(directory) 

99 constraint = None 

100 for filename in ('CONTCAR', 'POSCAR'): 

101 if (directory / filename).is_file(): 

102 constraint = read(directory / filename, 

103 format='vasp', 

104 parallel=False).constraints 

105 break 

106 return constraint 

107 

108 

109class VaspPropertyParser(ABC): 

110 NAME = None # type: str 

111 

112 @classmethod 

113 def get_name(cls): 

114 """Name of parser. Override the NAME constant in the class to 

115 specify a custom name, 

116 otherwise the class name is used""" 

117 return cls.NAME or cls.__name__ 

118 

119 @abstractmethod 

120 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool: 

121 """Function which checks if a property can be derived from a given 

122 cursor position""" 

123 

124 @staticmethod 

125 def get_line(cursor: _CURSOR, lines: _CHUNK) -> str: 

126 """Helper function to get a line, and apply the check_line function""" 

127 return _check_line(lines[cursor]) 

128 

129 @abstractmethod 

130 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT: 

131 """Extract a property from the cursor position. 

132 Assumes that "has_property" would evaluate to True 

133 from cursor position """ 

134 

135 

136class SimpleProperty(VaspPropertyParser, ABC): 

137 LINE_DELIMITER = None # type: str 

138 

139 def __init__(self): 

140 super().__init__() 

141 if self.LINE_DELIMITER is None: 

142 raise ValueError('Must specify a line delimiter.') 

143 

144 def has_property(self, cursor, lines) -> bool: 

145 line = lines[cursor] 

146 return self.LINE_DELIMITER in line 

147 

148 

149class VaspChunkPropertyParser(VaspPropertyParser, ABC): 

150 """Base class for parsing a chunk of the OUTCAR. 

151 The base assumption is that only a chunk of lines is passed""" 

152 

153 def __init__(self, header: _HEADER = None): 

154 super().__init__() 

155 header = header or {} 

156 self.header = header 

157 

158 def get_from_header(self, key: str) -> Any: 

159 """Get a key from the header, and raise a ParseError 

160 if that key doesn't exist""" 

161 try: 

162 return self.header[key] 

163 except KeyError: 

164 raise ParseError( 

165 'Parser requested unavailable key "{}" from header'.format( 

166 key)) 

167 

168 

169class VaspHeaderPropertyParser(VaspPropertyParser, ABC): 

170 """Base class for parsing the header of an OUTCAR""" 

171 

172 

173class SimpleVaspChunkParser(VaspChunkPropertyParser, SimpleProperty, ABC): 

174 """Class for properties in a chunk can be 

175 determined to exist from 1 line""" 

176 

177 

178class SimpleVaspHeaderParser(VaspHeaderPropertyParser, SimpleProperty, ABC): 

179 """Class for properties in the header 

180 which can be determined to exist from 1 line""" 

181 

182 

183class Spinpol(SimpleVaspHeaderParser): 

184 """Parse if the calculation is spin-polarized. 

185 

186 Example line: 

187 " ISPIN = 2 spin polarized calculation?" 

188 

189 """ 

190 LINE_DELIMITER = 'ISPIN' 

191 

192 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT: 

193 line = lines[cursor].strip() 

194 parts = line.split() 

195 ispin = int(parts[2]) 

196 # ISPIN 2 = spinpolarized, otherwise no 

197 # ISPIN 1 = non-spinpolarized 

198 spinpol = ispin == 2 

199 return {'spinpol': spinpol} 

200 

201 

202class SpeciesTypes(SimpleVaspHeaderParser): 

203 """Parse species types. 

204 

205 Example line: 

206 " POTCAR: PAW_PBE Ni 02Aug2007" 

207 

208 We must parse this multiple times, as it's scattered in the header. 

209 So this class has to simply parse the entire header. 

210 """ 

211 LINE_DELIMITER = 'POTCAR:' 

212 

213 def __init__(self, *args, **kwargs): 

214 self._species = [] # Store species as we find them 

215 # We count the number of times we found the line, 

216 # as we only want to parse every second, 

217 # due to repeated entries in the OUTCAR 

218 super().__init__(*args, **kwargs) 

219 

220 @property 

221 def species(self) -> List[str]: 

222 """Internal storage of each found line. 

223 Will contain the double counting. 

224 Use the get_species() method to get the un-doubled list.""" 

225 return self._species 

226 

227 def get_species(self) -> List[str]: 

228 """The OUTCAR will contain two 'POTCAR:' entries per species. 

229 This method only returns the first half, 

230 effectively removing the double counting. 

231 """ 

232 # Get the index of the first half 

233 # In case we have an odd number, we round up (for testing purposes) 

234 # Tests like to just add species 1-by-1 

235 # Having an odd number should never happen in a real OUTCAR 

236 # For even length lists, this is just equivalent to idx = 

237 # len(self.species) // 2 

238 idx = sum(divmod(len(self.species), 2)) 

239 # Make a copy 

240 return list(self.species[:idx]) 

241 

242 def _make_returnval(self) -> _RESULT: 

243 """Construct the return value for the "parse" method""" 

244 return {'species': self.get_species()} 

245 

246 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT: 

247 line = lines[cursor].strip() 

248 

249 parts = line.split() 

250 # Determine in what position we'd expect to find the symbol 

251 if '1/r potential' in line: 

252 # This denotes an AE potential 

253 # Currently only H_AE 

254 # " H 1/r potential " 

255 idx = 1 

256 else: 

257 # Regular PAW potential, e.g. 

258 # "PAW_PBE H1.25 07Sep2000" or 

259 # "PAW_PBE Fe_pv 02Aug2007" 

260 idx = 2 

261 

262 sym = parts[idx] 

263 # remove "_h", "_GW", "_3" tags etc. 

264 sym = sym.split('_')[0] 

265 # in the case of the "H1.25" potentials etc., 

266 # remove any non-alphabetic characters 

267 sym = ''.join([s for s in sym if s.isalpha()]) 

268 

269 if sym not in atomic_numbers: 

270 # Check that we have properly parsed the symbol, and we found 

271 # an element 

272 raise ParseError( 

273 f'Found an unexpected symbol {sym} in line {line}') 

274 

275 self.species.append(sym) 

276 

277 return self._make_returnval() 

278 

279 

280class IonsPerSpecies(SimpleVaspHeaderParser): 

281 """Example line: 

282 

283 " ions per type = 32 31 2" 

284 """ 

285 LINE_DELIMITER = 'ions per type' 

286 

287 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT: 

288 line = lines[cursor].strip() 

289 parts = line.split() 

290 ion_types = list(map(int, parts[4:])) 

291 return {'ion_types': ion_types} 

292 

293 

294class KpointHeader(VaspHeaderPropertyParser): 

295 """Reads nkpts and nbands from the line delimiter. 

296 Then it also searches for the ibzkpts and kpt_weights""" 

297 

298 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool: 

299 line = lines[cursor] 

300 return "NKPTS" in line and "NBANDS" in line 

301 

302 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT: 

303 line = lines[cursor].strip() 

304 parts = line.split() 

305 nkpts = int(parts[3]) 

306 nbands = int(parts[-1]) 

307 

308 results: Dict[str, Any] = {'nkpts': nkpts, 'nbands': nbands} 

309 # We also now get the k-point weights etc., 

310 # because we need to know how many k-points we have 

311 # for parsing that 

312 # Move cursor down to next delimiter 

313 delim2 = 'k-points in reciprocal lattice and weights' 

314 for offset, line in enumerate(lines[cursor:], start=0): 

315 line = line.strip() 

316 if delim2 in line: 

317 # build k-points 

318 ibzkpts = np.zeros((nkpts, 3)) 

319 kpt_weights = np.zeros(nkpts) 

320 for nk in range(nkpts): 

321 # Offset by 1, as k-points starts on the next line 

322 line = lines[cursor + offset + nk + 1].strip() 

323 parts = line.split() 

324 ibzkpts[nk] = list(map(float, parts[:3])) 

325 kpt_weights[nk] = float(parts[-1]) 

326 results['ibzkpts'] = ibzkpts 

327 results['kpt_weights'] = kpt_weights 

328 break 

329 else: 

330 raise ParseError('Did not find the K-points in the OUTCAR') 

331 

332 return results 

333 

334 

335class Stress(SimpleVaspChunkParser): 

336 """Process the stress from an OUTCAR""" 

337 LINE_DELIMITER = 'in kB ' 

338 

339 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT: 

340 line = self.get_line(cursor, lines) 

341 result = None # type: Optional[Sequence[float]] 

342 try: 

343 stress = [float(a) for a in line.split()[2:]] 

344 except ValueError: 

345 # Vasp FORTRAN string formatting issues, can happen with 

346 # some bad geometry steps Alternatively, we can re-raise 

347 # as a ParseError? 

348 warn('Found badly formatted stress line. Setting stress to None.') 

349 else: 

350 result = convert_vasp_outcar_stress(stress) 

351 return {'stress': result} 

352 

353 

354class Cell(SimpleVaspChunkParser): 

355 LINE_DELIMITER = 'direct lattice vectors' 

356 

357 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT: 

358 nskip = 1 

359 cell = np.zeros((3, 3)) 

360 for i in range(3): 

361 line = self.get_line(cursor + i + nskip, lines) 

362 parts = line.split() 

363 cell[i, :] = list(map(float, parts[0:3])) 

364 return {'cell': cell} 

365 

366 

367class PositionsAndForces(SimpleVaspChunkParser): 

368 """Positions and forces are written in the same block. 

369 We parse both simultaneously""" 

370 LINE_DELIMITER = 'POSITION ' 

371 

372 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT: 

373 nskip = 2 

374 natoms = self.get_from_header('natoms') 

375 positions = np.zeros((natoms, 3)) 

376 forces = np.zeros((natoms, 3)) 

377 

378 for i in range(natoms): 

379 line = self.get_line(cursor + i + nskip, lines) 

380 parts = list(map(float, line.split())) 

381 positions[i] = parts[0:3] 

382 forces[i] = parts[3:6] 

383 return {'positions': positions, 'forces': forces} 

384 

385 

386class Magmom(VaspChunkPropertyParser): 

387 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool: 

388 """ We need to check for two separate delimiter strings, 

389 to ensure we are at the right place """ 

390 line = lines[cursor] 

391 if 'number of electron' in line: 

392 parts = line.split() 

393 if len(parts) > 5 and parts[0].strip() != "NELECT": 

394 return True 

395 return False 

396 

397 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT: 

398 line = self.get_line(cursor, lines) 

399 parts = line.split() 

400 idx = parts.index('magnetization') + 1 

401 magmom_lst = parts[idx:] 

402 if len(magmom_lst) != 1: 

403 magmom: Union[np.ndarray, float] = np.array( 

404 list(map(float, magmom_lst)) 

405 ) 

406 else: 

407 magmom = float(magmom_lst[0]) 

408 return {'magmom': magmom} 

409 

410 

411class Magmoms(VaspChunkPropertyParser): 

412 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool: 

413 line = lines[cursor] 

414 if 'magnetization (x)' in line: 

415 natoms = self.get_from_header('natoms') 

416 self.non_collinear = False 

417 if cursor + natoms + 9 < len(lines): 

418 line_y = self.get_line(cursor + natoms + 9, lines) 

419 if 'magnetization (y)' in line_y: 

420 self.non_collinear = True 

421 return True 

422 return False 

423 

424 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT: 

425 

426 natoms = self.get_from_header('natoms') 

427 if self.non_collinear: 

428 magmoms = np.zeros((natoms, 3)) 

429 nskip = 4 # Skip some lines 

430 for i in range(natoms): 

431 line = self.get_line(cursor + i + nskip, lines) 

432 magmoms[i, 0] = float(line.split()[-1]) 

433 nskip = natoms + 13 # Skip some lines 

434 for i in range(natoms): 

435 line = self.get_line(cursor + i + nskip, lines) 

436 magmoms[i, 1] = float(line.split()[-1]) 

437 nskip = 2 * natoms + 22 # Skip some lines 

438 for i in range(natoms): 

439 line = self.get_line(cursor + i + nskip, lines) 

440 magmoms[i, 2] = float(line.split()[-1]) 

441 else: 

442 magmoms = np.zeros(natoms) 

443 nskip = 4 # Skip some lines 

444 for i in range(natoms): 

445 line = self.get_line(cursor + i + nskip, lines) 

446 magmoms[i] = float(line.split()[-1]) 

447 

448 return {'magmoms': magmoms} 

449 

450 

451class EFermi(SimpleVaspChunkParser): 

452 LINE_DELIMITER = 'E-fermi :' 

453 

454 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT: 

455 line = self.get_line(cursor, lines) 

456 parts = line.split() 

457 efermi = float(parts[2]) 

458 return {'efermi': efermi} 

459 

460 

461class Energy(SimpleVaspChunkParser): 

462 LINE_DELIMITER = _OUTCAR_SCF_DELIM 

463 

464 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT: 

465 nskip = 2 

466 line = self.get_line(cursor + nskip, lines) 

467 parts = line.strip().split() 

468 energy_free = float(parts[4]) # Force consistent 

469 

470 nskip = 4 

471 line = self.get_line(cursor + nskip, lines) 

472 parts = line.strip().split() 

473 energy_zero = float(parts[6]) # Extrapolated to 0 K 

474 

475 return {'free_energy': energy_free, 'energy': energy_zero} 

476 

477 

478class Kpoints(VaspChunkPropertyParser): 

479 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool: 

480 line = lines[cursor] 

481 # Example line: 

482 # " spin component 1" or " spin component 2" 

483 # We only check spin up, as if we are spin-polarized, we'll parse that 

484 # as well 

485 if 'spin component 1' in line: 

486 parts = line.strip().split() 

487 # This string is repeated elsewhere, but not with this exact shape 

488 if len(parts) == 3: 

489 try: 

490 # The last part of te line should be an integer, denoting 

491 # spin-up or spin-down 

492 int(parts[-1]) 

493 except ValueError: 

494 pass 

495 else: 

496 return True 

497 return False 

498 

499 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT: 

500 nkpts = self.get_from_header('nkpts') 

501 nbands = self.get_from_header('nbands') 

502 weights = self.get_from_header('kpt_weights') 

503 spinpol = self.get_from_header('spinpol') 

504 nspins = 2 if spinpol else 1 

505 

506 kpts = [] 

507 for spin in range(nspins): 

508 # for Vasp 6, they added some extra information after the 

509 # spin components. so we might need to seek the spin 

510 # component line 

511 cursor = search_lines(f'spin component {spin + 1}', cursor, lines) 

512 

513 cursor += 2 # Skip two lines 

514 for _ in range(nkpts): 

515 # Skip empty lines 

516 cursor = find_next_non_empty_line(cursor, lines) 

517 

518 line = self.get_line(cursor, lines) 

519 # Example line: 

520 # "k-point 1 : 0.0000 0.0000 0.0000" 

521 parts = line.strip().split() 

522 ikpt = int(parts[1]) - 1 # Make kpt idx start from 0 

523 weight = weights[ikpt] 

524 

525 cursor += 2 # Move down two 

526 eigenvalues = np.zeros(nbands) 

527 occupations = np.zeros(nbands) 

528 for n in range(nbands): 

529 # Example line: 

530 # " 1 -9.9948 1.00000" 

531 parts = lines[cursor].strip().split() 

532 eps_n, f_n = map(float, parts[1:]) 

533 occupations[n] = f_n 

534 eigenvalues[n] = eps_n 

535 cursor += 1 

536 kpt = SinglePointKPoint(weight, 

537 spin, 

538 ikpt, 

539 eps_n=eigenvalues, 

540 f_n=occupations) 

541 kpts.append(kpt) 

542 

543 return {'kpts': kpts} 

544 

545 

546class DefaultParsersContainer: 

547 """Container for the default OUTCAR parsers. 

548 Allows for modification of the global default parsers. 

549 

550 Takes in an arbitrary number of parsers. 

551 The parsers should be uninitialized, 

552 as they are created on request. 

553 """ 

554 

555 def __init__(self, *parsers_cls): 

556 self._parsers_dct = {} 

557 for parser in parsers_cls: 

558 self.add_parser(parser) 

559 

560 @property 

561 def parsers_dct(self) -> dict: 

562 return self._parsers_dct 

563 

564 def make_parsers(self): 

565 """Return a copy of the internally stored parsers. 

566 Parsers are created upon request.""" 

567 return [parser() for parser in self.parsers_dct.values()] 

568 

569 def remove_parser(self, name: str): 

570 """Remove a parser based on the name. 

571 The name must match the parser name exactly.""" 

572 self.parsers_dct.pop(name) 

573 

574 def add_parser(self, parser) -> None: 

575 """Add a parser""" 

576 self.parsers_dct[parser.get_name()] = parser 

577 

578 

579class TypeParser(ABC): 

580 """Base class for parsing a type, e.g. header or chunk, 

581 by applying the internal attached parsers""" 

582 

583 def __init__(self, parsers): 

584 self.parsers = parsers 

585 

586 @property 

587 def parsers(self): 

588 return self._parsers 

589 

590 @parsers.setter 

591 def parsers(self, new_parsers) -> None: 

592 self._check_parsers(new_parsers) 

593 self._parsers = new_parsers 

594 

595 @abstractmethod 

596 def _check_parsers(self, parsers) -> None: 

597 """Check the parsers are of correct type""" 

598 

599 def parse(self, lines) -> _RESULT: 

600 """Execute the attached paresers, and return the parsed properties""" 

601 properties = {} 

602 for cursor, _ in enumerate(lines): 

603 for parser in self.parsers: 

604 # Check if any of the parsers can extract a property 

605 # from this line Note: This will override any existing 

606 # properties we found, if we found it previously. This 

607 # is usually correct, as some VASP settings can cause 

608 # certain pieces of information to be written multiple 

609 # times during SCF. We are only interested in the 

610 # final values within a given chunk. 

611 if parser.has_property(cursor, lines): 

612 prop = parser.parse(cursor, lines) 

613 properties.update(prop) 

614 return properties 

615 

616 

617class ChunkParser(TypeParser, ABC): 

618 def __init__(self, parsers, header=None): 

619 super().__init__(parsers) 

620 self.header = header 

621 

622 @property 

623 def header(self) -> _HEADER: 

624 return self._header 

625 

626 @header.setter 

627 def header(self, value: Optional[_HEADER]) -> None: 

628 self._header = value or {} 

629 self.update_parser_headers() 

630 

631 def update_parser_headers(self) -> None: 

632 """Apply the header to all available parsers""" 

633 for parser in self.parsers: 

634 parser.header = self.header 

635 

636 def _check_parsers(self, 

637 parsers: Sequence[VaspChunkPropertyParser]) -> None: 

638 """Check the parsers are of correct type 'VaspChunkPropertyParser'""" 

639 if not all( 

640 isinstance(parser, VaspChunkPropertyParser) 

641 for parser in parsers): 

642 raise TypeError( 

643 'All parsers must be of type VaspChunkPropertyParser') 

644 

645 @abstractmethod 

646 def build(self, lines: _CHUNK) -> Atoms: 

647 """Construct an atoms object of the chunk from the parsed results""" 

648 

649 

650class HeaderParser(TypeParser, ABC): 

651 def _check_parsers(self, 

652 parsers: Sequence[VaspHeaderPropertyParser]) -> None: 

653 """Check the parsers are of correct type 'VaspHeaderPropertyParser'""" 

654 if not all( 

655 isinstance(parser, VaspHeaderPropertyParser) 

656 for parser in parsers): 

657 raise TypeError( 

658 'All parsers must be of type VaspHeaderPropertyParser') 

659 

660 @abstractmethod 

661 def build(self, lines: _CHUNK) -> _HEADER: 

662 """Construct the header object from the parsed results""" 

663 

664 

665class OutcarChunkParser(ChunkParser): 

666 """Class for parsing a chunk of an OUTCAR.""" 

667 

668 def __init__(self, 

669 header: _HEADER = None, 

670 parsers: Sequence[VaspChunkPropertyParser] = None): 

671 global default_chunk_parsers 

672 parsers = parsers or default_chunk_parsers.make_parsers() 

673 super().__init__(parsers, header=header) 

674 

675 def build(self, lines: _CHUNK) -> Atoms: 

676 """Apply outcar chunk parsers, and build an atoms object""" 

677 self.update_parser_headers() # Ensure header is in sync 

678 

679 results = self.parse(lines) 

680 symbols = self.header['symbols'] 

681 constraint = self.header.get('constraint', None) 

682 

683 atoms_kwargs = dict(symbols=symbols, constraint=constraint, pbc=True) 

684 

685 # Find some required properties in the parsed results. 

686 # Raise ParseError if they are not present 

687 for prop in ('positions', 'cell'): 

688 try: 

689 atoms_kwargs[prop] = results.pop(prop) 

690 except KeyError: 

691 raise ParseError( 

692 'Did not find required property {} during parse.'.format( 

693 prop)) 

694 atoms = Atoms(**atoms_kwargs) 

695 

696 kpts = results.pop('kpts', None) 

697 calc = SinglePointDFTCalculator(atoms, **results) 

698 if kpts is not None: 

699 calc.kpts = kpts 

700 calc.name = 'vasp' 

701 atoms.calc = calc 

702 return atoms 

703 

704 

705class OutcarHeaderParser(HeaderParser): 

706 """Class for parsing a chunk of an OUTCAR.""" 

707 

708 def __init__(self, 

709 parsers: Sequence[VaspHeaderPropertyParser] = None, 

710 workdir: Union[str, PurePath] = None): 

711 global default_header_parsers 

712 parsers = parsers or default_header_parsers.make_parsers() 

713 super().__init__(parsers) 

714 self.workdir = workdir 

715 

716 @property 

717 def workdir(self): 

718 return self._workdir 

719 

720 @workdir.setter 

721 def workdir(self, value): 

722 if value is not None: 

723 value = Path(value) 

724 self._workdir = value 

725 

726 def _build_symbols(self, results: _RESULT) -> Sequence[str]: 

727 if 'symbols' in results: 

728 # Safeguard, in case a different parser already 

729 # did this. Not currently available in a default parser 

730 return results.pop('symbols') 

731 

732 # Build the symbols of the atoms 

733 for required_key in ('ion_types', 'species'): 

734 if required_key not in results: 

735 raise ParseError( 

736 'Did not find required key "{}" in parsed header results.'. 

737 format(required_key)) 

738 

739 ion_types = results.pop('ion_types') 

740 species = results.pop('species') 

741 if len(ion_types) != len(species): 

742 raise ParseError( 

743 ('Expected length of ion_types to be same as species, ' 

744 'but got ion_types={} and species={}').format( 

745 len(ion_types), len(species))) 

746 

747 # Expand the symbols list 

748 symbols = [] 

749 for n, sym in zip(ion_types, species): 

750 symbols.extend(n * [sym]) 

751 return symbols 

752 

753 def _get_constraint(self): 

754 """Try and get the constraints from the POSCAR of CONTCAR 

755 since they aren't located in the OUTCAR, and thus we cannot construct an 

756 OUTCAR parser which does this. 

757 """ 

758 constraint = None 

759 if self.workdir is not None: 

760 constraint = read_constraints_from_file(self.workdir) 

761 return constraint 

762 

763 def build(self, lines: _CHUNK) -> _RESULT: 

764 """Apply the header parsers, and build the header""" 

765 results = self.parse(lines) 

766 

767 # Get the symbols from the parsed results 

768 # will pop the keys which we use for that purpose 

769 symbols = self._build_symbols(results) 

770 natoms = len(symbols) 

771 

772 constraint = self._get_constraint() 

773 

774 # Remaining results from the parse goes into the header 

775 header = dict(symbols=symbols, 

776 natoms=natoms, 

777 constraint=constraint, 

778 **results) 

779 return header 

780 

781 

782class OUTCARChunk(ImageChunk): 

783 """Container class for a chunk of the OUTCAR which consists of a 

784 self-contained SCF step, i.e. and image. Also contains the header_data 

785 """ 

786 

787 def __init__(self, 

788 lines: _CHUNK, 

789 header: _HEADER, 

790 parser: ChunkParser = None): 

791 super().__init__() 

792 self.lines = lines 

793 self.header = header 

794 self.parser = parser or OutcarChunkParser() 

795 

796 def build(self): 

797 self.parser.header = self.header # Ensure header is syncronized 

798 return self.parser.build(self.lines) 

799 

800 

801def build_header(fd: TextIO) -> _CHUNK: 

802 """Build a chunk containing the header data""" 

803 lines = [] 

804 for line in fd: 

805 lines.append(line) 

806 if 'Iteration' in line: 

807 # Start of SCF cycle 

808 return lines 

809 

810 # We never found the SCF delimiter, so the OUTCAR must be incomplete 

811 raise ParseError('Incomplete OUTCAR') 

812 

813 

814def build_chunk(fd: TextIO) -> _CHUNK: 

815 """Build chunk which contains 1 complete atoms object""" 

816 lines = [] 

817 while True: 

818 line = next(fd) 

819 lines.append(line) 

820 if _OUTCAR_SCF_DELIM in line: 

821 # Add 4 more lines to include energy 

822 for _ in range(4): 

823 lines.append(next(fd)) 

824 break 

825 return lines 

826 

827 

828def outcarchunks(fd: TextIO, 

829 chunk_parser: ChunkParser = None, 

830 header_parser: HeaderParser = None) -> Iterator[OUTCARChunk]: 

831 """Function to build chunks of OUTCAR from a file stream""" 

832 name = Path(fd.name) 

833 workdir = name.parent 

834 

835 # First we get header info 

836 # pass in the workdir from the fd, so we can try and get the constraints 

837 header_parser = header_parser or OutcarHeaderParser(workdir=workdir) 

838 

839 lines = build_header(fd) 

840 header = header_parser.build(lines) 

841 assert isinstance(header, dict) 

842 

843 chunk_parser = chunk_parser or OutcarChunkParser() 

844 

845 while True: 

846 try: 

847 lines = build_chunk(fd) 

848 except StopIteration: 

849 # End of file 

850 return 

851 yield OUTCARChunk(lines, header, parser=chunk_parser) 

852 

853 

854# Create the default chunk parsers 

855default_chunk_parsers = DefaultParsersContainer( 

856 Cell, 

857 PositionsAndForces, 

858 Stress, 

859 Magmoms, 

860 Magmom, 

861 EFermi, 

862 Kpoints, 

863 Energy, 

864) 

865 

866# Create the default header parsers 

867default_header_parsers = DefaultParsersContainer( 

868 SpeciesTypes, 

869 IonsPerSpecies, 

870 Spinpol, 

871 KpointHeader, 

872)