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

438 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-12-10 11:04 +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, format='vasp').constraints 

99 break 

100 return constraint 

101 

102 

103class VaspPropertyParser(ABC): 

104 NAME = None # type: str 

105 

106 @classmethod 

107 def get_name(cls): 

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

109 specify a custom name, 

110 otherwise the class name is used""" 

111 return cls.NAME or cls.__name__ 

112 

113 @abstractmethod 

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

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

116 cursor position""" 

117 

118 @staticmethod 

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

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

121 return _check_line(lines[cursor]) 

122 

123 @abstractmethod 

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

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

126 Assumes that "has_property" would evaluate to True 

127 from cursor position """ 

128 

129 

130class SimpleProperty(VaspPropertyParser, ABC): 

131 LINE_DELIMITER = None # type: str 

132 

133 def __init__(self): 

134 super().__init__() 

135 if self.LINE_DELIMITER is None: 

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

137 

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

139 line = lines[cursor] 

140 return self.LINE_DELIMITER in line 

141 

142 

143class VaspChunkPropertyParser(VaspPropertyParser, ABC): 

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

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

146 

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

148 super().__init__() 

149 header = header or {} 

150 self.header = header 

151 

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

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

154 if that key doesn't exist""" 

155 try: 

156 return self.header[key] 

157 except KeyError: 

158 raise ParseError( 

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

160 key)) 

161 

162 

163class VaspHeaderPropertyParser(VaspPropertyParser, ABC): 

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

165 

166 

167class SimpleVaspChunkParser(VaspChunkPropertyParser, SimpleProperty, ABC): 

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

169 determined to exist from 1 line""" 

170 

171 

172class SimpleVaspHeaderParser(VaspHeaderPropertyParser, SimpleProperty, ABC): 

173 """Class for properties in the header 

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

175 

176 

177class Spinpol(SimpleVaspHeaderParser): 

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

179 

180 Example line: 

181 " ISPIN = 2 spin polarized calculation?" 

182 

183 """ 

184 LINE_DELIMITER = 'ISPIN' 

185 

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

187 line = lines[cursor].strip() 

188 parts = line.split() 

189 ispin = int(parts[2]) 

190 # ISPIN 2 = spinpolarized, otherwise no 

191 # ISPIN 1 = non-spinpolarized 

192 spinpol = ispin == 2 

193 return {'spinpol': spinpol} 

194 

195 

196class SpeciesTypes(SimpleVaspHeaderParser): 

197 """Parse species types. 

198 

199 Example line: 

200 " POTCAR: PAW_PBE Ni 02Aug2007" 

201 

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

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

204 """ 

205 LINE_DELIMITER = 'POTCAR:' 

206 

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

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

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

210 # as we only want to parse every second, 

211 # due to repeated entries in the OUTCAR 

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

213 

214 @property 

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

216 """Internal storage of each found line. 

217 Will contain the double counting. 

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

219 return self._species 

220 

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

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

223 This method only returns the first half, 

224 effectively removing the double counting. 

225 """ 

226 # Get the index of the first half 

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

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

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

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

231 # len(self.species) // 2 

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

233 # Make a copy 

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

235 

236 def _make_returnval(self) -> _RESULT: 

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

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

239 

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

241 line = lines[cursor].strip() 

242 

243 parts = line.split() 

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

245 if '1/r potential' in line: 

246 # This denotes an AE potential 

247 # Currently only H_AE 

248 # " H 1/r potential " 

249 idx = 1 

250 else: 

251 # Regular PAW potential, e.g. 

252 # "PAW_PBE H1.25 07Sep2000" or 

253 # "PAW_PBE Fe_pv 02Aug2007" 

254 idx = 2 

255 

256 sym = parts[idx] 

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

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

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

260 # remove any non-alphabetic characters 

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

262 

263 if sym not in atomic_numbers: 

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

265 # an element 

266 raise ParseError( 

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

268 

269 self.species.append(sym) 

270 

271 return self._make_returnval() 

272 

273 

274class IonsPerSpecies(SimpleVaspHeaderParser): 

275 """Example line: 

276 

277 " ions per type = 32 31 2" 

