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