Coverage for /builds/debichem-team/python-ase/ase/io/vasp_parsers/vasp_outcar_parsers.py: 95.24%

462 statements  

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

1""" 

2Module for parsing OUTCAR files. 

3""" 

4import re 

5from abc import ABC, abstractmethod 

6from pathlib import Path, PurePath 

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

8from warnings import warn 

9 

10import numpy as np 

11 

12import ase 

13from ase import Atoms 

14from ase.calculators.singlepoint import ( 

15 SinglePointDFTCalculator, 

16 SinglePointKPoint, 

17) 

18from ase.data import atomic_numbers 

19from ase.io import ParseError, read 

20from ase.io.utils import ImageChunk 

21 

22# Denotes end of Ionic step for OUTCAR reading 

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

24 

25# Some type aliases 

26_HEADER = Dict[str, Any] 

27_CURSOR = int 

28_CHUNK = Sequence[str] 

29_RESULT = Dict[str, Any] 

30 

31 

32class NoNonEmptyLines(Exception): 

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

34 

35 

36class UnableToLocateDelimiter(Exception): 

37 """Did not find the provided delimiter""" 

38 

39 def __init__(self, delimiter, msg): 

40 self.delimiter = delimiter 

41 super().__init__(msg) 

42 

43 

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

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

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

47 Only call in cases we need the numeric values 

48 """ 

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

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

51 return line 

52 

53 

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

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

56 line which is non-empty. 

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

58 """ 

59 for line in lines[cursor:]: 

60 if line.strip(): 

61 # Line was non-empty 

62 return cursor 

63 # Empty line, increment the cursor position 

64 cursor += 1 

65 # There was no non-empty line 

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

67 

68 

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

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

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

72 for line in lines[cursor:]: 

73 if delim in line: 

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

75 assert delim in lines[cursor] 

76 return cursor 

77 # We didn't find the delimiter 

78 cursor += 1 

79 raise UnableToLocateDelimiter( 

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

81 

82 

83def convert_vasp_outcar_stress(stress: Sequence): 

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

85 expected units in ASE """ 

86 stress_arr = -np.array(stress) 

87 shape = stress_arr.shape 

88 if shape != (6, ): 

89 raise ValueError( 

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

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

92 return stress_arr 

93 

94 

95def read_constraints_from_file(directory): 

96 directory = Path(directory) 

97 constraint = None 

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

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

100 constraint = read(directory / filename, 

101 format='vasp', 

102 parallel=False).constraints 

103 break 

104 return constraint 

105 

106 

107class VaspPropertyParser(ABC): 

108 NAME = None # type: str 

109 

110 @classmethod 

111 def get_name(cls): 

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

113 specify a custom name, 

114 otherwise the class name is used""" 

115 return cls.NAME or cls.__name__ 

116 

117 @abstractmethod 

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

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

120 cursor position""" 

121 

122 @staticmethod 

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

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

125 return _check_line(lines[cursor]) 

126 

127 @abstractmethod 

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

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

130 Assumes that "has_property" would evaluate to True 

131 from cursor position """ 

132 

133 

134class SimpleProperty(VaspPropertyParser, ABC): 

135 LINE_DELIMITER = None # type: str 

136 

137 def __init__(self): 

138 super().__init__() 

139 if self.LINE_DELIMITER is None: 

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

141 

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

143 line = lines[cursor] 

144 return self.LINE_DELIMITER in line 

145 

146 

147class VaspChunkPropertyParser(VaspPropertyParser, ABC): 

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

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

150 

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

152 super().__init__() 

153 header = header or {} 

154 self.header = header 

155 

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

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

158 if that key doesn't exist""" 

159 try: 

160 return self.header[key] 

161 except KeyError: 

162 raise ParseError( 

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

164 key)) 

165 

166 

167class VaspHeaderPropertyParser(VaspPropertyParser, ABC): 

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

169 

170 

171class SimpleVaspChunkParser(VaspChunkPropertyParser, SimpleProperty, ABC): 

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