278 """ 

279 LINE_DELIMITER = 'ions per type' 

280 

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

282 line = lines[cursor].strip() 

283 parts = line.split() 

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

285 return {'ion_types': ion_types} 

286 

287 

288class KpointHeader(VaspHeaderPropertyParser): 

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

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

291 

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

293 line = lines[cursor] 

294 return "NKPTS" in line and "NBANDS" in line 

295 

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

297 line = lines[cursor].strip() 

298 parts = line.split() 

299 nkpts = int(parts[3]) 

300 nbands = int(parts[-1]) 

301 

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

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

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

305 # for parsing that 

306 # Move cursor down to next delimiter 

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

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

309 line = line.strip() 

310 if delim2 in line: 

311 # build k-points 

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

313 kpt_weights = np.zeros(nkpts) 

314 for nk in range(nkpts): 

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

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

317 parts = line.split() 

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

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

320 results['ibzkpts'] = ibzkpts 

321 results['kpt_weights'] = kpt_weights 

322 break 

323 else: 

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

325 

326 return results 

327 

328 

329class Stress(SimpleVaspChunkParser): 

330 """Process the stress from an OUTCAR""" 

331 LINE_DELIMITER = 'in kB ' 

332 

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

334 line = self.get_line(cursor, lines) 

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

336 try: 

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

338 except ValueError: 

339 # Vasp FORTRAN string formatting issues, can happen with 

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

341 # as a ParseError? 

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

343 else: 

344 result = convert_vasp_outcar_stress(stress) 

345 return {'stress': result} 

346 

347 

348class Cell(SimpleVaspChunkParser): 

349 LINE_DELIMITER = 'direct lattice vectors' 

350 

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

352 nskip = 1 

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

354 for i in range(3): 

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

356 parts = line.split() 

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

358 return {'cell': cell} 

359 

360 

361class PositionsAndForces(SimpleVaspChunkParser): 

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

363 We parse both simultaneously""" 

364 LINE_DELIMITER = 'POSITION ' 

365 

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

367 nskip = 2 

368 natoms = self.get_from_header('natoms') 

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

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

371 

372 for i in range(natoms): 

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

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

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

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

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

378 

379 

380class Magmom(VaspChunkPropertyParser): 

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

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

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

384 line = lines[cursor] 

385 if 'number of electron' in line: 

386 parts = line.split() 

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

388 return True 

389 return False 

390 

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

392 line = self.get_line(cursor, lines) 

393 parts = line.split() 

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

395 magmom_lst = parts[idx:] 

396 if len(magmom_lst) != 1: 

397 warn( 

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

399 'Setting magmom to x value.') 

400 magmom = float(magmom_lst[0]) 

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

402 # Remember to check that format fits! 

403 # else: 

404 # # Non-collinear spin 

405 # # Make a (3,) dim array 

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

407 return {'magmom': magmom} 

408 

409 

410class Magmoms(SimpleVaspChunkParser): 

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

412 This is just the magmoms in the collinear case. 

413 

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

415 LINE_DELIMITER = 'magnetization (x)' 

416 

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

418 # Magnetization for collinear 

419 natoms = self.get_from_header('natoms') 

420 nskip = 4 # Skip some lines 

421 magmoms = np.zeros(natoms) 

422 for i in range(natoms): 

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

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

425 # Once we support non-collinear spin, 

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

427 return {'magmoms': magmoms} 

428 

429 

430class EFermi(SimpleVaspChunkParser): 

431 LINE_DELIMITER = 'E-fermi :' 

432 

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

434 line = self.get_line(cursor, lines) 

435 parts = line.split() 

436 efermi = float(parts[2]) 

437 return {'efermi': efermi} 

438 

439 

440class Energy(SimpleVaspChunkParser): 

441 LINE_DELIMITER = _OUTCAR_SCF_DELIM 

442 

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

444 nskip = 2 

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

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

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

448 

449 nskip = 4 

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

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

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

453 

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

455 

456 

457class Kpoints(VaspChunkPropertyParser): 

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

459 line = lines[cursor] 

460 # Example line: 

461 # " spin component 1" or " spin component 2" 

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

463 # as well 

464 if 'spin component 1' in line: 

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

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

467 if len(parts) == 3: 

468 try: 

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

470 # spin-up or spin-down 

471 int(parts[-1]) 

472 except ValueError: 

473 pass 

474 else: 

475 return True 

476 return False 

477 

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

479 nkpts = self.get_from_header('nkpts') 

480 nbands = self.get_from_header('nbands') 

481 weights = self.get_from_header('kpt_weights') 

482 spinpol = self.get_from_header('spinpol') 

