Coverage for ase / io / vasp_parsers / vasp_outcar_parsers.py: 94.84%

465 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-30 08:22 +0000

1# fmt: off 

2 

3""" 

4Module for parsing OUTCAR files. 

5""" 

6import re 

7from abc import ABC, abstractmethod 

8from collections.abc import Iterator, Sequence 

9from pathlib import Path, PurePath 

10from typing import Any, TextIO 

11from warnings import warn 

12 

13import numpy as np 

14 

15import ase 

16from ase import Atoms 

17from ase.calculators.singlepoint import ( 

18 SinglePointDFTCalculator, 

19 SinglePointKPoint, 

20) 

21from ase.data import atomic_numbers 

22from ase.io import ParseError, read 

23from ase.io.utils import ImageChunk 

24 

25# Denotes end of Ionic step for OUTCAR reading 

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

27 

28# Some type aliases 

29_HEADER = dict[str, Any] 

30_CURSOR = int 

31_CHUNK = Sequence[str] 

32_RESULT = dict[str, Any] 

33 

34 

35class NoNonEmptyLines(Exception): 

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

37 

38 

39class UnableToLocateDelimiter(Exception): 

40 """Did not find the provided delimiter""" 

41 

42 def __init__(self, delimiter, msg): 

43 self.delimiter = delimiter 

44 super().__init__(msg) 

45 

46 

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

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

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

50 Only call in cases we need the numeric values 

51 """ 

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

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

54 return line 

55 

56 

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

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

59 line which is non-empty. 

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

61 """ 

62 for line in lines[cursor:]: 

63 if line.strip(): 

64 # Line was non-empty 

65 return cursor 

66 # Empty line, increment the cursor position 

67 cursor += 1 

68 # There was no non-empty line 

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

70 

71 

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

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

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

75 for line in lines[cursor:]: 

76 if delim in line: 

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

78 assert delim in lines[cursor] 

79 return cursor 

80 # We didn't find the delimiter 

81 cursor += 1 

82 raise UnableToLocateDelimiter( 

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

84 

85 

86def convert_vasp_outcar_stress(stress: Sequence): 

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

88 expected units in ASE """ 

89 stress_arr = -np.array(stress) 

90 shape = stress_arr.shape 

91 if shape != (6, ): 

92 raise ValueError( 

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

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

95 return stress_arr 

96 

97 

98def read_constraints_from_file( 

99 directory: str | Path 

100 ) -> Sequence[str] | None: 

101 directory = Path(directory) 

102 constraint = None 

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

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

105 atoms = read(directory / filename, 

106 format='vasp', 

107 parallel=False) 

108 if isinstance(atoms, Atoms): 

109 constraint = atoms.constraints 

110 break 

111 return constraint 

112 

113 

114class VaspPropertyParser(ABC): 

115 NAME: str | None = None 

116 

117 @classmethod 

118 def get_name(cls): 

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

120 specify a custom name, 

121 otherwise the class name is used""" 

122 return cls.NAME or cls.__name__ 

123 

124 @abstractmethod 

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

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

127 cursor position""" 

128 

129 @staticmethod 

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

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

132 return _check_line(lines[cursor]) 

133 

134 @abstractmethod 

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

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

137 Assumes that "has_property" would evaluate to True 

138 from cursor position """ 

139 

140 

141class SimpleProperty(VaspPropertyParser, ABC): 

142 LINE_DELIMITER: str | None = None 

143 

144 def __init__(self): 

145 super().__init__() 

146 if self.LINE_DELIMITER is None: 

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

148 

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

150 line = lines[cursor] 

151 return self.LINE_DELIMITER in line 

152 

153 

154class VaspChunkPropertyParser(VaspPropertyParser, ABC): 

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

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

157 

158 def __init__(self, header: _HEADER | None = None): 

159 super().__init__() 

160 header = header or {} 

161 self.header = header 

162 

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

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

165 if that key doesn't exist""" 

166 try: 

167 return self.header[key] 

168 except KeyError: 

169 raise ParseError( 

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

171 key)) 

172 

173 

174class VaspHeaderPropertyParser(VaspPropertyParser, ABC): 

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

176 

177 

