Coverage for /builds/kinetik161/ase/ase/io/vasp_parsers/vasp_outcar_parsers.py: 94.75%
438 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-12-10 11:04 +0000
1"""
2Module for parsing OUTCAR files.
3"""
4import re
5from abc import ABC, abstractmethod
6from pathlib import Path, PurePath
7from typing import Any, Dict, Iterator, List, Optional, Sequence, TextIO, Union
8from warnings import warn
10import numpy as np
12import ase
13from ase import Atoms
14from ase.calculators.singlepoint import (SinglePointDFTCalculator,
15 SinglePointKPoint)
16from ase.data import atomic_numbers
17from ase.io import ParseError, read
18from ase.io.utils import ImageChunk
20# Denotes end of Ionic step for OUTCAR reading
21_OUTCAR_SCF_DELIM = 'FREE ENERGIE OF THE ION-ELECTRON SYSTEM'
23# Some type aliases
24_HEADER = Dict[str, Any]
25_CURSOR = int
26_CHUNK = Sequence[str]
27_RESULT = Dict[str, Any]
30class NoNonEmptyLines(Exception):
31 """No more non-empty lines were left in the provided chunck"""
34class UnableToLocateDelimiter(Exception):
35 """Did not find the provided delimiter"""
37 def __init__(self, delimiter, msg):
38 self.delimiter = delimiter
39 super().__init__(msg)
42def _check_line(line: str) -> str:
43 """Auxiliary check line function for OUTCAR numeric formatting.
44 See issue #179, https://gitlab.com/ase/ase/issues/179
45 Only call in cases we need the numeric values
46 """
47 if re.search('[0-9]-[0-9]', line):
48 line = re.sub('([0-9])-([0-9])', r'\1 -\2', line)
49 return line
52def find_next_non_empty_line(cursor: _CURSOR, lines: _CHUNK) -> _CURSOR:
53 """Fast-forward the cursor from the current position to the next
54 line which is non-empty.
55 Returns the new cursor position on the next non-empty line.
56 """
57 for line in lines[cursor:]:
58 if line.strip():
59 # Line was non-empty
60 return cursor
61 # Empty line, increment the cursor position
62 cursor += 1
63 # There was no non-empty line
64 raise NoNonEmptyLines("Did not find a next line which was not empty")
67def search_lines(delim: str, cursor: _CURSOR, lines: _CHUNK) -> _CURSOR:
68 """Search through a chunk of lines starting at the cursor position for
69 a given delimiter. The new position of the cursor is returned."""
70 for line in lines[cursor:]:
71 if delim in line:
72 # The cursor should be on the line with the delimiter now
73 assert delim in lines[cursor]
74 return cursor
75 # We didn't find the delimiter
76 cursor += 1
77 raise UnableToLocateDelimiter(
78 delim, f'Did not find starting point for delimiter {delim}')
81def convert_vasp_outcar_stress(stress: Sequence):
82 """Helper function to convert the stress line in an OUTCAR to the
83 expected units in ASE """
84 stress_arr = -np.array(stress)
85 shape = stress_arr.shape
86 if shape != (6, ):
87 raise ValueError(
88 f'Stress has the wrong shape. Expected (6,), got {shape}')
89 stress_arr = stress_arr[[0, 1, 2, 4, 5, 3]] * 1e-1 * ase.units.GPa
90 return stress_arr
93def read_constraints_from_file(directory):
94 directory = Path(directory)
95 constraint = None
96 for filename in ('CONTCAR', 'POSCAR'):
97 if (directory / filename).is_file():
98 constraint = read(directory / filename, format='vasp').constraints
99 break
100 return constraint
103class VaspPropertyParser(ABC):
104 NAME = None # type: str
106 @classmethod
107 def get_name(cls):
108 """Name of parser. Override the NAME constant in the class to
109 specify a custom name,
110 otherwise the class name is used"""
111 return cls.NAME or cls.__name__
113 @abstractmethod
114 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool:
115 """Function which checks if a property can be derived from a given
116 cursor position"""
118 @staticmethod
119 def get_line(cursor: _CURSOR, lines: _CHUNK) -> str:
120 """Helper function to get a line, and apply the check_line function"""
121 return _check_line(lines[cursor])
123 @abstractmethod
124 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
125 """Extract a property from the cursor position.
126 Assumes that "has_property" would evaluate to True
127 from cursor position """
130class SimpleProperty(VaspPropertyParser, ABC):
131 LINE_DELIMITER = None # type: str
133 def __init__(self):
134 super().__init__()
135 if self.LINE_DELIMITER is None:
136 raise ValueError('Must specify a line delimiter.')
138 def has_property(self, cursor, lines) -> bool:
139 line = lines[cursor]
140 return self.LINE_DELIMITER in line
143class VaspChunkPropertyParser(VaspPropertyParser, ABC):
144 """Base class for parsing a chunk of the OUTCAR.
145 The base assumption is that only a chunk of lines is passed"""
147 def __init__(self, header: _HEADER = None):
148 super().__init__()
149 header = header or {}
150 self.header = header
152 def get_from_header(self, key: str) -> Any:
153 """Get a key from the header, and raise a ParseError
154 if that key doesn't exist"""
155 try:
156 return self.header[key]
157 except KeyError:
158 raise ParseError(
159 'Parser requested unavailable key "{}" from header'.format(
160 key))
163class VaspHeaderPropertyParser(VaspPropertyParser, ABC):
164 """Base class for parsing the header of an OUTCAR"""
167class SimpleVaspChunkParser(VaspChunkPropertyParser, SimpleProperty, ABC):
168 """Class for properties in a chunk can be
169 determined to exist from 1 line"""
172class SimpleVaspHeaderParser(VaspHeaderPropertyParser, SimpleProperty, ABC):
173 """Class for properties in the header
174 which can be determined to exist from 1 line"""
177class Spinpol(SimpleVaspHeaderParser):
178 """Parse if the calculation is spin-polarized.
180 Example line:
181 " ISPIN = 2 spin polarized calculation?"
183 """
184 LINE_DELIMITER = 'ISPIN'
186 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
187 line = lines[cursor].strip()
188 parts = line.split()
189 ispin = int(parts[2])
190 # ISPIN 2 = spinpolarized, otherwise no
191 # ISPIN 1 = non-spinpolarized
192 spinpol = ispin == 2
193 return {'spinpol': spinpol}
196class SpeciesTypes(SimpleVaspHeaderParser):
197 """Parse species types.
199 Example line:
200 " POTCAR: PAW_PBE Ni 02Aug2007"
202 We must parse this multiple times, as it's scattered in the header.
203 So this class has to simply parse the entire header.
204 """
205 LINE_DELIMITER = 'POTCAR:'
207 def __init__(self, *args, **kwargs):
208 self._species = [] # Store species as we find them
209 # We count the number of times we found the line,
210 # as we only want to parse every second,
211 # due to repeated entries in the OUTCAR
212 super().__init__(*args, **kwargs)
214 @property
215 def species(self) -> List[str]:
216 """Internal storage of each found line.
217 Will contain the double counting.
218 Use the get_species() method to get the un-doubled list."""
219 return self._species
221 def get_species(self) -> List[str]:
222 """The OUTCAR will contain two 'POTCAR:' entries per species.
223 This method only returns the first half,
224 effectively removing the double counting.
225 """
226 # Get the index of the first half
227 # In case we have an odd number, we round up (for testing purposes)
228 # Tests like to just add species 1-by-1
229 # Having an odd number should never happen in a real OUTCAR
230 # For even length lists, this is just equivalent to idx =
231 # len(self.species) // 2
232 idx = sum(divmod(len(self.species), 2))
233 # Make a copy
234 return list(self.species[:idx])
236 def _make_returnval(self) -> _RESULT:
237 """Construct the return value for the "parse" method"""
238 return {'species': self.get_species()}
240 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
241 line = lines[cursor].strip()
243 parts = line.split()
244 # Determine in what position we'd expect to find the symbol
245 if '1/r potential' in line:
246 # This denotes an AE potential
247 # Currently only H_AE
248 # " H 1/r potential "
249 idx = 1
250 else:
251 # Regular PAW potential, e.g.
252 # "PAW_PBE H1.25 07Sep2000" or
253 # "PAW_PBE Fe_pv 02Aug2007"
254 idx = 2
256 sym = parts[idx]
257 # remove "_h", "_GW", "_3" tags etc.
258 sym = sym.split('_')[0]
259 # in the case of the "H1.25" potentials etc.,
260 # remove any non-alphabetic characters
261 sym = ''.join([s for s in sym if s.isalpha()])
263 if sym not in atomic_numbers:
264 # Check that we have properly parsed the symbol, and we found
265 # an element
266 raise ParseError(
267 f'Found an unexpected symbol {sym} in line {line}')
269 self.species.append(sym)
271 return self._make_returnval()
274class IonsPerSpecies(SimpleVaspHeaderParser):
275 """Example line:
277 " ions per type = 32 31 2"
278 """
279 LINE_DELIMITER = 'ions per type'
281 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
282 line = lines[cursor].strip()
283 parts = line.split()
284 ion_types = list(map(int, parts[4:]))
285 return {'ion_types': ion_types}
288class KpointHeader(VaspHeaderPropertyParser):
289 """Reads nkpts and nbands from the line delimiter.
290 Then it also searches for the ibzkpts and kpt_weights"""
292 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool:
293 line = lines[cursor]
294 return "NKPTS" in line and "NBANDS" in line
296 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
297 line = lines[cursor].strip()
298 parts = line.split()
299 nkpts = int(parts[3])
300 nbands = int(parts[-1])
302 results: Dict[str, Any] = {'nkpts': nkpts, 'nbands': nbands}
303 # We also now get the k-point weights etc.,
304 # because we need to know how many k-points we have
305 # for parsing that
306 # Move cursor down to next delimiter
307 delim2 = 'k-points in reciprocal lattice and weights'
308 for offset, line in enumerate(lines[cursor:], start=0):
309 line = line.strip()
310 if delim2 in line:
311 # build k-points
312 ibzkpts = np.zeros((nkpts, 3))
313 kpt_weights = np.zeros(nkpts)
314 for nk in range(nkpts):
315 # Offset by 1, as k-points starts on the next line
316 line = lines[cursor + offset + nk + 1].strip()
317 parts = line.split()
318 ibzkpts[nk] = list(map(float, parts[:3]))
319 kpt_weights[nk] = float(parts[-1])
320 results['ibzkpts'] = ibzkpts
321 results['kpt_weights'] = kpt_weights
322 break
323 else:
324 raise ParseError('Did not find the K-points in the OUTCAR')
326 return results
329class Stress(SimpleVaspChunkParser):
330 """Process the stress from an OUTCAR"""
331 LINE_DELIMITER = 'in kB '
333 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
334 line = self.get_line(cursor, lines)
335 result = None # type: Optional[Sequence[float]]
336 try:
337 stress = [float(a) for a in line.split()[2:]]
338 except ValueError:
339 # Vasp FORTRAN string formatting issues, can happen with
340 # some bad geometry steps Alternatively, we can re-raise
341 # as a ParseError?
342 warn('Found badly formatted stress line. Setting stress to None.')
343 else:
344 result = convert_vasp_outcar_stress(stress)
345 return {'stress': result}
348class Cell(SimpleVaspChunkParser):
349 LINE_DELIMITER = 'direct lattice vectors'
351 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
352 nskip = 1
353 cell = np.zeros((3, 3))
354 for i in range(3):
355 line = self.get_line(cursor + i + nskip, lines)
356 parts = line.split()
357 cell[i, :] = list(map(float, parts[0:3]))
358 return {'cell': cell}
361class PositionsAndForces(SimpleVaspChunkParser):
362 """Positions and forces are written in the same block.
363 We parse both simultaneously"""
364 LINE_DELIMITER = 'POSITION '
366 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
367 nskip = 2
368 natoms = self.get_from_header('natoms')
369 positions = np.zeros((natoms, 3))
370 forces = np.zeros((natoms, 3))
372 for i in range(natoms):
373 line = self.get_line(cursor + i + nskip, lines)
374 parts = list(map(float, line.split()))
375 positions[i] = parts[0:3]
376 forces[i] = parts[3:6]
377 return {'positions': positions, 'forces': forces}
380class Magmom(VaspChunkPropertyParser):
381 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool:
382 """ We need to check for two separate delimiter strings,
383 to ensure we are at the right place """
384 line = lines[cursor]
385 if 'number of electron' in line:
386 parts = line.split()
387 if len(parts) > 5 and parts[0].strip() != "NELECT":
388 return True
389 return False
391 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
392 line = self.get_line(cursor, lines)
393 parts = line.split()
394 idx = parts.index('magnetization') + 1
395 magmom_lst = parts[idx:]
396 if len(magmom_lst) != 1:
397 warn(
398 'Non-collinear spin is not yet implemented. '
399 'Setting magmom to x value.')
400 magmom = float(magmom_lst[0])
401 # Use these lines when non-collinear spin is supported!
402 # Remember to check that format fits!
403 # else:
404 # # Non-collinear spin
405 # # Make a (3,) dim array
406 # magmom = np.array(list(map(float, magmom)))
407 return {'magmom': magmom}
410class Magmoms(SimpleVaspChunkParser):
411 """Get the x-component of the magnitization.
412 This is just the magmoms in the collinear case.
414 non-collinear spin is (currently) not supported"""
415 LINE_DELIMITER = 'magnetization (x)'
417 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
418 # Magnetization for collinear
419 natoms = self.get_from_header('natoms')
420 nskip = 4 # Skip some lines
421 magmoms = np.zeros(natoms)
422 for i in range(natoms):
423 line = self.get_line(cursor + i + nskip, lines)
424 magmoms[i] = float(line.split()[-1])
425 # Once we support non-collinear spin,
426 # search for magnetization (y) and magnetization (z) as well.
427 return {'magmoms': magmoms}
430class EFermi(SimpleVaspChunkParser):
431 LINE_DELIMITER = 'E-fermi :'
433 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
434 line = self.get_line(cursor, lines)
435 parts = line.split()
436 efermi = float(parts[2])
437 return {'efermi': efermi}
440class Energy(SimpleVaspChunkParser):
441 LINE_DELIMITER = _OUTCAR_SCF_DELIM
443 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
444 nskip = 2
445 line = self.get_line(cursor + nskip, lines)
446 parts = line.strip().split()
447 energy_free = float(parts[4]) # Force consistent
449 nskip = 4
450 line = self.get_line(cursor + nskip, lines)
451 parts = line.strip().split()
452 energy_zero = float(parts[6]) # Extrapolated to 0 K
454 return {'free_energy': energy_free, 'energy': energy_zero}
457class Kpoints(VaspChunkPropertyParser):
458 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool:
459 line = lines[cursor]
460 # Example line:
461 # " spin component 1" or " spin component 2"
462 # We only check spin up, as if we are spin-polarized, we'll parse that
463 # as well
464 if 'spin component 1' in line:
465 parts = line.strip().split()
466 # This string is repeated elsewhere, but not with this exact shape
467 if len(parts) == 3:
468 try:
469 # The last part of te line should be an integer, denoting
470 # spin-up or spin-down
471 int(parts[-1])
472 except ValueError:
473 pass
474 else:
475 return True
476 return False
478 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
479 nkpts = self.get_from_header('nkpts')
480 nbands = self.get_from_header('nbands')
481 weights = self.get_from_header('kpt_weights')
482 spinpol = self.get_from_header('spinpol')
483 nspins = 2 if spinpol else 1
485 kpts = []
486 for spin in range(nspins):
487 # for Vasp 6, they added some extra information after the
488 # spin components. so we might need to seek the spin
489 # component line
490 cursor = search_lines(f'spin component {spin + 1}', cursor, lines)
492 cursor += 2 # Skip two lines
493 for _ in range(nkpts):
494 # Skip empty lines
495 cursor = find_next_non_empty_line(cursor, lines)
497 line = self.get_line(cursor, lines)
498 # Example line:
499 # "k-point 1 : 0.0000 0.0000 0.0000"
500 parts = line.strip().split()
501 ikpt = int(parts[1]) - 1 # Make kpt idx start from 0
502 weight = weights[ikpt]
504 cursor += 2 # Move down two
505 eigenvalues = np.zeros(nbands)
506 occupations = np.zeros(nbands)
507 for n in range(nbands):
508 # Example line:
509 # " 1 -9.9948 1.00000"
510 parts = lines[cursor].strip().split()
511 eps_n, f_n = map(float, parts[1:])
512 occupations[n] = f_n
513 eigenvalues[n] = eps_n
514 cursor += 1
515 kpt = SinglePointKPoint(weight,
516 spin,
517 ikpt,
518 eps_n=eigenvalues,
519 f_n=occupations)
520 kpts.append(kpt)
522 return {'kpts': kpts}
525class DefaultParsersContainer:
526 """Container for the default OUTCAR parsers.
527 Allows for modification of the global default parsers.
529 Takes in an arbitrary number of parsers.
530 The parsers should be uninitialized,
531 as they are created on request.
532 """
534 def __init__(self, *parsers_cls):
535 self._parsers_dct = {}
536 for parser in parsers_cls:
537 self.add_parser(parser)
539 @property
540 def parsers_dct(self) -> dict:
541 return self._parsers_dct
543 def make_parsers(self):
544 """Return a copy of the internally stored parsers.
545 Parsers are created upon request."""
546 return [parser() for parser in self.parsers_dct.values()]
548 def remove_parser(self, name: str):
549 """Remove a parser based on the name.
550 The name must match the parser name exactly."""
551 self.parsers_dct.pop(name)
553 def add_parser(self, parser) -> None:
554 """Add a parser"""
555 self.parsers_dct[parser.get_name()] = parser
558class TypeParser(ABC):
559 """Base class for parsing a type, e.g. header or chunk,
560 by applying the internal attached parsers"""
562 def __init__(self, parsers):
563 self.parsers = parsers
565 @property
566 def parsers(self):
567 return self._parsers
569 @parsers.setter
570 def parsers(self, new_parsers) -> None:
571 self._check_parsers(new_parsers)
572 self._parsers = new_parsers
574 @abstractmethod
575 def _check_parsers(self, parsers) -> None:
576 """Check the parsers are of correct type"""
578 def parse(self, lines) -> _RESULT:
579 """Execute the attached paresers, and return the parsed properties"""
580 properties = {}
581 for cursor, _ in enumerate(lines):
582 for parser in self.parsers:
583 # Check if any of the parsers can extract a property
584 # from this line Note: This will override any existing
585 # properties we found, if we found it previously. This
586 # is usually correct, as some VASP settings can cause
587 # certain pieces of information to be written multiple
588 # times during SCF. We are only interested in the
589 # final values within a given chunk.
590 if parser.has_property(cursor, lines):
591 prop = parser.parse(cursor, lines)
592 properties.update(prop)
593 return properties
596class ChunkParser(TypeParser, ABC):
597 def __init__(self, parsers, header=None):
598 super().__init__(parsers)
599 self.header = header
601 @property
602 def header(self) -> _HEADER:
603 return self._header
605 @header.setter
606 def header(self, value: Optional[_HEADER]) -> None:
607 self._header = value or {}
608 self.update_parser_headers()
610 def update_parser_headers(self) -> None:
611 """Apply the header to all available parsers"""
612 for parser in self.parsers:
613 parser.header = self.header
615 def _check_parsers(self,
616 parsers: Sequence[VaspChunkPropertyParser]) -> None:
617 """Check the parsers are of correct type 'VaspChunkPropertyParser'"""
618 if not all(
619 isinstance(parser, VaspChunkPropertyParser)
620 for parser in parsers):
621 raise TypeError(
622 'All parsers must be of type VaspChunkPropertyParser')
624 @abstractmethod
625 def build(self, lines: _CHUNK) -> Atoms:
626 """Construct an atoms object of the chunk from the parsed results"""
629class HeaderParser(TypeParser, ABC):
630 def _check_parsers(self,
631 parsers: Sequence[VaspHeaderPropertyParser]) -> None:
632 """Check the parsers are of correct type 'VaspHeaderPropertyParser'"""
633 if not all(
634 isinstance(parser, VaspHeaderPropertyParser)
635 for parser in parsers):
636 raise TypeError(
637 'All parsers must be of type VaspHeaderPropertyParser')
639 @abstractmethod
640 def build(self, lines: _CHUNK) -> _HEADER:
641 """Construct the header object from the parsed results"""
644class OutcarChunkParser(ChunkParser):
645 """Class for parsing a chunk of an OUTCAR."""
647 def __init__(self,
648 header: _HEADER = None,
649 parsers: Sequence[VaspChunkPropertyParser] = None):
650 global default_chunk_parsers
651 parsers = parsers or default_chunk_parsers.make_parsers()
652 super().__init__(parsers, header=header)
654 def build(self, lines: _CHUNK) -> Atoms:
655 """Apply outcar chunk parsers, and build an atoms object"""
656 self.update_parser_headers() # Ensure header is in sync
658 results = self.parse(lines)
659 symbols = self.header['symbols']
660 constraint = self.header.get('constraint', None)
662 atoms_kwargs = dict(symbols=symbols, constraint=constraint, pbc=True)
664 # Find some required properties in the parsed results.
665 # Raise ParseError if they are not present
666 for prop in ('positions', 'cell'):
667 try:
668 atoms_kwargs[prop] = results.pop(prop)
669 except KeyError:
670 raise ParseError(
671 'Did not find required property {} during parse.'.format(
672 prop))
673 atoms = Atoms(**atoms_kwargs)
675 kpts = results.pop('kpts', None)
676 calc = SinglePointDFTCalculator(atoms, **results)
677 if kpts is not None:
678 calc.kpts = kpts
679 calc.name = 'vasp'
680 atoms.calc = calc
681 return atoms
684class OutcarHeaderParser(HeaderParser):
685 """Class for parsing a chunk of an OUTCAR."""
687 def __init__(self,
688 parsers: Sequence[VaspHeaderPropertyParser] = None,
689 workdir: Union[str, PurePath] = None):
690 global default_header_parsers
691 parsers = parsers or default_header_parsers.make_parsers()
692 super().__init__(parsers)
693 self.workdir = workdir
695 @property
696 def workdir(self):
697 return self._workdir
699 @workdir.setter
700 def workdir(self, value):
701 if value is not None:
702 value = Path(value)
703 self._workdir = value
705 def _build_symbols(self, results: _RESULT) -> Sequence[str]:
706 if 'symbols' in results:
707 # Safeguard, in case a different parser already
708 # did this. Not currently available in a default parser
709 return results.pop('symbols')
711 # Build the symbols of the atoms
712 for required_key in ('ion_types', 'species'):
713 if required_key not in results:
714 raise ParseError(
715 'Did not find required key "{}" in parsed header results.'.
716 format(required_key))
718 ion_types = results.pop('ion_types')
719 species = results.pop('species')
720 if len(ion_types) != len(species):
721 raise ParseError(
722 ('Expected length of ion_types to be same as species, '
723 'but got ion_types={} and species={}').format(
724 len(ion_types), len(species)))
726 # Expand the symbols list
727 symbols = []
728 for n, sym in zip(ion_types, species):
729 symbols.extend(n * [sym])
730 return symbols
732 def _get_constraint(self):
733 """Try and get the constraints from the POSCAR of CONTCAR
734 since they aren't located in the OUTCAR, and thus we cannot construct an
735 OUTCAR parser which does this.
736 """
737 constraint = None
738 if self.workdir is not None:
739 constraint = read_constraints_from_file(self.workdir)
740 return constraint
742 def build(self, lines: _CHUNK) -> _RESULT:
743 """Apply the header parsers, and build the header"""
744 results = self.parse(lines)
746 # Get the symbols from the parsed results
747 # will pop the keys which we use for that purpose
748 symbols = self._build_symbols(results)
749 natoms = len(symbols)
751 constraint = self._get_constraint()
753 # Remaining results from the parse goes into the header
754 header = dict(symbols=symbols,
755 natoms=natoms,
756 constraint=constraint,
757 **results)
758 return header
761class OUTCARChunk(ImageChunk):
762 """Container class for a chunk of the OUTCAR which consists of a
763 self-contained SCF step, i.e. and image. Also contains the header_data
764 """
766 def __init__(self,
767 lines: _CHUNK,
768 header: _HEADER,
769 parser: ChunkParser = None):
770 super().__init__()
771 self.lines = lines
772 self.header = header
773 self.parser = parser or OutcarChunkParser()
775 def build(self):
776 self.parser.header = self.header # Ensure header is syncronized
777 return self.parser.build(self.lines)
780def build_header(fd: TextIO) -> _CHUNK:
781 """Build a chunk containing the header data"""
782 lines = []
783 for line in fd:
784 lines.append(line)
785 if 'Iteration' in line:
786 # Start of SCF cycle
787 return lines
789 # We never found the SCF delimiter, so the OUTCAR must be incomplete
790 raise ParseError('Incomplete OUTCAR')
793def build_chunk(fd: TextIO) -> _CHUNK:
794 """Build chunk which contains 1 complete atoms object"""
795 lines = []
796 while True:
797 line = next(fd)
798 lines.append(line)
799 if _OUTCAR_SCF_DELIM in line:
800 # Add 4 more lines to include energy
801 for _ in range(4):
802 lines.append(next(fd))
803 break
804 return lines
807def outcarchunks(fd: TextIO,
808 chunk_parser: ChunkParser = None,
809 header_parser: HeaderParser = None) -> Iterator[OUTCARChunk]:
810 """Function to build chunks of OUTCAR from a file stream"""
811 name = Path(fd.name)
812 workdir = name.parent
814 # First we get header info
815 # pass in the workdir from the fd, so we can try and get the constraints
816 header_parser = header_parser or OutcarHeaderParser(workdir=workdir)
818 lines = build_header(fd)
819 header = header_parser.build(lines)
820 assert isinstance(header, dict)
822 chunk_parser = chunk_parser or OutcarChunkParser()
824 while True:
825 try:
826 lines = build_chunk(fd)
827 except StopIteration:
828 # End of file
829 return
830 yield OUTCARChunk(lines, header, parser=chunk_parser)
833# Create the default chunk parsers
834default_chunk_parsers = DefaultParsersContainer(
835 Cell,
836 PositionsAndForces,
837 Stress,
838 Magmoms,
839 Magmom,
840 EFermi,
841 Kpoints,
842 Energy,
843)
845# Create the default header parsers
846default_header_parsers = DefaultParsersContainer(
847 SpeciesTypes,
848 IonsPerSpecies,
849 Spinpol,
850 KpointHeader,
851)