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

438 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-22 11:22 +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 (SinglePointDFTCalculator, 

15 SinglePointKPoint) 

16from ase.data import atomic_numbers 

17from ase.io import ParseError, read 

18from ase.io.utils import ImageChunk 

19 

20# Denotes end of Ionic step for OUTCAR reading 

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

22 

23# Some type aliases 

24_HEADER = Dict[str, Any] 

25_CURSOR = int 

26_CHUNK = Sequence[str] 

27_RESULT = Dict[str, Any] 

28 

29 

30class NoNonEmptyLines(Exception): 

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

32 

33 

34class UnableToLocateDelimiter(Exception): 

35 """Did not find the provided delimiter""" 

36 

37 def __init__(self, delimiter, msg): 

38 self.delimiter = delimiter 

39 super().__init__(msg) 

40 

41 

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

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

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

45 Only call in cases we need the numeric values 

46 """ 

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

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

49 return line 

50 

51 

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

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

54 line which is non-empty. 

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

56 """ 

57 for line in lines[cursor:]: 

58 if line.strip(): 

59 # Line was non-empty 

60 return cursor 

61 # Empty line, increment the cursor position 

62 cursor += 1 

63 # There was no non-empty line 

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

65 

66 

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

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

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

70 for line in lines[cursor:]: 

71 if delim in line: 

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

73 assert delim in lines[cursor] 

74 return cursor 

75 # We didn't find the delimiter 

76 cursor += 1 

77 raise UnableToLocateDelimiter( 

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

79 

80 

81def convert_vasp_outcar_stress(stress: Sequence): 

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

83 expected units in ASE """ 

84 stress_arr = -np.array(stress) 

85 shape = stress_arr.shape 

86 if shape != (6, ): 

87 raise ValueError( 

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

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

90 return stress_arr 

91 

92 

93def read_constraints_from_file(directory): 

94 directory = Path(directory) 

95 constraint = None 

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

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

98 constraint = read(directory / filename, 

99 format='vasp', 

100 parallel=False).constraints 

101 break 

102 return constraint 

103 

104 

105class VaspPropertyParser(ABC): 

106 NAME = None # type: str 

107 

108 @classmethod 

109 def get_name(cls): 

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

111 specify a custom name, 

112 otherwise the class name is used""" 

113 return cls.NAME or cls.__name__ 

114 

115 @abstractmethod 

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

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

118 cursor position""" 

119 

120 @staticmethod 

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

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

123 return _check_line(lines[cursor]) 

124 

125 @abstractmethod 

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

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

128 Assumes that "has_property" would evaluate to True 

129 from cursor position """ 

130 

131 

132class SimpleProperty(VaspPropertyParser, ABC): 

133 LINE_DELIMITER = None # type: str 

134 

135 def __init__(self): 

136 super().__init__() 

137 if self.LINE_DELIMITER is None: 

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

139 

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

141 line = lines[cursor] 

142 return self.LINE_DELIMITER in line 

143 

144 

145class VaspChunkPropertyParser(VaspPropertyParser, ABC): 

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

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

148 

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

150 super().__init__() 

151 header = header or {} 

152 self.header = header 

153 

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

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

156 if that key doesn't exist""" 

157 try: 

158 return self.header[key] 

159 except KeyError: 

160 raise ParseError( 

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

162 key)) 

163 

164 

165class VaspHeaderPropertyParser(VaspPropertyParser, ABC): 

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

167 

168 

169class SimpleVaspChunkParser(VaspChunkPropertyParser, SimpleProperty, ABC): 

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

171 determined to exist from 1 line""" 

172 

173 

174class SimpleVaspHeaderParser(VaspHeaderPropertyParser, SimpleProperty, ABC): 

175 """Class for properties in the header 

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

177 

178 

179class Spinpol(SimpleVaspHeaderParser): 

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

181 

182 Example line: 

183 " ISPIN = 2 spin polarized calculation?" 

184 