178class SimpleVaspChunkParser(VaspChunkPropertyParser, SimpleProperty, ABC): 

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

180 determined to exist from 1 line""" 

181 

182 

183class SimpleVaspHeaderParser(VaspHeaderPropertyParser, SimpleProperty, ABC): 

184 """Class for properties in the header 

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

186 

187 

188class Spinpol(SimpleVaspHeaderParser): 

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

190 

191 Example line: 

192 " ISPIN = 2 spin polarized calculation?" 

193 

194 """ 

195 LINE_DELIMITER = 'ISPIN' 

196 

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

198 line = lines[cursor].strip() 

199 parts = line.split() 

200 ispin = int(parts[2]) 

201 # ISPIN 2 = spinpolarized, otherwise no 

202 # ISPIN 1 = non-spinpolarized 

203 spinpol = ispin == 2 

204 return {'spinpol': spinpol} 

205 

206 

207class SpeciesTypes(SimpleVaspHeaderParser): 

208 """Parse species types. 

209 

210 Example line: 

211 " POTCAR: PAW_PBE Ni 02Aug2007" 

212 

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

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

215 """ 

216 LINE_DELIMITER = 'POTCAR:' 

217 

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

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

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

221 # as we only want to parse every second, 

222 # due to repeated entries in the OUTCAR 

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

224 

225 @property 

226 def species(self) -> list[str]: 

227 """Internal storage of each found line. 

228 Will contain the double counting. 

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

230 return self._species 

231 

232 def get_species(self) -> list[str]: 

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

234 This method only returns the first half, 

235 effectively removing the double counting. 

236 """ 

237 # Get the index of the first half 

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

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

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

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

242 # len(self.species) // 2 

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

244 # Make a copy 

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

246 

247 def _make_returnval(self) -> _RESULT: 

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

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

250 

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

252 line = lines[cursor].strip() 

253 

254 parts = line.split() 

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

256 if '1/r potential' in line: 

257 # This denotes an AE potential 

258 # Currently only H_AE 

259 # " H 1/r potential " 

260 idx = 1 

261 else: 

262 # Regular PAW potential, e.g. 

263 # "PAW_PBE H1.25 07Sep2000" or 

264 # "PAW_PBE Fe_pv 02Aug2007" 

265 idx = 2 

266 

267 sym = parts[idx] 

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

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

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

271 # remove any non-alphabetic characters 

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

273 

274 if sym not in atomic_numbers: 

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

276 # an element 

277 raise ParseError( 

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

279 

280 self.species.append(sym) 

281 

282 return self._make_returnval() 

283 

284 

285class IonsPerSpecies(SimpleVaspHeaderParser): 

286 """Example line: 

287 

288 " ions per type = 32 31 2" 

289 """ 

290 LINE_DELIMITER = 'ions per type' 

291 

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

293 line = lines[cursor].strip() 

294 parts = line.split() 

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

296 return {'ion_types': ion_types} 

297 

298 

299class KpointHeader(VaspHeaderPropertyParser): 

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

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

302 

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

304 line = lines[cursor] 

305 return "NKPTS" in line and "NBANDS" in line 

306 

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

308 line = lines[cursor].strip() 

309 parts = line.split() 

310 nkpts = int(parts[3]) 

311 nbands = int(parts[-1]) 

312 

313 results: dict[str, Any] = {'nkpts': nkpts, 'nbands': nbands} 

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

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

316 # for parsing that 

317 # Move cursor down to next delimiter 

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

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

320 line = line.strip() 

321 if delim2 in line: 

322 # build k-points 

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

324 kpt_weights = np.zeros(nkpts) 

325 for nk in range(nkpts): 

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

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

328 parts = line.split() 

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

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

331 results['ibzkpts'] = ibzkpts 

332 results['kpt_weights'] = kpt_weights 

333 break 

334 else: 

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

336 

337 return results 

338 

339 

340class Stress(SimpleVaspChunkParser): 

341 """Process the stress from an OUTCAR""" 

342 LINE_DELIMITER = 'in kB ' 

343 

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

345 line = self.get_line(cursor, lines) 

346 result: Sequence[float] | None = None 

347 try: 

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

349 except ValueError: 

350 # Vasp FORTRAN string formatting issues, can happen with 

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

352 # as a ParseError? 

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

354 else: 

355 result = convert_vasp_outcar_stress(stress) 

356 return {'stress': result} 

357 

358 

359class Cell(SimpleVaspChunkParser): 

360 LINE_DELIMITER = 'direct lattice vectors' 

361 

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

363 nskip = 1 

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

365 for i in range(3): 

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

367 parts = line.split() 

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

369 return {'cell': cell} 

370 

371 

372class PositionsAndForces(SimpleVaspChunkParser): 

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

374 We parse both simultaneously""" 

