Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2Module for parsing OUTCAR files. 

3""" 

4from abc import ABC, abstractmethod 

5from typing import (Dict, Any, Sequence, TextIO, Iterator, Optional, Union, 

6 List) 

7import re 

8from warnings import warn 

9from pathlib import Path, PurePath 

10 

11import numpy as np 

12import ase 

13from ase import Atoms 

14from ase.data import atomic_numbers 

15from ase.io import ParseError, read 

16from ase.io.utils import ImageChunk 

17from ase.calculators.singlepoint import SinglePointDFTCalculator, SinglePointKPoint 

18 

19# Denotes end of Ionic step for OUTCAR reading 

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

21 

22# Some type aliases 

23_HEADER = Dict[str, Any] 

24_CURSOR = int 

25_CHUNK = Sequence[str] 

26_RESULT = Dict[str, Any] 

27 

28 

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

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

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

32 Only call in cases we need the numeric values 

33 """ 

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

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

36 return line 

37 

38 

39def convert_vasp_outcar_stress(stress: Sequence): 

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

41 expected units in ASE """ 

42 stress_arr = -np.array(stress) 

43 shape = stress_arr.shape 

44 if shape != (6, ): 

45 raise ValueError( 

46 'Stress has the wrong shape. Expected (6,), got {}'.format(shape)) 

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

48 return stress_arr 

49 

50 

51def read_constraints_from_file(directory): 

52 directory = Path(directory) 

53 constraint = None 

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

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

56 constraint = read(directory / filename, format='vasp').constraints 

57 break 

58 return constraint 

59 

60 

61class VaspPropertyParser(ABC): 

62 NAME = None # type: str 

63 

64 @classmethod 

65 def get_name(cls): 

66 """Name of parser. Override the NAME constant in the class to specify a custom name, 

67 otherwise the class name is used""" 

68 return cls.NAME or cls.__name__ 

69 

70 @abstractmethod 

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

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

73 cursor position""" 

74 

75 @staticmethod 

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

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

78 return _check_line(lines[cursor]) 

79 

80 @abstractmethod 

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

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

83 Assumes that "has_property" would evaluate to True 

84 from cursor position """ 

85 

86 

87class SimpleProperty(VaspPropertyParser, ABC): 

88 LINE_DELIMITER = None # type: str 

89 

90 def __init__(self): 

91 super().__init__() 

92 if self.LINE_DELIMITER is None: 

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

94 

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

96 line = lines[cursor] 

97 return self.LINE_DELIMITER in line 

98 

99 

100class VaspChunkPropertyParser(VaspPropertyParser, ABC): 

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

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

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

104 super().__init__() 

105 header = header or {} 

106 self.header = header 

107 

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

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

110 if that key doesn't exist""" 

111 try: 

112 return self.header[key] 

113 except KeyError: 

114 raise ParseError( 

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

116 key)) 

117 

118 

119class VaspHeaderPropertyParser(VaspPropertyParser, ABC): 

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

121 

122 

123class SimpleVaspChunkParser(VaspChunkPropertyParser, SimpleProperty, ABC): 

124 """Class for properties in a chunk can be determined to exist from 1 line""" 

125 

126 

127class SimpleVaspHeaderParser(VaspHeaderPropertyParser, SimpleProperty, ABC): 

128 """Class for properties in the header which can be determined to exist from 1 line""" 

129 

130 

131class Spinpol(SimpleVaspHeaderParser): 

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

133  

134 Example line: 

135 " ISPIN = 2 spin polarized calculation?" 

136 

137 """ 

138 LINE_DELIMITER = 'ISPIN' 

139 

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

141 line = lines[cursor].strip() 

142 parts = line.split() 

143 ispin = int(parts[2]) 

144 # ISPIN 2 = spinpolarized, otherwise no 

145 # ISPIN 1 = non-spinpolarized 

146 spinpol = ispin == 2 

147 return {'spinpol': spinpol} 

148 

149 

150class SpeciesTypes(SimpleVaspHeaderParser): 

151 """Parse species types. 