173 determined to exist from 1 line""" 

174 

175 

176class SimpleVaspHeaderParser(VaspHeaderPropertyParser, SimpleProperty, ABC): 

177 """Class for properties in the header 

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

179 

180 

181class Spinpol(SimpleVaspHeaderParser): 

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

183 

184 Example line: 

185 " ISPIN = 2 spin polarized calculation?" 

186 

187 """ 

188 LINE_DELIMITER = 'ISPIN' 

189 

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

191 line = lines[cursor].strip() 

192 parts = line.split() 

193 ispin = int(parts[2]) 

194 # ISPIN 2 = spinpolarized, otherwise no 

195 # ISPIN 1 = non-spinpolarized 

196 spinpol = ispin == 2 

197 return {'spinpol': spinpol} 

198 

199 

200class SpeciesTypes(SimpleVaspHeaderParser): 

201 """Parse species types. 

202 

203 Example line: 

204 " POTCAR: PAW_PBE Ni 02Aug2007" 

205 

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

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

208 """ 

209 LINE_DELIMITER = 'POTCAR:' 

210 

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

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

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

214 # as we only want to parse every second, 

215 # due to repeated entries in the OUTCAR 

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

217 

218 @property 

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

220 """Internal storage of each found line. 

221 Will contain the double counting. 

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

223 return self._species 

224 

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

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

227 This method only returns the first half, 

228 effectively removing the double counting. 

229 """ 

230 # Get the index of the first half 

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

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

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

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

235 # len(self.species) // 2 

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

237 # Make a copy 

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

239 

240 def _make_returnval(self) -> _RESULT: 

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

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

243 

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

245 line = lines[cursor].strip() 

246 

247 parts = line.split() 

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

249 if '1/r potential' in line: 

250 # This denotes an AE potential 

251 # Currently only H_AE 

252 # " H 1/r potential " 

253 idx = 1 

254 else: 

255 # Regular PAW potential, e.g. 

256 # "PAW_PBE H1.25 07Sep2000" or 

257 # "PAW_PBE Fe_pv 02Aug2007" 

258 idx = 2 

259 

260 sym = parts[idx] 

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

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

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

264 # remove any non-alphabetic characters 

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

266 

267 if sym not in atomic_numbers: 

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

269 # an element 

270 raise ParseError( 

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

272 

273 self.species.append(sym) 

274 

275 return self._make_returnval() 

276 

277 

278class IonsPerSpecies(SimpleVaspHeaderParser): 

279 """Example line: 

280 

281 " ions per type = 32 31 2" 

282 """ 

283 LINE_DELIMITER = 'ions per type' 

284 

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

286 line = lines[cursor].strip() 

287 parts = line.split() 

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

289 return {'ion_types': ion_types} 

290 

291 

292class KpointHeader(VaspHeaderPropertyParser): 

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

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

295 

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

297 line = lines[cursor] 

298 return "NKPTS" in line and "NBANDS" in line 

299 

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

301 line = lines[cursor].strip() 

302 parts = line.split() 

303 nkpts = int(parts[3]) 

304 nbands = int(parts[-1]) 

305 

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

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

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

309 # for parsing that 

310 # Move cursor down to next delimiter 

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

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

313 line = line.strip() 

314 if delim2 in line: 

315 # build k-points 

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

317 kpt_weights = np.zeros(nkpts) 

318 for nk in range(nkpts): 

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

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

321 parts = line.split() 

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

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

324 results['ibzkpts'] = ibzkpts 

325 results['kpt_weights'] = kpt_weights 

326 break 

327 else: 

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

329 

330 return results 

331 

332 

333class Stress(SimpleVaspChunkParser): 

334 """Process the stress from an OUTCAR""" 

335 LINE_DELIMITER = 'in kB ' 

336 

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

338 line = self.get_line(cursor, lines) 

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

340 try: 

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

342 except ValueError: 

343 # Vasp FORTRAN string formatting issues, can happen with 

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

345 # as a ParseError? 

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

347 else: 

348 result = convert_vasp_outcar_stress(stress) 

349 return {'stress': result} 

350 

351 

352class Cell(SimpleVaspChunkParser): 

353 LINE_DELIMITER = 'direct lattice vectors' 

354 

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

356 nskip = 1 

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

358 for i in range(3): 

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

360 parts = line.split() 

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

362 return {'cell': cell} 

363 

364 

365class PositionsAndForces(SimpleVaspChunkParser): 

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

367 We parse both simultaneously""" 

