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
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
19# Denotes end of Ionic step for OUTCAR reading
20_OUTCAR_SCF_DELIM = 'FREE ENERGIE OF THE ION-ELECTRON SYSTEM'
22# Some type aliases
23_HEADER = Dict[str, Any]
24_CURSOR = int
25_CHUNK = Sequence[str]
26_RESULT = Dict[str, Any]
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
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
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
61class VaspPropertyParser(ABC):
62 NAME = None # type: str
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__
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"""
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])
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 """
87class SimpleProperty(VaspPropertyParser, ABC):
88 LINE_DELIMITER = None # type: str
90 def __init__(self):
91 super().__init__()
92 if self.LINE_DELIMITER is None:
93 raise ValueError('Must specify a line delimiter.')
95 def has_property(self, cursor, lines) -> bool:
96 line = lines[cursor]
97 return self.LINE_DELIMITER in line
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
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))
119class VaspHeaderPropertyParser(VaspPropertyParser, ABC):
120 """Base class for parsing the header of an OUTCAR"""
123class SimpleVaspChunkParser(VaspChunkPropertyParser, SimpleProperty, ABC):
124 """Class for properties in a chunk can be determined to exist from 1 line"""
127class SimpleVaspHeaderParser(VaspHeaderPropertyParser, SimpleProperty, ABC):
128 """Class for properties in the header which can be determined to exist from 1 line"""
131class Spinpol(SimpleVaspHeaderParser):
132 """Parse if the calculation is spin-polarized.
134 Example line:
135 " ISPIN = 2 spin polarized calculation?"
137 """
138 LINE_DELIMITER = 'ISPIN'
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}
150class SpeciesTypes(SimpleVaspHeaderParser):
151 """Parse species types.
153 Example line:
154 " POTCAR: PAW_PBE Ni 02Aug2007"
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:'
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)
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
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])
189 def _make_returnval(self) -> _RESULT:
190 """Construct the return value for the "parse" method"""
191 return {'species': self.get_species()}
193 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
194 line = lines[cursor].strip()
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
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()])
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}')
222 self.species.append(sym)
224 return self._make_returnval()
227class IonsPerSpecies(SimpleVaspHeaderParser):
228 """Example line:
230 " ions per type = 32 31 2"
231 """
232 LINE_DELIMITER = 'ions per type'
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}
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'
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])
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')
276 return results
279class Stress(SimpleVaspChunkParser):
280 """Process the stress from an OUTCAR"""
281 LINE_DELIMITER = 'in kB '
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}
297class Cell(SimpleVaspChunkParser):
298 LINE_DELIMITER = 'direct lattice vectors'
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}
310class PositionsAndForces(SimpleVaspChunkParser):
311 """Positions and forces are written in the same block.
312 We parse both simultaneously"""
313 LINE_DELIMITER = 'POSITION '
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))
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}
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
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}
359class Magmoms(SimpleVaspChunkParser):
360 """Get the x-component of the magnitization.
361 This is just the magmoms in the collinear case.
363 non-collinear spin is (currently) not supported"""
364 LINE_DELIMITER = 'magnetization (x)'
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}
379class EFermi(SimpleVaspChunkParser):
380 LINE_DELIMITER = 'E-fermi :'
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}
389class Energy(SimpleVaspChunkParser):
390 LINE_DELIMITER = _OUTCAR_SCF_DELIM
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
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
403 return {'free_energy': energy_free, 'energy': energy_zero}
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
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
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]
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}
467class DefaultParsersContainer:
468 """Container for the default OUTCAR parsers.
469 Allows for modification of the global default parsers.
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)
479 @property
480 def parsers_dct(self) -> dict:
481 return self._parsers_dct
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())
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)
492 def add_parser(self, parser) -> None:
493 """Add a parser"""
494 self.parsers_dct[parser.get_name()] = parser
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
503 @property
504 def parsers(self):
505 return self._parsers
507 @parsers.setter
508 def parsers(self, new_parsers) -> None:
509 self._check_parsers(new_parsers)
510 self._parsers = new_parsers
512 @abstractmethod
513 def _check_parsers(self, parsers) -> None:
514 """Check the parsers are of correct type"""
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
532class ChunkParser(TypeParser, ABC):
533 def __init__(self, parsers, header=None):
534 super().__init__(parsers)
535 self.header = header
537 @property
538 def header(self) -> _HEADER:
539 return self._header
541 @header.setter
542 def header(self, value: Optional[_HEADER]) -> None:
543 self._header = value or {}
544 self.update_parser_headers()
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
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')
560 @abstractmethod
561 def build(self, lines: _CHUNK) -> Atoms:
562 """Construct an atoms object of the chunk from the parsed results"""
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')
575 @abstractmethod
576 def build(self, lines: _CHUNK) -> _HEADER:
577 """Construct the header object from the parsed results"""
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)
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
593 results = self.parse(lines)
594 symbols = self.header['symbols']
595 constraint = self.header.get('constraint', None)
597 atoms_kwargs = dict(symbols=symbols, constraint=constraint, pbc=True)
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)
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
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
629 @property
630 def workdir(self):
631 return self._workdir
633 @workdir.setter
634 def workdir(self, value):
635 if value is not None:
636 value = Path(value)
637 self._workdir = value
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')
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))
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)))
660 # Expand the symbols list
661 symbols = []
662 for n, sym in zip(ion_types, species):
663 symbols.extend(n * [sym])
664 return symbols
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
676 def build(self, lines: _CHUNK) -> _RESULT:
677 """Apply the header parsers, and build the header"""
678 results = self.parse(lines)
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)
685 constraint = self._get_constraint()
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
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()
708 def build(self):
709 self.parser.header = self.header # Ensure header is syncronized
710 return self.parser.build(self.lines)
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
722 # We never found the SCF delimiter, so the OUTCAR must be incomplete
723 raise ParseError('Incomplete OUTCAR')
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
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
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)
751 lines = build_header(fd)
752 header = header_parser.build(lines)
753 assert isinstance(header, dict)
755 chunk_parser = chunk_parser or OutcarChunkParser()
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)
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)
778# Create the default header parsers
779default_header_parsers = DefaultParsersContainer(
780 SpeciesTypes,
781 IonsPerSpecies,
782 Spinpol,
783 KpointHeader,
784)