152 

153 Example line: 

154 " POTCAR: PAW_PBE Ni 02Aug2007" 

155 

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

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

158 """ 

159 LINE_DELIMITER = 'POTCAR:' 

160 

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

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

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

164 # as we only want to parse every second, 

165 # due to repeated entries in the OUTCAR 

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

167 

168 @property 

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

170 """Internal storage of each found line. 

171 Will contain the double counting. 

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

173 return self._species 

174 

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

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

177 This method only returns the first half, 

178 effectively removing the double counting. 

179 """ 

180 # Get the index of the first half 

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

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

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

184 # For even length lists, this is just equivalent to idx = len(self.species) // 2 

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

186 # Make a copy 

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

188 

189 def _make_returnval(self) -> _RESULT: 

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

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

192 

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

194 line = lines[cursor].strip() 

195 

196 parts = line.split() 

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

198 if '1/r potential' in line: 

199 # This denotes an AE potential 

200 # Currently only H_AE 

201 # " H 1/r potential " 

202 idx = 1 

203 else: 

204 # Regular PAW potential, e.g. 

205 # "PAW_PBE H1.25 07Sep2000" or 

206 # "PAW_PBE Fe_pv 02Aug2007" 

207 idx = 2 

208 

209 sym = parts[idx] 

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

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

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

213 # remove any non-alphabetic characters 

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

215 

216 if sym not in atomic_numbers: 

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

218 # an element 

219 raise ParseError( 

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

221 

222 self.species.append(sym) 

223 

224 return self._make_returnval() 

225 

226 

227class IonsPerSpecies(SimpleVaspHeaderParser): 

228 """Example line: 

229 

230 " ions per type = 32 31 2" 

231 """ 

232 LINE_DELIMITER = 'ions per type' 

233 

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

235 line = lines[cursor].strip() 

236 parts = line.split() 

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

238 return {'ion_types': ion_types} 

239 

240 

241class KpointHeader(SimpleVaspHeaderParser): 

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

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

244 LINE_DELIMITER = 'NKPTS' 

245 

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

247 line = lines[cursor].strip() 

248 parts = line.split() 

249 nkpts = int(parts[3]) 

250 nbands = int(parts[-1]) 

251 

252 results = {'nkpts': nkpts, 'nbands': nbands} 

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

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

255 # for parsing that 

256 # Move cursor down to next delimiter 

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

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

259 line = line.strip() 

260 if delim2 in line: 

261 # build k-points 

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

263 kpt_weights = np.zeros(nkpts) 

264 for nk in range(nkpts): 

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

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

267 parts = line.split() 

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

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

270 results['ibzkpts'] = ibzkpts 

271 results['kpt_weights'] = kpt_weights 

272 break 

273 else: 

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

275 

276 return results 

277 

278 

279class Stress(SimpleVaspChunkParser): 

280 """Process the stress from an OUTCAR""" 

281 LINE_DELIMITER = 'in kB ' 

282 

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

284 line = self.get_line(cursor, lines) 

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

286 try: 

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

288 except ValueError: 

289 # Vasp FORTRAN string formatting issues, can happen with some bad geometry steps 

290 # Alternatively, we can re-raise as a ParseError? 

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

292 else: 

293 result = convert_vasp_outcar_stress(stress) 

294 return {'stress': result} 

295 

296 

297class Cell(SimpleVaspChunkParser): 

298 LINE_DELIMITER = 'direct lattice vectors' 

299 

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

301 nskip = 1 

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

303 for i in range(3): 

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

305 parts = line.split() 

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

307 return {'cell': cell} 

308 

309 

310class PositionsAndForces(SimpleVaspChunkParser): 

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

312 We parse both simultaneously""" 

313 LINE_DELIMITER = 'POSITION ' 

314 

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