368 LINE_DELIMITER = 'POSITION ' 

369 

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

371 nskip = 2 

372 natoms = self.get_from_header('natoms') 

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

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

375 

376 for i in range(natoms): 

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

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

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

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

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

382 

383 

384class Magmom(VaspChunkPropertyParser): 

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

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

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

388 line = lines[cursor] 

389 if 'number of electron' in line: 

390 parts = line.split() 

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

392 return True 

393 return False 

394 

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

396 line = self.get_line(cursor, lines) 

397 parts = line.split() 

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

399 magmom_lst = parts[idx:] 

400 if len(magmom_lst) != 1: 

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

402 list(map(float, magmom_lst)) 

403 ) 

404 else: 

405 magmom = float(magmom_lst[0]) 

406 return {'magmom': magmom} 

407 

408 

409class Magmoms(VaspChunkPropertyParser): 

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

411 line = lines[cursor] 

412 if 'magnetization (x)' in line: 

413 natoms = self.get_from_header('natoms') 

414 self.non_collinear = False 

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

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

417 if 'magnetization (y)' in line_y: 

418 self.non_collinear = True 

419 return True 

420 return False 

421 

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

423 

424 natoms = self.get_from_header('natoms') 

425 if self.non_collinear: 

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

427 nskip = 4 # Skip some lines 

428 for i in range(natoms): 

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

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

431 nskip = natoms + 13 # Skip some lines 

432 for i in range(natoms): 

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

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

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

436 for i in range(natoms): 

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

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

439 else: 

440 magmoms = np.zeros(natoms) 

441 nskip = 4 # Skip some lines 

442 for i in range(natoms): 

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

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

445 

446 return {'magmoms': magmoms} 

447 

448 

449class EFermi(SimpleVaspChunkParser): 

450 LINE_DELIMITER = 'E-fermi :' 

451 

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

453 line = self.get_line(cursor, lines) 

454 parts = line.split() 

455 efermi = float(parts[2]) 

456 return {'efermi': efermi} 

457 

458 

459class Energy(SimpleVaspChunkParser): 

460 LINE_DELIMITER = _OUTCAR_SCF_DELIM 

461 

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

463 nskip = 2 

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

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

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

467 

468 nskip = 4 

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

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

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

472 

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

474 

475 

476class Kpoints(VaspChunkPropertyParser): 

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

478 line = lines[cursor] 

479 # Example line: 

480 # " spin component 1" or " spin component 2" 

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

482 # as well 

483 if 'spin component 1' in line: 

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

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

486 if len(parts) == 3: 

487 try: 

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

489 # spin-up or spin-down 

490 int(parts[-1]) 

491 except ValueError: 

492 pass 

493 else: 

494 return True 

495 return False 

496 

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

498 nkpts = self.get_from_header('nkpts') 

499 nbands = self.get_from_header('nbands') 

500 weights = self.get_from_header('kpt_weights') 

501 spinpol = self.get_from_header('spinpol') 

502 nspins = 2 if spinpol else 1 

503 

504 kpts = [] 

505 for spin in range(nspins): 

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

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

508 # component line 

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

510 

511 cursor += 2 # Skip two lines 

512 for _ in range(nkpts): 

513 # Skip empty lines 

514 cursor = find_next_non_empty_line(cursor, lines) 

515 

516 line = self.get_line(cursor, lines) 

517 # Example line: 

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

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

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

521 weight = weights[ikpt] 

522 

523 cursor += 2 # Move down two 

524 eigenvalues = np.zeros(nbands) 

525 occupations = np.zeros(nbands) 

526 for n in range(nbands): 

527 # Example line: 