375 LINE_DELIMITER = 'POSITION ' 

376 

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

378 nskip = 2 

379 natoms = self.get_from_header('natoms') 

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

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

382 

383 for i in range(natoms): 

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

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

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

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

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

389 

390 

391class Magmom(VaspChunkPropertyParser): 

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

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

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

395 line = lines[cursor] 

396 if 'number of electron' in line: 

397 parts = line.split() 

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

399 return True 

400 return False 

401 

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

403 line = self.get_line(cursor, lines) 

404 parts = line.split() 

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

406 magmom_lst = parts[idx:] 

407 if len(magmom_lst) != 1: 

408 magmom: np.ndarray | float = np.array( 

409 list(map(float, magmom_lst)) 

410 ) 

411 else: 

412 magmom = float(magmom_lst[0]) 

413 return {'magmom': magmom} 

414 

415 

416class Magmoms(VaspChunkPropertyParser): 

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

418 line = lines[cursor] 

419 if 'magnetization (x)' in line: 

420 natoms = self.get_from_header('natoms') 

421 self.non_collinear = False 

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

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

424 if 'magnetization (y)' in line_y: 

425 self.non_collinear = True 

426 return True 

427 return False 

428 

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

430 

431 natoms = self.get_from_header('natoms') 

432 if self.non_collinear: 

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

434 nskip = 4 # Skip some lines 

435 for i in range(natoms): 

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

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

438 nskip = natoms + 13 # Skip some lines 

439 for i in range(natoms): 

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

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

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

443 for i in range(natoms): 

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

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

446 else: 

447 magmoms = np.zeros(natoms) 

448 nskip = 4 # Skip some lines 

449 for i in range(natoms): 

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

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

452 

453 return {'magmoms': magmoms} 

454 

455 

456class EFermi(SimpleVaspChunkParser): 

457 LINE_DELIMITER = 'E-fermi :' 

458 

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

460 line = self.get_line(cursor, lines) 

461 parts = line.split() 

462 efermi = float(parts[2]) 

463 return {'efermi': efermi} 

464 

465 

466class Energy(SimpleVaspChunkParser): 

467 LINE_DELIMITER = _OUTCAR_SCF_DELIM 

468 

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

470 nskip = 2 

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

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

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

474 

475 nskip = 4 

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

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

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

479 

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

481 

482 

483class Kpoints(VaspChunkPropertyParser): 

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

485 line = lines[cursor] 

486 # Example line: 

487 # " spin component 1" or " spin component 2" 

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

489 # as well 

490 if 'spin component 1' in line: 

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

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

493 if len(parts) == 3: 

494 try: 

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

496 # spin-up or spin-down 

497 int(parts[-1]) 

498 except ValueError: 

499 pass 

500 else: 

501 return True 

502 return False 

503 

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

505 nkpts = self.get_from_header('nkpts') 

506 nbands = self.get_from_header('nbands') 

507 weights = self.get_from_header('kpt_weights') 

508 spinpol = self.get_from_header('spinpol') 

509 nspins = 2 if spinpol else 1 

510 

511 kpts = [] 

512 for spin in range(nspins): 

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

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

515 # component line 

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

517 

518 cursor += 2 # Skip two lines 

519 for _ in range(nkpts): 

520 # Skip empty lines 

521 cursor = find_next_non_empty_line(cursor, lines) 

522 

523 line = self.get_line(cursor, lines) 

524 # Example line: 

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

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

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

528 weight = weights[ikpt] 

529 

530 cursor += 2 # Move down two 

531 eigenvalues = np.zeros(nbands) 

532 occupations = np.zeros(nbands) 