316 nskip = 2 

317 natoms = self.get_from_header('natoms') 

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

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

320 

321 for i in range(natoms): 

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

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

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

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

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

327 

328 

329class Magmom(VaspChunkPropertyParser): 

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

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

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

333 line = lines[cursor] 

334 if 'number of electron' in line: 

335 parts = line.split() 

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

337 return True 

338 return False 

339 

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

341 line = self.get_line(cursor, lines) 

342 parts = line.split() 

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

344 magmom_lst = parts[idx:] 

345 if len(magmom_lst) != 1: 

346 warn( 

347 'Non-collinear spin is not yet implemented. Setting magmom to x value.' 

348 ) 

349 magmom = float(magmom_lst[0]) 

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

351 # Remember to check that format fits! 

352 # else: 

353 # # Non-collinear spin 

354 # # Make a (3,) dim array 

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

356 return {'magmom': magmom} 

357 

358 

359class Magmoms(SimpleVaspChunkParser): 

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

361 This is just the magmoms in the collinear case. 

362  

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

364 LINE_DELIMITER = 'magnetization (x)' 

365 

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

367 # Magnetization for collinear 

368 natoms = self.get_from_header('natoms') 

369 nskip = 4 # Skip some lines 

370 magmoms = np.zeros(natoms) 

371 for i in range(natoms): 

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

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

374 # Once we support non-collinear spin, 

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

376 return {'magmoms': magmoms} 

377 

378 

379class EFermi(SimpleVaspChunkParser): 

380 LINE_DELIMITER = 'E-fermi :' 

381 

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

383 line = self.get_line(cursor, lines) 

384 parts = line.split() 

385 efermi = float(parts[2]) 

386 return {'efermi': efermi} 

387 

388 

389class Energy(SimpleVaspChunkParser): 

390 LINE_DELIMITER = _OUTCAR_SCF_DELIM 

391 

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

393 nskip = 2 

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

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

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

397 

398 nskip = 4 

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

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

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

402 

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

404 

405 

406class Kpoints(VaspChunkPropertyParser): 

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

408 line = lines[cursor] 

409 # Example line: 

410 # " spin component 1" or " spin component 2" 

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

412 if 'spin component 1' in line: 

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

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

415 if len(parts) == 3: 

416 try: 

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

418 # spin-up or spin-down 

419 int(parts[-1]) 

420 except ValueError: 

421 pass 

422 else: 

423 return True 

424 return False 

425 

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

427 nkpts = self.get_from_header('nkpts') 

428 nbands = self.get_from_header('nbands') 

429 weights = self.get_from_header('kpt_weights') 

430 spinpol = self.get_from_header('spinpol') 

431 nspins = 2 if spinpol else 1 

432 

433 kpts = [] 

434 for spin in range(nspins): 

435 # The cursor should be on a "spin componenet" line now 

436 assert 'spin component' in lines[cursor] 

437 cursor += 2 # Skip two lines 

438 for _ in range(nkpts): 

439 line = self.get_line(cursor, lines) 

440 # Example line: 

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

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

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

444 weight = weights[ikpt] 

445 

446 cursor += 2 # Move down two 

447 eigenvalues = np.zeros(nbands) 

448 occupations = np.zeros(nbands) 

449 for n in range(nbands): 

450 # Example line: 

451 # " 1 -9.9948 1.00000" 

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

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

454 occupations[n] = f_n 

455 eigenvalues[n] = eps_n 

456 cursor += 1 

457 kpt = SinglePointKPoint(weight, 

458 spin, 

459 ikpt, 

460 eps_n=eigenvalues, 

461 f_n=occupations) 

462 kpts.append(kpt) 

463 cursor += 1 # shift by 1 more at the end, prepare for next k-point 

464 return {'kpts': kpts} 

465 

466 

467class DefaultParsersContainer: 