528 # " 1 -9.9948 1.00000" 

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

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

531 occupations[n] = f_n 

532 eigenvalues[n] = eps_n 

533 cursor += 1 

534 kpt = SinglePointKPoint(weight, 

535 spin, 

536 ikpt, 

537 eps_n=eigenvalues, 

538 f_n=occupations) 

539 kpts.append(kpt) 

540 

541 return {'kpts': kpts} 

542 

543 

544class DefaultParsersContainer: 

545 """Container for the default OUTCAR parsers. 

546 Allows for modification of the global default parsers. 

547 

548 Takes in an arbitrary number of parsers. 

549 The parsers should be uninitialized, 

550 as they are created on request. 

551 """ 

552 

553 def __init__(self, *parsers_cls): 

554 self._parsers_dct = {} 

555 for parser in parsers_cls: 

556 self.add_parser(parser) 

557 

558 @property 

559 def parsers_dct(self) -> dict: 

560 return self._parsers_dct 

561 

562 def make_parsers(self): 

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

564 Parsers are created upon request.""" 

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

566 

567 def remove_parser(self, name: str): 

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

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

570 self.parsers_dct.pop(name) 

571 

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

573 """Add a parser""" 

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

575 

576 

577class TypeParser(ABC): 

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

579 by applying the internal attached parsers""" 

580 

581 def __init__(self, parsers): 

582 self.parsers = parsers 

583 

584 @property 

585 def parsers(self): 

586 return self._parsers 

587 

588 @parsers.setter 

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

590 self._check_parsers(new_parsers) 

591 self._parsers = new_parsers 

592 

593 @abstractmethod 

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

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

596 

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

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

599 properties = {} 

600 for cursor, _ in enumerate(lines): 

601 for parser in self.parsers: 

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

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

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

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

606 # certain pieces of information to be written multiple 

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

608 # final values within a given chunk. 

609 if parser.has_property(cursor, lines): 

610 prop = parser.parse(cursor, lines) 

611 properties.update(prop) 

612 return properties 

613 

614 

615class ChunkParser(TypeParser, ABC): 

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

617 super().__init__(parsers) 

618 self.header = header 

619 

620 @property 

621 def header(self) -> _HEADER: 

622 return self._header 

623 

624 @header.setter 

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

626 self._header = value or {} 

627 self.update_parser_headers() 

628 

629 def update_parser_headers(self) -> None: 

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

631 for parser in self.parsers: 

632 parser.header = self.header 

633 

