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

462 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2024-08-05 14:37 +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 magmom = np.array(list(map(float, magmom_lst))) 

400 else: 

401 magmom = float(magmom_lst[0]) 

402 return {'magmom': magmom} 

403 

404 

405class Magmoms(VaspChunkPropertyParser): 

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

407 line = lines[cursor] 

408 if 'magnetization (x)' in line: 

409 natoms = self.get_from_header('natoms') 

410 self.non_collinear = False 

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

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

413 if 'magnetization (y)' in line_y: 

414 self.non_collinear = True 

415 return True 

416 return False 

417 

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

419 

420 natoms = self.get_from_header('natoms') 

421 if self.non_collinear: 

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

423 nskip = 4 # Skip some lines 

424 for i in range(natoms): 

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

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

427 nskip = natoms + 13 # Skip some lines 

428 for i in range(natoms): 

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

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

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

432 for i in range(natoms): 

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

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

435 else: 

436 magmoms = np.zeros(natoms) 

437 nskip = 4 # Skip some lines 

438 for i in range(natoms): 

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

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

441 

442 return {'magmoms': magmoms} 

443 

444 

445class EFermi(SimpleVaspChunkParser): 

446 LINE_DELIMITER = 'E-fermi :' 

447 

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

449 line = self.get_line(cursor, lines) 

450 parts = line.split() 

451 efermi = float(parts[2]) 

452 return {'efermi': efermi} 

453 

454 

455class Energy(SimpleVaspChunkParser): 

456 LINE_DELIMITER = _OUTCAR_SCF_DELIM 

457 

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

459 nskip = 2 

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

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

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

463 

464 nskip = 4 

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

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

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

468 

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

470 

471 

472class Kpoints(VaspChunkPropertyParser): 

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

474 line = lines[cursor] 

475 # Example line: 

476 # " spin component 1" or " spin component 2" 

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

478 # as well 

479 if 'spin component 1' in line: 

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

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

482 if len(parts) == 3: 

483 try: 

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

485 # spin-up or spin-down 

486 int(parts[-1]) 

487 except ValueError: 

488 pass 

489 else: 

490 return True 

491 return False 

492 

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

494 nkpts = self.get_from_header('nkpts') 

495 nbands = self.get_from_header('nbands') 

496 weights = self.get_from_header('kpt_weights') 

497 spinpol = self.get_from_header('spinpol') 

498 nspins = 2 if spinpol else 1 

499 

500 kpts = [] 

501 for spin in range(nspins): 

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

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

504 # component line 

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

506 

507 cursor += 2 # Skip two lines 

508 for _ in range(nkpts): 

509 # Skip empty lines 

510 cursor = find_next_non_empty_line(cursor, lines) 

511 

512 line = self.get_line(cursor, lines) 

513 # Example line: 

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

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

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

517 weight = weights[ikpt] 

518 

519 cursor += 2 # Move down two 

520 eigenvalues = np.zeros(nbands) 

521 occupations = np.zeros(nbands) 

522 for n in range(nbands): 

523 # Example line: 

524 # " 1 -9.9948 1.00000" 

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

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

527 occupations[n] = f_n 

528 eigenvalues[n] = eps_n 

529 cursor += 1 

530 kpt = SinglePointKPoint(weight, 

531 spin, 

532 ikpt, 

533 eps_n=eigenvalues, 

534 f_n=occupations) 

535 kpts.append(kpt) 

536 

537 return {'kpts': kpts} 

538 

539 

540class DefaultParsersContainer: 

541 """Container for the default OUTCAR parsers. 

542 Allows for modification of the global default parsers. 

543 

544 Takes in an arbitrary number of parsers. 

545 The parsers should be uninitialized, 

546 as they are created on request. 

547 """ 

548 

549 def __init__(self, *parsers_cls): 

550 self._parsers_dct = {} 

551 for parser in parsers_cls: 

552 self.add_parser(parser) 

553 

554 @property 

555 def parsers_dct(self) -> dict: 

556 return self._parsers_dct 

557 

558 def make_parsers(self): 

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

560 Parsers are created upon request.""" 

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

562 

563 def remove_parser(self, name: str): 

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

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

566 self.parsers_dct.pop(name) 

567 

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

569 """Add a parser""" 

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

571 

572 

573class TypeParser(ABC): 

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

575 by applying the internal attached parsers""" 

576 

577 def __init__(self, parsers): 

578 self.parsers = parsers 

579 

580 @property 

581 def parsers(self): 

582 return self._parsers 

583 

584 @parsers.setter 

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

586 self._check_parsers(new_parsers) 

587 self._parsers = new_parsers 

588 

589 @abstractmethod 

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

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

592 

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

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

595 properties = {} 

596 for cursor, _ in enumerate(lines): 

597 for parser in self.parsers: 

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

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

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

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

602 # certain pieces of information to be written multiple 

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

604 # final values within a given chunk. 

605 if parser.has_property(cursor, lines): 

606 prop = parser.parse(cursor, lines) 

607 properties.update(prop) 

608 return properties 

609 

610 

611class ChunkParser(TypeParser, ABC): 

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

613 super().__init__(parsers) 

614 self.header = header 

615 

616 @property 

617 def header(self) -> _HEADER: 

618 return self._header 

619 

620 @header.setter 

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

622 self._header = value or {} 

623 self.update_parser_headers() 

624 

625 def update_parser_headers(self) -> None: 

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

627 for parser in self.parsers: 

628 parser.header = self.header 

629 