468 """Container for the default OUTCAR parsers. 

469 Allows for modification of the global default parsers. 

470  

471 Takes in an arbitrary number of parsers. The parsers should be uninitialized, 

472 as they are created on request. 

473 """ 

474 def __init__(self, *parsers_cls): 

475 self._parsers_dct = {} 

476 for parser in parsers_cls: 

477 self.add_parser(parser) 

478 

479 @property 

480 def parsers_dct(self) -> dict: 

481 return self._parsers_dct 

482 

483 def make_parsers(self): 

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

485 Parsers are created upon request.""" 

486 return list(parser() for parser in self.parsers_dct.values()) 

487 

488 def remove_parser(self, name: str): 

489 """Remove a parser based on the name. The name must match the parser name exactly.""" 

490 self.parsers_dct.pop(name) 

491 

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

493 """Add a parser""" 

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

495 

496 

497class TypeParser(ABC): 

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

499 by applying the internal attached parsers""" 

500 def __init__(self, parsers): 

501 self.parsers = parsers 

502 

503 @property 

504 def parsers(self): 

505 return self._parsers 

506 

507 @parsers.setter 

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

509 self._check_parsers(new_parsers) 

510 self._parsers = new_parsers 

511 

512 @abstractmethod 

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

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

515 

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

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

518 properties = {} 

519 for cursor, _ in enumerate(lines): 

520 for parser in self.parsers: 

521 # Check if any of the parsers can extract a property from this line 

522 # Note: This will override any existing properties we found, if we found it 

523 # previously. This is usually correct, as some VASP settings can cause certain 

524 # pieces of information to be written multiple times during SCF. We are only 

525 # interested in the final values within a given chunk. 

526 if parser.has_property(cursor, lines): 

527 prop = parser.parse(cursor, lines) 

528 properties.update(prop) 

529 return properties 

530 

531 

532class ChunkParser(TypeParser, ABC): 

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

534 super().__init__(parsers) 

535 self.header = header 

536 

537 @property 

538 def header(self) -> _HEADER: 

539 return self._header 

540 

541 @header.setter 

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

543 self._header = value or {} 

544 self.update_parser_headers() 

545 

546 def update_parser_headers(self) -> None: 

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

548 for parser in self.parsers: 

549 parser.header = self.header 

550 