185 """ 

186 LINE_DELIMITER = 'ISPIN' 

187 

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

189 line = lines[cursor].strip() 

190 parts = line.split() 

191 ispin = int(parts[2]) 

192 # ISPIN 2 = spinpolarized, otherwise no 

193 # ISPIN 1 = non-spinpolarized 

194 spinpol = ispin == 2 

195 return {'spinpol': spinpol} 

196 

197 

198class SpeciesTypes(SimpleVaspHeaderParser): 

199 """Parse species types. 

200 

201 Example line: 

202 " POTCAR: PAW_PBE Ni 02Aug2007" 

203 

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

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

206 """ 

207 LINE_DELIMITER = 'POTCAR:' 

208 

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

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

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

212 # as we only want to parse every second, 

213 # due to repeated entries in the OUTCAR 

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

215 

216 @property 

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

218 """Internal storage of each found line. 

219 Will contain the double counting. 

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

221 return self._species 

222 

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

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

225 This method only returns the first half, 

226 effectively removing the double counting. 

227 """ 

228 # Get the index of the first half 

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

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

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

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

233 # len(self.species) // 2 

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

235 # Make a copy 

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

237 

238 def _make_returnval(self) -> _RESULT: 

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

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

241 

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

243 line = lines[cursor].strip() 

244 

245 parts = line.split() 

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

247 if '1/r potential' in line: 

248 # This denotes an AE potential 

249 # Currently only H_AE 

250 # " H 1/r potential " 

251 idx = 1 

252 else: 

253 # Regular PAW potential, e.g. 

254 # "PAW_PBE H1.25 07Sep2000" or 

255 # "PAW_PBE Fe_pv 02Aug2007" 

256 idx = 2 

257 

258 sym = parts[idx] 

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

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

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

262 # remove any non-alphabetic characters 

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

264 

265 if sym not in atomic_numbers: 

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

267 # an element 

268 raise ParseError( 

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

270 

271 self.species.append(sym) 

272 

273 return self._make_returnval() 

274 

275 

276class IonsPerSpecies(SimpleVaspHeaderParser): 

277 """Example line: 

278 

279 " ions per type = 32 31 2" 

280 """ 

281 LINE_DELIMITER = 'ions per type' 

282 

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

284 line = lines[cursor].strip() 

285 parts = line.split() 

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

287 return {'ion_types': ion_types} 

288 

289 

290class KpointHeader(VaspHeaderPropertyParser): 

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

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

293 

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

295 line = lines[cursor] 

296 return "NKPTS" in line and "NBANDS" in line 

297 

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

299 line = lines[cursor].strip() 

300 parts = line.split() 

301 nkpts = int(parts[3]) 

302 nbands = int(parts[-1]) 

303 

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

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

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

307 # for parsing that 

308 # Move cursor down to next delimiter 

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

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

311 line = line.strip() 

312 if delim2 in line: 

313 # build k-points 

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

315 kpt_weights = np.zeros(nkpts) 

316 for nk in range(nkpts): 

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

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

319 parts = line.split() 

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

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

322 results['ibzkpts'] = ibzkpts 

323 results['kpt_weights'] = kpt_weights 

324 break 

325 else: 

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

327 

328 return results 

329 

330 

331class Stress(SimpleVaspChunkParser): 

332 """Process the stress from an OUTCAR""" 

333 LINE_DELIMITER = 'in kB ' 

334 

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

336 line = self.get_line(cursor, lines) 

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

338 try: 

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

340 except ValueError: 

341 # Vasp FORTRAN string formatting issues, can happen with 

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

343 # as a ParseError? 

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

345 else: 

346 result = convert_vasp_outcar_stress(stress) 

347 return {'stress': result} 

348 

349 

350class Cell(SimpleVaspChunkParser): 

351 LINE_DELIMITER = 'direct lattice vectors' 

352 

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

354 nskip = 1 

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

356 for i in range(3): 

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

358 parts = line.split() 

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

360 return {'cell': cell} 

361 

362 

363class PositionsAndForces(SimpleVaspChunkParser): 

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

365 We parse both simultaneously""" 