630 def _check_parsers(self, 

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

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

633 if not all( 

634 isinstance(parser, VaspChunkPropertyParser) 

635 for parser in parsers): 

636 raise TypeError( 

637 'All parsers must be of type VaspChunkPropertyParser') 

638 

639 @abstractmethod 

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

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

642 

643 

644class HeaderParser(TypeParser, ABC): 

645 def _check_parsers(self, 

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

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

648 if not all( 

649 isinstance(parser, VaspHeaderPropertyParser) 

650 for parser in parsers): 

651 raise TypeError( 

652 'All parsers must be of type VaspHeaderPropertyParser') 

653 

654 @abstractmethod 

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

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

657 

658 

659class OutcarChunkParser(ChunkParser): 

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

661 

662 def __init__(self, 

663 header: _HEADER = None, 

664 parsers: Sequence[VaspChunkPropertyParser] = None): 

665 global default_chunk_parsers 

666 parsers = parsers or default_chunk_parsers.make_parsers() 

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

668 

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

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

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

672 

673 results = self.parse(lines) 

674 symbols = self.header['symbols'] 

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

676 

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

678 

679 # Find some required properties in the parsed results. 

680 # Raise ParseError if they are not present 

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

682 try: 

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

684 except KeyError: 

685 raise ParseError( 

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

687 prop)) 

688 atoms = Atoms(**atoms_kwargs) 

689 

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

691 calc = SinglePointDFTCalculator(atoms, **results) 

692 if kpts is not None: 

693 calc.kpts = kpts 

694 calc.name = 'vasp' 

695 atoms.calc = calc 

696 return atoms 

697 

698 

699class OutcarHeaderParser(HeaderParser): 

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

701 

702 def __init__(self, 

703 parsers: Sequence[VaspHeaderPropertyParser] = None, 

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

705 global default_header_parsers 

706 parsers = parsers or default_header_parsers.make_parsers() 

707 super().__init__(parsers) 

708 self.workdir = workdir 

709 

710 @property 

711 def workdir(self): 

712 return self._workdir 

713 

714 @workdir.setter 

715 def workdir(self, value): 

716 if value is not None: 

717 value = Path(value) 

718 self._workdir = value 

719 

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

721 if 'symbols' in results: 

722 # Safeguard, in case a different parser already 

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

724 return results.pop('symbols') 

725 

726 # Build the symbols of the atoms 

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

728 if required_key not in results: 

729 raise ParseError( 

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

731 format(required_key)) 

732 

733 ion_types = results.pop('ion_types') 

734 species = results.pop('species') 

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

736 raise ParseError( 

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

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

739 len(ion_types), len(species))) 

740 

741 # Expand the symbols list 

742 symbols = [] 

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

744 symbols.extend(n * [sym]) 

745 return symbols 

746 

747 def _get_constraint(self): 

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

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

750 OUTCAR parser which does this. 

751 """ 

752 constraint = None 

753 if self.workdir is not None: 

754 constraint = read_constraints_from_file(self.workdir) 

755 return constraint 

756 

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

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

759 results = self.parse(lines) 

760 

761 # Get the symbols from the parsed results 

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

763 symbols = self._build_symbols(results) 

764 natoms = len(symbols) 

765 

766 constraint = self._get_constraint() 

767 

768 # Remaining results from the parse goes into the header 

769 header = dict(symbols=symbols, 

770 natoms=natoms, 

771 constraint=constraint, 

772 **results) 

773 return header 

774 

775 

776class OUTCARChunk(ImageChunk): 

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

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

779 """ 

780 

781 def __init__(self, 

782 lines: _CHUNK, 

783 header: _HEADER, 

784 parser: ChunkParser = None): 

785 super().__init__() 

786 self.lines = lines 

787 self.header = header 

788 self.parser = parser or OutcarChunkParser() 

789 

790 def build(self): 

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

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

793 

794 

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

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

797 lines = [] 

798 for line in fd: 

799 lines.append(line) 

800 if 'Iteration' in line: 

801 # Start of SCF cycle 

802 return lines 

803 

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

805 raise ParseError('Incomplete OUTCAR') 

806 

807 

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

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

810 lines = [] 

811 while True: 

812 line = next(fd) 

813 lines.append(line) 

814 if _OUTCAR_SCF_DELIM in line: 

815 # Add 4 more lines to include energy 

816 for _ in range(4): 

817 lines.append(next(fd)) 

818 break 

819 return lines 

820 

821 

822def outcarchunks(fd: TextIO, 

823 chunk_parser: ChunkParser = None, 

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

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

826 name = Path(fd.name) 

827 workdir = name.parent 

828 

829 # First we get header info 

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

831 header_parser = header_parser or OutcarHeaderParser(workdir=workdir) 

832 

833 lines = build_header(fd) 

834 header = header_parser.build(lines) 

835 assert isinstance(header, dict) 

836 

837 chunk_parser = chunk_parser or OutcarChunkParser() 

838 

839 while True: 

840 try: 

841 lines = build_chunk(fd) 

842 except StopIteration: 

843 # End of file 

844 return 

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

846 

847 

848# Create the default chunk parsers 

849default_chunk_parsers = DefaultParsersContainer( 

850 Cell, 

851 PositionsAndForces, 

852 Stress, 

853 Magmoms, 

854 Magmom, 

855 EFermi, 

856 Kpoints, 

857 Energy, 

858) 

859 

860# Create the default header parsers 

861default_header_parsers = DefaultParsersContainer( 

862 SpeciesTypes, 

863 IonsPerSpecies, 

864 Spinpol, 

865 KpointHeader, 

866)