551 def _check_parsers(self, 

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

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

554 if not all( 

555 isinstance(parser, VaspChunkPropertyParser) 

556 for parser in parsers): 

557 raise TypeError( 

558 'All parsers must be of type VaspChunkPropertyParser') 

559 

560 @abstractmethod 

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

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

563 

564 

565class HeaderParser(TypeParser, ABC): 

566 def _check_parsers(self, 

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

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

569 if not all( 

570 isinstance(parser, VaspHeaderPropertyParser) 

571 for parser in parsers): 

572 raise TypeError( 

573 'All parsers must be of type VaspHeaderPropertyParser') 

574 

575 @abstractmethod 

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

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

578 

579 

580class OutcarChunkParser(ChunkParser): 

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

582 def __init__(self, 

583 header: _HEADER = None, 

584 parsers: Sequence[VaspChunkPropertyParser] = None): 

585 global default_chunk_parsers 

586 parsers = parsers or default_chunk_parsers.make_parsers() 

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

588 

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

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

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

592 

593 results = self.parse(lines) 

594 symbols = self.header['symbols'] 

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

596 

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

598 

599 # Find some required properties in the parsed results. 

600 # Raise ParseError if they are not present 

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

602 try: 

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

604 except KeyError: 

605 raise ParseError( 

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

607 prop)) 

608 atoms = Atoms(**atoms_kwargs) 

609 

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

611 calc = SinglePointDFTCalculator(atoms, **results) 

612 if kpts is not None: 

613 calc.kpts = kpts 

614 calc.name = 'vasp' 

615 atoms.calc = calc 

616 return atoms 

617 

618 

619class OutcarHeaderParser(HeaderParser): 

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

621 def __init__(self, 

622 parsers: Sequence[VaspHeaderPropertyParser] = None, 

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

624 global default_header_parsers 

625 parsers = parsers or default_header_parsers.make_parsers() 

626 super().__init__(parsers) 

627 self.workdir = workdir 

628 

629 @property 

630 def workdir(self): 

631 return self._workdir 

632 

633 @workdir.setter 

634 def workdir(self, value): 

635 if value is not None: 

636 value = Path(value) 

637 self._workdir = value 

638 

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

640 if 'symbols' in results: 

641 # Safeguard, in case a different parser already 

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

643 return results.pop('symbols') 

644 

645 # Build the symbols of the atoms 

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

647 if required_key not in results: 

648 raise ParseError( 

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

650 format(required_key)) 

651 

652 ion_types = results.pop('ion_types') 

653 species = results.pop('species') 

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

655 raise ParseError( 

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

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

658 len(ion_types), len(species))) 

659 

660 # Expand the symbols list 

661 symbols = [] 

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

663 symbols.extend(n * [sym]) 

664 return symbols 

665 

666 def _get_constraint(self): 

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

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

669 OUTCAR parser which does this. 

670 """ 

671 constraint = None 

672 if self.workdir is not None: 

673 constraint = read_constraints_from_file(self.workdir) 

674 return constraint 

675 

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

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

678 results = self.parse(lines) 

679 

680 # Get the symbols from the parsed results 

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

682 symbols = self._build_symbols(results) 

683 natoms = len(symbols) 

684 

685 constraint = self._get_constraint() 

686 

687 # Remaining results from the parse goes into the header 

688 header = dict(symbols=symbols, 

689 natoms=natoms, 

690 constraint=constraint, 

691 **results) 

692 return header 

693 

694 

695class OUTCARChunk(ImageChunk): 

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

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

698 """ 

699 def __init__(self, 

700 lines: _CHUNK, 

701 header: _HEADER, 

702 parser: ChunkParser = None): 

703 super().__init__() 

704 self.lines = lines 

705 self.header = header 

706 self.parser = parser or OutcarChunkParser() 

707 

708 def build(self): 

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

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

711 

712 

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

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

715 lines = [] 

716 for line in fd: 

717 lines.append(line) 

718 if 'Iteration' in line: 

719 # Start of SCF cycle 

720 return lines 

721 

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

723 raise ParseError('Incomplete OUTCAR') 

724 

725 

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

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

728 lines = [] 

729 while True: 

730 line = next(fd) 

731 lines.append(line) 

732 if _OUTCAR_SCF_DELIM in line: 

733 # Add 4 more lines to include energy 

734 for _ in range(4): 

735 lines.append(next(fd)) 

736 break 

737 return lines 

738 

739 

740def outcarchunks(fd: TextIO, 

741 chunk_parser: ChunkParser = None, 

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

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

744 name = Path(fd.name) 

745 workdir = name.parent 

746 

747 # First we get header info 

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

749 header_parser = header_parser or OutcarHeaderParser(workdir=workdir) 

750 

751 lines = build_header(fd) 

752 header = header_parser.build(lines) 

753 assert isinstance(header, dict) 

754 

755 chunk_parser = chunk_parser or OutcarChunkParser() 

756 

757 while True: 

758 try: 

759 lines = build_chunk(fd) 

760 except StopIteration: 

761 # End of file 

762 return 

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

764 

765 

766# Create the default chunk parsers 

767default_chunk_parsers = DefaultParsersContainer( 

768 Cell, 

769 PositionsAndForces, 

770 Stress, 

771 Magmoms, 

772 Magmom, 

773 EFermi, 

774 Kpoints, 

775 Energy, 

776) 

777 

778# Create the default header parsers 

779default_header_parsers = DefaultParsersContainer( 

780 SpeciesTypes, 

781 IonsPerSpecies, 

782 Spinpol, 

783 KpointHeader, 

784)