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) 785