483 nspins = 2 if spinpol else 1 

484 

485 kpts = [] 

486 for spin in range(nspins): 

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

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

489 # component line 

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

491 

492 cursor += 2 # Skip two lines 

493 for _ in range(nkpts): 

494 # Skip empty lines 

495 cursor = find_next_non_empty_line(cursor, lines) 

496 

497 line = self.get_line(cursor, lines) 

498 # Example line: 

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

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

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

502 weight = weights[ikpt] 

503 

504 cursor += 2 # Move down two 

505 eigenvalues = np.zeros(nbands) 

506 occupations = np.zeros(nbands) 

507 for n in range(nbands): 

508 # Example line: 

509 # " 1 -9.9948 1.00000" 

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

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

512 occupations[n] = f_n 

513 eigenvalues[n] = eps_n 

514 cursor += 1 

515 kpt = SinglePointKPoint(weight, 

516 spin, 

517 ikpt, 

518 eps_n=eigenvalues, 

519 f_n=occupations) 

520 kpts.append(kpt) 

521 

522 return {'kpts': kpts} 

523 

524 

525class DefaultParsersContainer: 

526 """Container for the default OUTCAR parsers. 

527 Allows for modification of the global default parsers. 

528 

529 Takes in an arbitrary number of parsers. 

530 The parsers should be uninitialized, 

531 as they are created on request. 

532 """ 

533 

534 def __init__(self, *parsers_cls): 

535 self._parsers_dct = {} 

536 for parser in parsers_cls: 

537 self.add_parser(parser) 

538 

539 @property 

540 def parsers_dct(self) -> dict: 

541 return self._parsers_dct 

542 

543 def make_parsers(self): 

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

545 Parsers are created upon request.""" 

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

547 

548 def remove_parser(self, name: str): 

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

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

551 self.parsers_dct.pop(name) 

552 

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

554 """Add a parser""" 

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

556 

557 

558class TypeParser(ABC): 

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

560 by applying the internal attached parsers""" 

561 

562 def __init__(self, parsers): 

563 self.parsers = parsers 

564 

565 @property 

566 def parsers(self): 

567 return self._parsers 

568 

569 @parsers.setter 

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

571 self._check_parsers(new_parsers) 

572 self._parsers = new_parsers 

573 

574 @abstractmethod 

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

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

577 

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

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

580 properties = {} 

581 for cursor, _ in enumerate(lines): 

582 for parser in self.parsers: 

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

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

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

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

587 # certain pieces of information to be written multiple 

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

589 # final values within a given chunk. 

590 if parser.has_property(cursor, lines): 

591 prop = parser.parse(cursor, lines) 

592 properties.update(prop) 

593 return properties 

594 

595 

596class ChunkParser(TypeParser, ABC): 

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

598 super().__init__(parsers) 

599 self.header = header 

600 

601 @property 

602 def header(self) -> _HEADER: 

603 return self._header 

604 

605 @header.setter 

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

607 self._header = value or {} 

608 self.update_parser_headers() 

609 

610 def update_parser_headers(self) -> None: 

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

612 for parser in self.parsers: 

613 parser.header = self.header 

614 