533 for n in range(nbands): 

534 # Example line: 

535 # " 1 -9.9948 1.00000" 

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

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

538 occupations[n] = f_n 

539 eigenvalues[n] = eps_n 

540 cursor += 1 

541 kpt = SinglePointKPoint(weight, 

542 spin, 

543 ikpt, 

544 eps_n=eigenvalues, 

545 f_n=occupations) 

546 kpts.append(kpt) 

547 

548 return {'kpts': kpts} 

549 

550 

551class DefaultParsersContainer: 

552 """Container for the default OUTCAR parsers. 

553 Allows for modification of the global default parsers. 

554 

555 Takes in an arbitrary number of parsers. 

556 The parsers should be uninitialized, 

557 as they are created on request. 

558 """ 

559 

560 def __init__(self, *parsers_cls): 

561 self._parsers_dct = {} 

562 for parser in parsers_cls: 

563 self.add_parser(parser) 

564 

565 @property 

566 def parsers_dct(self) -> dict: 

567 return self._parsers_dct 

568 

569 def make_parsers(self): 

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

571 Parsers are created upon request.""" 

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

573 

574 def remove_parser(self, name: str): 

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

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

577 self.parsers_dct.pop(name) 

578 

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

580 """Add a parser""" 

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

582 

583 

584class TypeParser(ABC): 

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

586 by applying the internal attached parsers""" 

587 

588 def __init__(self, parsers): 

589 self.parsers = parsers 

590 

591 @property 

592 def parsers(self): 

593 return self._parsers 

594 

595 @parsers.setter 

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

597 self._check_parsers(new_parsers) 

598 self._parsers = new_parsers 

599 

600 @abstractmethod 

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

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

603 

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

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

606 properties = {} 

607 for cursor, _ in enumerate(lines): 

608 for parser in self.parsers: 

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

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

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

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

613 # certain pieces of information to be written multiple 

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

615 # final values within a given chunk. 

616 if parser.has_property(cursor, lines): 

617 prop = parser.parse(cursor, lines) 

618 properties.update(prop) 

619 return properties 

620 

621 

622class ChunkParser(TypeParser, ABC): 

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

624 super().__init__(parsers) 

625 self.header = header 

626 

627 @property 

628 def header(self) -> _HEADER: 

629 return self._header 

630 

631 @header.setter 

632 def header(self, value: _HEADER | None) -> None: 

633 self._header = value or {} 

634 self.update_parser_headers() 

635 

636 def update_parser_headers(self) -> None: 

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

638 for parser in self.parsers: 

639 parser.header = self.header 

640 

641 def _check_parsers(self, 

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

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

644 if not all( 

645 isinstance(parser, VaspChunkPropertyParser) 

646 for parser in parsers): 

647 raise TypeError( 

648 'All parsers must be of type VaspChunkPropertyParser') 

649 

650 @abstractmethod 

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

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

653 

654 

655class HeaderParser(TypeParser, ABC): 

656 def _check_parsers(self, 

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

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

659 if not all( 

660 isinstance(parser, VaspHeaderPropertyParser) 

661 for parser in parsers): 

662 raise TypeError( 

663 'All parsers must be of type VaspHeaderPropertyParser') 

664 

665 @abstractmethod 

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

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

668 

669 

670class OutcarChunkParser(ChunkParser): 

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

672 

673 def __init__(self, 

674 header: _HEADER | None = None, 

675 parsers: Sequence[VaspChunkPropertyParser] | None = None): 

676 global default_chunk_parsers 

677 parsers = parsers or default_chunk_parsers.make_parsers() 

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

679 

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

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

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

683 

684 results = self.parse(lines) 

685 symbols = self.header['symbols'] 

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

687 

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

689 

690 # Find some required properties in the parsed results. 

691 # Raise ParseError if they are not present 

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

693 try: 

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

695 except KeyError: 

696 raise ParseError( 

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

698 prop)) 

699 atoms = Atoms(**atoms_kwargs) 

700 

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

702 calc = SinglePointDFTCalculator(atoms, **results) 

703 if kpts is not None: 

704 calc.kpts = kpts 

705 calc.name = 'vasp' 

706 atoms.calc = calc 

707 return atoms 

708 

709 

710class OutcarHeaderParser(HeaderParser): 

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

712 

713 def __init__(self, 

714 parsers: Sequence[VaspHeaderPropertyParser] | None = None, 

715 workdir: str | PurePath | None = None): 

716 global default_header_parsers 

717 parsers = parsers or default_header_parsers.make_parsers() 

718 super().__init__(parsers) 

719 self.workdir = workdir 

720 

721 @property 

722 def workdir(self): 

723 return self._workdir 

724 

725 @workdir.setter 

726 def workdir(self, value): 

727 if value is not None: 

728 value = Path(value) 

729 self._workdir = value 

730 

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

732 if 'symbols' in results: 

733 # Safeguard, in case a different parser already 

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

735 return results.pop('symbols') 

736 

737 # Build the symbols of the atoms 

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

739 if required_key not in results: 

740 raise ParseError( 

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

742 format(required_key)) 

743 

744 ion_types = results.pop('ion_types') 

745 species = results.pop('species') 

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

747 raise ParseError( 

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

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

750 len(ion_types), len(species))) 