366 LINE_DELIMITER = 'POSITION ' 

367 

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

369 nskip = 2 

370 natoms = self.get_from_header('natoms') 

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

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

373 

374 for i in range(natoms): 

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

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

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

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

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

380 

381 

382class Magmom(VaspChunkPropertyParser): 

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

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

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

386 line = lines[cursor] 

387 if 'number of electron' in line: 

388 parts = line.split() 

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

390 return True 

391 return False 

392 

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

394 line = self.get_line(cursor, lines) 

395 parts = line.split() 

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

397 magmom_lst = parts[idx:] 

398 if len(magmom_lst) != 1: 

399 warn( 

400 'Non-collinear spin is not yet implemented. ' 

401 'Setting magmom to x value.') 

402 magmom = float(magmom_lst[0]) 

403 # Use these lines when non-collinear spin is supported! 

404 # Remember to check that format fits! 

405 # else: 

406 # # Non-collinear spin 

407 # # Make a (3,) dim array 

408 # magmom = np.array(list(map(float, magmom))) 

409 return {'magmom': magmom} 

410 

411 

412class Magmoms(SimpleVaspChunkParser): 

413 """Get the x-component of the magnitization. 

414 This is just the magmoms in the collinear case. 

415 

416 non-collinear spin is (currently) not supported""" 

417 LINE_DELIMITER = 'magnetization (x)' 

418 

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

420 # Magnetization for collinear 

421 natoms = self.get_from_header('natoms') 

422 nskip = 4 # Skip some lines 

423 magmoms = np.zeros(natoms) 

424 for i in range(natoms): 

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

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

427 # Once we support non-collinear spin, 

428 # search for magnetization (y) and magnetization (z) as well. 

429 return {'magmoms': magmoms} 

430 

431 

432class EFermi(SimpleVaspChunkParser): 

433 LINE_DELIMITER = 'E-fermi :' 

434 

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

436 line = self.get_line(cursor, lines) 

437 parts = line.split() 

438 efermi = float(parts[2]) 

439 return {'efermi': efermi} 

440 

441 

442class Energy(SimpleVaspChunkParser): 

443 LINE_DELIMITER = _OUTCAR_SCF_DELIM 

444 

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

446 nskip = 2 

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

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

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

450 

451 nskip = 4 

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

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

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

455 

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

457 

458 

459class Kpoints(VaspChunkPropertyParser): 

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

461 line = lines[cursor] 

462 # Example line: 

463 # " spin component 1" or " spin component 2" 

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

465 # as well 

466 if 'spin component 1' in line: 

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

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

469 if len(parts) == 3: 

470 try: 

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

472 # spin-up or spin-down 

473 int(parts[-1]) 

474 except ValueError: 

475 pass 

476 else: 

477 return True 

478 return False 

479 

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

481 nkpts = self.get_from_header('nkpts') 

482 nbands = self.get_from_header('nbands') 

483 weights = self.get_from_header('kpt_weights') 

484 spinpol = self.get_from_header('spinpol') 

485 nspins = 2 if spinpol else 1 

486 

487 kpts = [] 

488 for spin in range(nspins): 

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

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

491 # component line 

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

493 

494 cursor += 2 # Skip two lines 

495 for _ in range(nkpts): 

496 # Skip empty lines 

497 cursor = find_next_non_empty_line(cursor, lines) 

498 

499 line = self.get_line(cursor, lines) 

500 # Example line: 

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

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

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

504 weight = weights[ikpt] 

505 

506 cursor += 2 # Move down two 

507 eigenvalues = np.zeros(nbands) 

508 occupations = np.zeros(nbands) 

509 for n in range(nbands): 

510 # Example line: 

511 # " 1 -9.9948 1.00000" 

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

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

514 occupations[n] = f_n 

515 eigenvalues[n] = eps_n 

516 cursor += 1 