634 def _check_parsers(self, 

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

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

637 if not all( 

638 isinstance(parser, VaspChunkPropertyParser) 

639 for parser in parsers): 

640 raise TypeError( 

641 'All parsers must be of type VaspChunkPropertyParser') 

642 

643 @abstractmethod 

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

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

646 

647 

648class HeaderParser(TypeParser, ABC): 

649 def _check_parsers(self, 

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

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

652 if not all( 

653 isinstance(parser, VaspHeaderPropertyParser) 

654 for parser in parsers): 

655 raise TypeError( 

656 'All parsers must be of type VaspHeaderPropertyParser') 

657 

658 @abstractmethod 

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

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

661 

662 

663class OutcarChunkParser(ChunkParser): 

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

665 

666 def __init__(self, 

667 header: _HEADER = None, 

668 parsers: Sequence[VaspChunkPropertyParser] = None): 

669 global default_chunk_parsers 

670 parsers = parsers or default_chunk_parsers.make_parsers() 

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

672 

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

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

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

676 

677 results = self.parse(lines) 

678 symbols = self.header['symbols'] 

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

680 

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

682 

683 # Find some required properties in the parsed results. 

684 # Raise ParseError if they are not present 

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

686 try: 

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

688 except KeyError: 

689 raise ParseError( 

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

691 prop)) 

692 atoms = Atoms(**atoms_kwargs) 

693 

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

695 calc = SinglePointDFTCalculator(atoms, **results) 

696 if kpts is not None: 

697 calc.kpts = kpts 

698 calc.name = 'vasp' 

699 atoms.calc = calc 

700 return atoms 

701 

702 

703class OutcarHeaderParser(HeaderParser): 

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

705 

706 def __init__(self, 

707 parsers: Sequence[VaspHeaderPropertyParser] = None, 

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

709 global default_header_parsers 

710 parsers = parsers or default_header_parsers.make_parsers() 

711 super().__init__(parsers) 

712 self.workdir = workdir 

713 

714 @property 

715 def workdir(self): 

716 return self._workdir 

717 

718 @workdir.setter 

719 def workdir(self, value): 

720 if value is not None: 

721 value = Path(value) 

722 self._workdir = value 

723 

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

725 if 'symbols' in results: 

726 # Safeguard, in case a different parser already 

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

728 return results.pop('symbols') 

729 

730 # Build the symbols of the atoms 

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

732 if required_key not in results: 

733 raise ParseError( 

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

735 format(required_key)) 

736 

737 ion_types = results.pop('ion_types') 

738 species = results.pop('species') 

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

740 raise ParseError( 

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

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

743 len(ion_types), len(species))) 

744 

745 # Expand the symbols list 

746 symbols = [] 

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

748 symbols.extend(n * [sym]) 

749 return symbols 

750 

751 def _get_constraint(self): 

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

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

754 OUTCAR parser which does this. 

755 """ 

756 constraint = None 

757 if self.workdir is not None: 

758 constraint = read_constraints_from_file(self.workdir) 

759 return constraint 

760 

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

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

763 results = self.parse(lines) 

764 

765 # Get the symbols from the parsed results 

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

767 symbols = self._build_symbols(results) 

768 natoms = len(symbols) 

769 

770 constraint = self._get_constraint() 

771 

772 # Remaining results from the parse goes into the header 

773 header = dict(symbols=symbols, 

774 natoms=natoms, 

775 constraint=constraint, 

776 **results) 

777 return header 

778 

779 

780class OUTCARChunk(ImageChunk): 

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

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

783 """ 

784 

785 def __init__(self, 

786 lines: _CHUNK, 

787 header: _HEADER, 

788 parser: ChunkParser = None): 

789 super().__init__() 

790 self.lines = lines 

791 self.header = header 

792 self.parser = parser or OutcarChunkParser() 

793 

794 def build(self): 

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

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

797 

798 

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

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

801 lines = [] 

802 for line in fd: 

803 lines.append(line) 

804 if 'Iteration' in line: 

805 # Start of SCF cycle 

806 return lines 

807 

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

809 raise ParseError('Incomplete OUTCAR') 

810 

811 

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

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

814 lines = [] 

815 while True: 

816 line = next(fd) 

817 lines.append(line) 

818 if _OUTCAR_SCF_DELIM in line: 

819 # Add 4 more lines to include energy 

820 for _ in range(4): 

821 lines.append(next(fd)) 

822 break 

823 return lines 

824 

825 

826def outcarchunks(fd: TextIO, 

827 chunk_parser: ChunkParser = None, 

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

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

830 name = Path(fd.name) 

831 workdir = name.parent 

832 

833 # First we get header info 

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

835 header_parser = header_parser or OutcarHeaderParser(workdir=workdir) 

836 

837 lines = build_header(fd) 

838 header = header_parser.build(lines) 

839 assert isinstance(header, dict) 

840 

841 chunk_parser = chunk_parser or OutcarChunkParser() 

842 

843 while True: 

844 try: 

845 lines = build_chunk(fd) 

846 except StopIteration: 

847 # End of file 

848 return 

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

850 

851 

852# Create the default chunk parsers 

853default_chunk_parsers = DefaultParsersContainer( 

854 Cell, 

855 PositionsAndForces, 

856 Stress, 

857 Magmoms, 

858 Magmom, 

859 EFermi, 

860 Kpoints, 

861 Energy, 

862) 

863 

864# Create the default header parsers 

865default_header_parsers = DefaultParsersContainer( 

866 SpeciesTypes, 

867 IonsPerSpecies, 

868 Spinpol, 

869 KpointHeader, 

870)