751 

752 # Expand the symbols list 

753 symbols = [] 

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

755 symbols.extend(n * [sym]) 

756 return symbols 

757 

758 def _get_constraint(self): 

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

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

761 OUTCAR parser which does this. 

762 """ 

763 constraint = None 

764 if self.workdir is not None: 

765 constraint = read_constraints_from_file(self.workdir) 

766 return constraint 

767 

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

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

770 results = self.parse(lines) 

771 

772 # Get the symbols from the parsed results 

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

774 symbols = self._build_symbols(results) 

775 natoms = len(symbols) 

776 

777 constraint = self._get_constraint() 

778 

779 # Remaining results from the parse goes into the header 

780 header = dict(symbols=symbols, 

781 natoms=natoms, 

782 constraint=constraint, 

783 **results) 

784 return header 

785 

786 

787class OUTCARChunk(ImageChunk): 

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

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

790 """ 

791 

792 def __init__(self, 

793 lines: _CHUNK, 

794 header: _HEADER, 

795 parser: ChunkParser | None = None): 

796 super().__init__() 

797 self.lines = lines 

798 self.header = header 

799 self.parser = parser or OutcarChunkParser() 

800 

801 def build(self): 

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

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

804 

805 

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

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

808 lines = [] 

809 for line in fd: 

810 lines.append(line) 

811 if 'Iteration' in line: 

812 # Start of SCF cycle 

813 return lines 

814 

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

816 raise ParseError('Incomplete OUTCAR') 

817 

818 

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

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

821 lines = [] 

822 while True: 

823 line = next(fd) 

824 lines.append(line) 

825 if _OUTCAR_SCF_DELIM in line: 

826 # Add 4 more lines to include energy 

827 for _ in range(4): 

828 lines.append(next(fd)) 

829 break 

830 return lines 

831 

832 

833def outcarchunks( 

834 fd: TextIO, 

835 chunk_parser: ChunkParser | None = None, 

836 header_parser: HeaderParser | None = None 

837 ) -> Iterator[OUTCARChunk]: 

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

839 name = Path(fd.name) 

840 workdir = name.parent 

841 

842 # First we get header info 

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

844 header_parser = header_parser or OutcarHeaderParser(workdir=workdir) 

845 

846 lines = build_header(fd) 

847 header = header_parser.build(lines) 

848 assert isinstance(header, dict) 

849 

850 chunk_parser = chunk_parser or OutcarChunkParser() 

851 

852 while True: 

853 try: 

854 lines = build_chunk(fd) 

855 except StopIteration: 

856 # End of file 

857 return 

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

859 

860 

861# Create the default chunk parsers 

862default_chunk_parsers = DefaultParsersContainer( 

863 Cell, 

864 PositionsAndForces, 

865 Stress, 

866 Magmoms, 

867 Magmom, 

868 EFermi, 

869 Kpoints, 

870 Energy, 

871) 

872 

873# Create the default header parsers 

874default_header_parsers = DefaultParsersContainer( 

875 SpeciesTypes, 

876 IonsPerSpecies, 

877 Spinpol, 

878 KpointHeader, 

879)