517 kpt = SinglePointKPoint(weight, 

518 spin, 

519 ikpt, 

520 eps_n=eigenvalues, 

521 f_n=occupations) 

522 kpts.append(kpt) 

523 

524 return {'kpts': kpts} 

525 

526 

527class DefaultParsersContainer: 

528 """Container for the default OUTCAR parsers. 

529 Allows for modification of the global default parsers. 

530 

531 Takes in an arbitrary number of parsers. 

532 The parsers should be uninitialized, 

533 as they are created on request. 

534 """ 

535 

536 def __init__(self, *parsers_cls): 

537 self._parsers_dct = {} 

538 for parser in parsers_cls: 

539 self.add_parser(parser) 

540 

541 @property 

542 def parsers_dct(self) -> dict: 

543 return self._parsers_dct 

544 

545 def make_parsers(self): 

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

547 Parsers are created upon request.""" 

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

549 

550 def remove_parser(self, name: str): 

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

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

553 self.parsers_dct.pop(name) 

554 

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

556 """Add a parser""" 

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

558 

559 

560class TypeParser(ABC): 

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

562 by applying the internal attached parsers""" 

563 

564 def __init__(self, parsers): 

565 self.parsers = parsers 

566 

567 @property 

568 def parsers(self): 

569 return self._parsers 

570 

571 @parsers.setter 

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

573 self._check_parsers(new_parsers) 

574 self._parsers = new_parsers 

575 

576 @abstractmethod 

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

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

579 

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

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

582 properties = {} 

583 for cursor, _ in enumerate(lines): 

584 for parser in self.parsers: 

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

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

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

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

589 # certain pieces of information to be written multiple 

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

591 # final values within a given chunk. 

592 if parser.has_property(cursor, lines): 

593 prop = parser.parse(cursor, lines) 

594 properties.update(prop) 

595 return properties 

596 

597 

598class ChunkParser(TypeParser, ABC): 

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

600 super().__init__(parsers) 

601 self.header = header 

602 

603 @property 

604 def header(self) -> _HEADER: 

605 return self._header 

606 

607 @header.setter 

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

609 self._header = value or {} 

610 self.update_parser_headers() 

611 

612 def update_parser_headers(self) -> None: 

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

614 for parser in self.parsers: 

615 parser.header = self.header 

616 