615 def _check_parsers(self, 

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

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

618 if not all( 

619 isinstance(parser, VaspChunkPropertyParser) 

620 for parser in parsers): 

621 raise TypeError( 

622 'All parsers must be of type VaspChunkPropertyParser') 

623 

624 @abstractmethod 

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

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

627 

628 

629class HeaderParser(TypeParser, ABC): 

630 def _check_parsers(self, 

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

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

633 if not all( 

634 isinstance(parser, VaspHeaderPropertyParser) 

635 for parser in parsers): 

636 raise TypeError( 

637 'All parsers must be of type VaspHeaderPropertyParser') 

638 

639 @abstractmethod 

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

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

642 

643 

644class OutcarChunkParser(ChunkParser): 

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

646 

647 def __init__(self, 

648 header: _HEADER = None, 

649 parsers: Sequence[VaspChunkPropertyParser] = None): 

650 global default_chunk_parsers 

651 parsers = parsers or default_chunk_parsers.make_parsers() 

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

653 

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

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

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

657 

658 results = self.parse(lines) 

659 symbols = self.header['symbols'] 

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

661 

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

663 

664 # Find some required properties in the parsed results. 

665 # Raise ParseError if they are not present 

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

667 try: 

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

669 except KeyError: 

670 raise ParseError( 

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

672 prop)) 

673 atoms = Atoms(**atoms_kwargs) 

674 

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

676 calc = SinglePointDFTCalculator(atoms, **results) 

677 if kpts is not None: 

678 calc.kpts = kpts 

679 calc.name = 'vasp' 

680 atoms.calc = calc 

681 return atoms 

682 

683 

684class OutcarHeaderParser(HeaderParser): 

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

686 

687 def __init__(self, 

688 parsers: Sequence[VaspHeaderPropertyParser] = None, 

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

690 global default_header_parsers 

691 parsers = parsers or default_header_parsers.make_parsers() 

692 super().__init__(parsers) 

693 self.workdir = workdir 

694 

695 @property 

696 def workdir(self): 

697 return self._workdir 

698 

699 @workdir.setter 

700 def workdir(self, value): 

701 if value is not None: 

702 value = Path(value) 

703 self._workdir = value 

704 

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

706 if 'symbols' in results: 

707 # Safeguard, in case a different parser already 

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

709 return results.pop('symbols') 

710 

711 # Build the symbols of the atoms 

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

713 if required_key not in results: 

714 raise ParseError( 

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

716 format(required_key)) 

717 

718 ion_types = results.pop('ion_types') 

719 species = results.pop('species') 

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

721 raise ParseError( 

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

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

724 len(ion_types), len(species))) 

725 

726 # Expand the symbols list 

727 symbols = [] 

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

729 symbols.extend(n * [sym]) 

730 return symbols 

731 

732 def _get_constraint(self): 

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

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

735 OUTCAR parser which does this. 

736 """ 

737 constraint = None 

738 if self.workdir is not None: 

739 constraint = read_constraints_from_file(self.workdir) 

740 return constraint 

741 

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

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

744 results = self.parse(lines) 

745 

746 # Get the symbols from the parsed results 

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

748 symbols = self._build_symbols(results) 

749 natoms = len(symbols) 

750 

751 constraint = self._get_constraint() 

752 

753 # Remaining results from the parse goes into the header 

754 header = dict(symbols=symbols, 

755 natoms=natoms, 

756 constraint=constraint, 

757 **results) 

758 return header 

759 

760 

761class OUTCARChunk(ImageChunk): 

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

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

764 """ 

765 

766 def __init__(self, 

767 lines: _CHUNK, 

768 header: _HEADER, 

769 parser: ChunkParser = None): 

770 super().__init__() 

771 self.lines = lines 

772 self.header = header 

773 self.parser = parser or OutcarChunkParser() 

774 

775 def build(self): 

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

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

778 

779 

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

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

782 lines = [] 

783 for line in fd: 

784 lines.append(line) 

785 if 'Iteration' in line: 

786 # Start of SCF cycle 

787 return lines 

788 

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

790 raise ParseError('Incomplete OUTCAR') 

791 

792 

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

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

795 lines = [] 

796 while True: 

797 line = next(fd) 

798 lines.append(line) 

799 if _OUTCAR_SCF_DELIM in line: 

800 # Add 4 more lines to include energy 

801 for _ in range(4): 

802 lines.append(next(fd)) 

803 break 

804 return lines 

805 

806 

807def outcarchunks(fd: TextIO, 

808 chunk_parser: ChunkParser = None, 

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

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

811 name = Path(fd.name) 

812 workdir = name.parent 

813 

814 # First we get header info 

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

816 header_parser = header_parser or OutcarHeaderParser(workdir=workdir) 

817 

818 lines = build_header(fd) 

819 header = header_parser.build(lines) 

820 assert isinstance(header, dict) 

821 

822 chunk_parser = chunk_parser or OutcarChunkParser() 

823 

824 while True: 

825 try: 

826 lines = build_chunk(fd) 

827 except StopIteration: 

828 # End of file 

829 return 

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

831 

832 

833# Create the default chunk parsers 

834default_chunk_parsers = DefaultParsersContainer( 

835 Cell, 

836 PositionsAndForces, 

837 Stress, 

838 Magmoms, 

839 Magmom, 

840 EFermi, 

841 Kpoints, 

842 Energy, 

843) 

844 

845# Create the default header parsers 

846default_header_parsers = DefaultParsersContainer( 

847 SpeciesTypes, 

848 IonsPerSpecies, 

849 Spinpol, 

850 KpointHeader, 

851)