617 def _check_parsers(self, 

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

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

620 if not all( 

621 isinstance(parser, VaspChunkPropertyParser) 

622 for parser in parsers): 

623 raise TypeError( 

624 'All parsers must be of type VaspChunkPropertyParser') 

625 

626 @abstractmethod 

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

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

629 

630 

631class HeaderParser(TypeParser, ABC): 

632 def _check_parsers(self, 

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

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

635 if not all( 

636 isinstance(parser, VaspHeaderPropertyParser) 

637 for parser in parsers): 

638 raise TypeError( 

639 'All parsers must be of type VaspHeaderPropertyParser') 

640 

641 @abstractmethod 

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

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

644 

645 

646class OutcarChunkParser(ChunkParser): 

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

648 

649 def __init__(self, 

650 header: _HEADER = None, 

651 parsers: Sequence[VaspChunkPropertyParser] = None): 

652 global default_chunk_parsers 

653 parsers = parsers or default_chunk_parsers.make_parsers() 

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

655 

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

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

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

659 

660 results = self.parse(lines) 

661 symbols = self.header['symbols'] 

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

663 

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

665 

666 # Find some required properties in the parsed results. 

667 # Raise ParseError if they are not present 

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

669 try: 

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

671 except KeyError: 

672 raise ParseError( 

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

674 prop)) 

675 atoms = Atoms(**atoms_kwargs) 

676 

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

678 calc = SinglePointDFTCalculator(atoms, **results) 

679 if kpts is not None: 

680 calc.kpts = kpts 

681 calc.name = 'vasp' 

682 atoms.calc = calc 

683 return atoms 

684 

685 

686class OutcarHeaderParser(HeaderParser): 

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

688 

689 def __init__(self, 

690 parsers: Sequence[VaspHeaderPropertyParser] = None, 

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

692 global default_header_parsers 

693 parsers = parsers or default_header_parsers.make_parsers() 

694 super().__init__(parsers) 

695 self.workdir = workdir 

696 

697 @property 

698 def workdir(self): 

699 return self._workdir 

700 

701 @workdir.setter 

702 def workdir(self, value): 

703 if value is not None: 

704 value = Path(value) 

705 self._workdir = value 

706 

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

708 if 'symbols' in results: 

709 # Safeguard, in case a different parser already 

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

711 return results.pop('symbols') 

712 

713 # Build the symbols of the atoms 

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

715 if required_key not in results: 

716 raise ParseError( 

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

718 format(required_key)) 

719 

720 ion_types = results.pop('ion_types') 

721 species = results.pop('species') 

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

723 raise ParseError( 

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

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

726 len(ion_types), len(species))) 

727 

728 # Expand the symbols list 

729 symbols = [] 

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

731 symbols.extend(n * [sym]) 

732 return symbols 

733 

734 def _get_constraint(self): 

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

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

737 OUTCAR parser which does this. 

738 """ 

739 constraint = None 

740 if self.workdir is not None: 

741 constraint = read_constraints_from_file(self.workdir) 

742 return constraint 

743 

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

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

746 results = self.parse(lines) 

747 

748 # Get the symbols from the parsed results 

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

750 symbols = self._build_symbols(results) 

751 natoms = len(symbols) 

752 

753 constraint = self._get_constraint() 

754 

755 # Remaining results from the parse goes into the header 

756 header = dict(symbols=symbols, 

757 natoms=natoms, 

758 constraint=constraint, 

759 **results) 

760 return header 

761 

762 

763class OUTCARChunk(ImageChunk): 

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

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

766 """ 

767 

768 def __init__(self, 

769 lines: _CHUNK, 

770 header: _HEADER, 

771 parser: ChunkParser = None): 

772 super().__init__() 

773 self.lines = lines 

774 self.header = header 

775 self.parser = parser or OutcarChunkParser() 

776 

777 def build(self): 

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

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

780 

781 

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

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

784 lines = [] 

785 for line in fd: 

786 lines.append(line) 

787 if 'Iteration' in line: 

788 # Start of SCF cycle 

789 return lines 

790 

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

792 raise ParseError('Incomplete OUTCAR') 

793 

794 

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

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

797 lines = [] 

798 while True: 

799 line = next(fd) 

800 lines.append(line) 

801 if _OUTCAR_SCF_DELIM in line: 

802 # Add 4 more lines to include energy 

803 for _ in range(4): 

804 lines.append(next(fd)) 

805 break 

806 return lines 

807 

808 

809def outcarchunks(fd: TextIO, 

810 chunk_parser: ChunkParser = None, 

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

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

813 name = Path(fd.name) 

814 workdir = name.parent 

815 

816 # First we get header info 

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

818 header_parser = header_parser or OutcarHeaderParser(workdir=workdir) 

819 

820 lines = build_header(fd) 

821 header = header_parser.build(lines) 

822 assert isinstance(header, dict) 

823 

824 chunk_parser = chunk_parser or OutcarChunkParser() 

825 

826 while True: 

827 try: 

828 lines = build_chunk(fd) 

829 except StopIteration: 

830 # End of file 

831 return 

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

833 

834 

835# Create the default chunk parsers 

836default_chunk_parsers = DefaultParsersContainer( 

837 Cell, 

838 PositionsAndForces, 

839 Stress, 

840 Magmoms, 

841 Magmom, 

842 EFermi, 

843 Kpoints, 

844 Energy, 

845) 

846 

847# Create the default header parsers 

848default_header_parsers = DefaultParsersContainer( 

849 SpeciesTypes, 

850 IonsPerSpecies, 

851 Spinpol, 

852 KpointHeader, 

853)