Coverage for /builds/hweiske/ase/ase/io/vasp_parsers/vasp_outcar_parsers.py: 94.75%
438 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-22 11:22 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-04-22 11:22 +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,
99 format='vasp',
100 parallel=False).constraints
101 break
102 return constraint
105class VaspPropertyParser(ABC):
106 NAME = None # type: str
108 @classmethod
109 def get_name(cls):
110 """Name of parser. Override the NAME constant in the class to
111 specify a custom name,
112 otherwise the class name is used"""
113 return cls.NAME or cls.__name__
115 @abstractmethod
116 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool:
117 """Function which checks if a property can be derived from a given
118 cursor position"""
120 @staticmethod
121 def get_line(cursor: _CURSOR, lines: _CHUNK) -> str:
122 """Helper function to get a line, and apply the check_line function"""
123 return _check_line(lines[cursor])
125 @abstractmethod
126 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
127 """Extract a property from the cursor position.
128 Assumes that "has_property" would evaluate to True
129 from cursor position """
132class SimpleProperty(VaspPropertyParser, ABC):
133 LINE_DELIMITER = None # type: str
135 def __init__(self):
136 super().__init__()
137 if self.LINE_DELIMITER is None:
138 raise ValueError('Must specify a line delimiter.')
140 def has_property(self, cursor, lines) -> bool:
141 line = lines[cursor]
142 return self.LINE_DELIMITER in line
145class VaspChunkPropertyParser(VaspPropertyParser, ABC):
146 """Base class for parsing a chunk of the OUTCAR.
147 The base assumption is that only a chunk of lines is passed"""
149 def __init__(self, header: _HEADER = None):
150 super().__init__()
151 header = header or {}
152 self.header = header
154 def get_from_header(self, key: str) -> Any:
155 """Get a key from the header, and raise a ParseError
156 if that key doesn't exist"""
157 try:
158 return self.header[key]
159 except KeyError:
160 raise ParseError(
161 'Parser requested unavailable key "{}" from header'.format(
162 key))
165class VaspHeaderPropertyParser(VaspPropertyParser, ABC):
166 """Base class for parsing the header of an OUTCAR"""
169class SimpleVaspChunkParser(VaspChunkPropertyParser, SimpleProperty, ABC):
170 """Class for properties in a chunk can be
171 determined to exist from 1 line"""
174class SimpleVaspHeaderParser(VaspHeaderPropertyParser, SimpleProperty, ABC):
175 """Class for properties in the header
176 which can be determined to exist from 1 line"""
179class Spinpol(SimpleVaspHeaderParser):
180 """Parse if the calculation is spin-polarized.
182 Example line:
183 " ISPIN = 2 spin polarized calculation?"
185 """
186 LINE_DELIMITER = 'ISPIN'
188 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
189 line = lines[cursor].strip()
190 parts = line.split()
191 ispin = int(parts[2])
192 # ISPIN 2 = spinpolarized, otherwise no
193 # ISPIN 1 = non-spinpolarized
194 spinpol = ispin == 2
195 return {'spinpol': spinpol}
198class SpeciesTypes(SimpleVaspHeaderParser):
199 """Parse species types.
201 Example line:
202 " POTCAR: PAW_PBE Ni 02Aug2007"
204 We must parse this multiple times, as it's scattered in the header.
205 So this class has to simply parse the entire header.
206 """
207 LINE_DELIMITER = 'POTCAR:'
209 def __init__(self, *args, **kwargs):
210 self._species = [] # Store species as we find them
211 # We count the number of times we found the line,
212 # as we only want to parse every second,
213 # due to repeated entries in the OUTCAR
214 super().__init__(*args, **kwargs)
216 @property
217 def species(self) -> List[str]:
218 """Internal storage of each found line.
219 Will contain the double counting.
220 Use the get_species() method to get the un-doubled list."""
221 return self._species
223 def get_species(self) -> List[str]:
224 """The OUTCAR will contain two 'POTCAR:' entries per species.
225 This method only returns the first half,
226 effectively removing the double counting.
227 """
228 # Get the index of the first half
229 # In case we have an odd number, we round up (for testing purposes)
230 # Tests like to just add species 1-by-1
231 # Having an odd number should never happen in a real OUTCAR
232 # For even length lists, this is just equivalent to idx =
233 # len(self.species) // 2
234 idx = sum(divmod(len(self.species), 2))
235 # Make a copy
236 return list(self.species[:idx])
238 def _make_returnval(self) -> _RESULT:
239 """Construct the return value for the "parse" method"""
240 return {'species': self.get_species()}
242 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
243 line = lines[cursor].strip()
245 parts = line.split()
246 # Determine in what position we'd expect to find the symbol
247 if '1/r potential' in line:
248 # This denotes an AE potential
249 # Currently only H_AE
250 # " H 1/r potential "
251 idx = 1
252 else:
253 # Regular PAW potential, e.g.
254 # "PAW_PBE H1.25 07Sep2000" or
255 # "PAW_PBE Fe_pv 02Aug2007"
256 idx = 2
258 sym = parts[idx]
259 # remove "_h", "_GW", "_3" tags etc.
260 sym = sym.split('_')[0]
261 # in the case of the "H1.25" potentials etc.,
262 # remove any non-alphabetic characters
263 sym = ''.join([s for s in sym if s.isalpha()])
265 if sym not in atomic_numbers:
266 # Check that we have properly parsed the symbol, and we found
267 # an element
268 raise ParseError(
269 f'Found an unexpected symbol {sym} in line {line}')
271 self.species.append(sym)
273 return self._make_returnval()
276class IonsPerSpecies(SimpleVaspHeaderParser):
277 """Example line:
279 " ions per type = 32 31 2"
280 """
281 LINE_DELIMITER = 'ions per type'
283 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
284 line = lines[cursor].strip()
285 parts = line.split()
286 ion_types = list(map(int, parts[4:]))
287 return {'ion_types': ion_types}
290class KpointHeader(VaspHeaderPropertyParser):
291 """Reads nkpts and nbands from the line delimiter.
292 Then it also searches for the ibzkpts and kpt_weights"""
294 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool:
295 line = lines[cursor]
296 return "NKPTS" in line and "NBANDS" in line
298 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
299 line = lines[cursor].strip()
300 parts = line.split()
301 nkpts = int(parts[3])
302 nbands = int(parts[-1])
304 results: Dict[str, Any] = {'nkpts': nkpts, 'nbands': nbands}
305 # We also now get the k-point weights etc.,
306 # because we need to know how many k-points we have
307 # for parsing that
308 # Move cursor down to next delimiter
309 delim2 = 'k-points in reciprocal lattice and weights'
310 for offset, line in enumerate(lines[cursor:], start=0):
311 line = line.strip()
312 if delim2 in line:
313 # build k-points
314 ibzkpts = np.zeros((nkpts, 3))
315 kpt_weights = np.zeros(nkpts)
316 for nk in range(nkpts):
317 # Offset by 1, as k-points starts on the next line
318 line = lines[cursor + offset + nk + 1].strip()
319 parts = line.split()
320 ibzkpts[nk] = list(map(float, parts[:3]))
321 kpt_weights[nk] = float(parts[-1])
322 results['ibzkpts'] = ibzkpts
323 results['kpt_weights'] = kpt_weights
324 break
325 else:
326 raise ParseError('Did not find the K-points in the OUTCAR')
328 return results
331class Stress(SimpleVaspChunkParser):
332 """Process the stress from an OUTCAR"""
333 LINE_DELIMITER = 'in kB '
335 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
336 line = self.get_line(cursor, lines)
337 result = None # type: Optional[Sequence[float]]
338 try:
339 stress = [float(a) for a in line.split()[2:]]
340 except ValueError:
341 # Vasp FORTRAN string formatting issues, can happen with
342 # some bad geometry steps Alternatively, we can re-raise
343 # as a ParseError?
344 warn('Found badly formatted stress line. Setting stress to None.')
345 else:
346 result = convert_vasp_outcar_stress(stress)
347 return {'stress': result}
350class Cell(SimpleVaspChunkParser):
351 LINE_DELIMITER = 'direct lattice vectors'
353 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
354 nskip = 1
355 cell = np.zeros((3, 3))
356 for i in range(3):
357 line = self.get_line(cursor + i + nskip, lines)
358 parts = line.split()
359 cell[i, :] = list(map(float, parts[0:3]))
360 return {'cell': cell}
363class PositionsAndForces(SimpleVaspChunkParser):
364 """Positions and forces are written in the same block.
365 We parse both simultaneously"""
366 LINE_DELIMITER = 'POSITION '
368 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
369 nskip = 2
370 natoms = self.get_from_header('natoms')
371 positions = np.zeros((natoms, 3))
372 forces = np.zeros((natoms, 3))
374 for i in range(natoms):
375 line = self.get_line(cursor + i + nskip, lines)
376 parts = list(map(float, line.split()))
377 positions[i] = parts[0:3]
378 forces[i] = parts[3:6]
379 return {'positions': positions, 'forces': forces}
382class Magmom(VaspChunkPropertyParser):
383 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool:
384 """ We need to check for two separate delimiter strings,
385 to ensure we are at the right place """
386 line = lines[cursor]
387 if 'number of electron' in line:
388 parts = line.split()
389 if len(parts) > 5 and parts[0].strip() != "NELECT":
390 return True
391 return False
393 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
394 line = self.get_line(cursor, lines)
395 parts = line.split()
396 idx = parts.index('magnetization') + 1
397 magmom_lst = parts[idx:]
398 if len(magmom_lst) != 1:
399 warn(
400 'Non-collinear spin is not yet implemented. '
401 'Setting magmom to x value.')
402 magmom = float(magmom_lst[0])
403 # Use these lines when non-collinear spin is supported!
404 # Remember to check that format fits!
405 # else:
406 # # Non-collinear spin
407 # # Make a (3,) dim array
408 # magmom = np.array(list(map(float, magmom)))
409 return {'magmom': magmom}
412class Magmoms(SimpleVaspChunkParser):
413 """Get the x-component of the magnitization.
414 This is just the magmoms in the collinear case.
416 non-collinear spin is (currently) not supported"""
417 LINE_DELIMITER = 'magnetization (x)'
419 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
420 # Magnetization for collinear
421 natoms = self.get_from_header('natoms')
422 nskip = 4 # Skip some lines
423 magmoms = np.zeros(natoms)
424 for i in range(natoms):
425 line = self.get_line(cursor + i + nskip, lines)
426 magmoms[i] = float(line.split()[-1])
427 # Once we support non-collinear spin,
428 # search for magnetization (y) and magnetization (z) as well.
429 return {'magmoms': magmoms}
432class EFermi(SimpleVaspChunkParser):
433 LINE_DELIMITER = 'E-fermi :'
435 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
436 line = self.get_line(cursor, lines)
437 parts = line.split()
438 efermi = float(parts[2])
439 return {'efermi': efermi}
442class Energy(SimpleVaspChunkParser):
443 LINE_DELIMITER = _OUTCAR_SCF_DELIM
445 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
446 nskip = 2
447 line = self.get_line(cursor + nskip, lines)
448 parts = line.strip().split()
449 energy_free = float(parts[4]) # Force consistent
451 nskip = 4
452 line = self.get_line(cursor + nskip, lines)
453 parts = line.strip().split()
454 energy_zero = float(parts[6]) # Extrapolated to 0 K
456 return {'free_energy': energy_free, 'energy': energy_zero}
459class Kpoints(VaspChunkPropertyParser):
460 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool:
461 line = lines[cursor]
462 # Example line:
463 # " spin component 1" or " spin component 2"
464 # We only check spin up, as if we are spin-polarized, we'll parse that
465 # as well
466 if 'spin component 1' in line:
467 parts = line.strip().split()
468 # This string is repeated elsewhere, but not with this exact shape
469 if len(parts) == 3:
470 try:
471 # The last part of te line should be an integer, denoting
472 # spin-up or spin-down
473 int(parts[-1])
474 except ValueError:
475 pass
476 else:
477 return True
478 return False
480 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
481 nkpts = self.get_from_header('nkpts')
482 nbands = self.get_from_header('nbands')
483 weights = self.get_from_header('kpt_weights')
484 spinpol = self.get_from_header('spinpol')
485 nspins = 2 if spinpol else 1
487 kpts = []
488 for spin in range(nspins):
489 # for Vasp 6, they added some extra information after the
490 # spin components. so we might need to seek the spin
491 # component line
492 cursor = search_lines(f'spin component {spin + 1}', cursor, lines)
494 cursor += 2 # Skip two lines
495 for _ in range(nkpts):
496 # Skip empty lines
497 cursor = find_next_non_empty_line(cursor, lines)
499 line = self.get_line(cursor, lines)
500 # Example line:
501 # "k-point 1 : 0.0000 0.0000 0.0000"
502 parts = line.strip().split()
503 ikpt = int(parts[1]) - 1 # Make kpt idx start from 0
504 weight = weights[ikpt]
506 cursor += 2 # Move down two
507 eigenvalues = np.zeros(nbands)
508 occupations = np.zeros(nbands)
509 for n in range(nbands):
510 # Example line:
511 # " 1 -9.9948 1.00000"
512 parts = lines[cursor].strip().split()
513 eps_n, f_n = map(float, parts[1:])
514 occupations[n] = f_n
515 eigenvalues[n] = eps_n
516 cursor += 1
517 kpt = SinglePointKPoint(weight,
518 spin,
519 ikpt,
520 eps_n=eigenvalues,
521 f_n=occupations)
522 kpts.append(kpt)
524 return {'kpts': kpts}
527class DefaultParsersContainer:
528 """Container for the default OUTCAR parsers.
529 Allows for modification of the global default parsers.
531 Takes in an arbitrary number of parsers.
532 The parsers should be uninitialized,
533 as they are created on request.
534 """
536 def __init__(self, *parsers_cls):
537 self._parsers_dct = {}
538 for parser in parsers_cls:
539 self.add_parser(parser)
541 @property
542 def parsers_dct(self) -> dict:
543 return self._parsers_dct
545 def make_parsers(self):
546 """Return a copy of the internally stored parsers.
547 Parsers are created upon request."""
548 return [parser() for parser in self.parsers_dct.values()]
550 def remove_parser(self, name: str):
551 """Remove a parser based on the name.
552 The name must match the parser name exactly."""
553 self.parsers_dct.pop(name)
555 def add_parser(self, parser) -> None:
556 """Add a parser"""
557 self.parsers_dct[parser.get_name()] = parser
560class TypeParser(ABC):
561 """Base class for parsing a type, e.g. header or chunk,
562 by applying the internal attached parsers"""
564 def __init__(self, parsers):
565 self.parsers = parsers
567 @property
568 def parsers(self):
569 return self._parsers
571 @parsers.setter
572 def parsers(self, new_parsers) -> None:
573 self._check_parsers(new_parsers)
574 self._parsers = new_parsers
576 @abstractmethod
577 def _check_parsers(self, parsers) -> None:
578 """Check the parsers are of correct type"""
580 def parse(self, lines) -> _RESULT:
581 """Execute the attached paresers, and return the parsed properties"""
582 properties = {}
583 for cursor, _ in enumerate(lines):
584 for parser in self.parsers:
585 # Check if any of the parsers can extract a property
586 # from this line Note: This will override any existing
587 # properties we found, if we found it previously. This
588 # is usually correct, as some VASP settings can cause
589 # certain pieces of information to be written multiple
590 # times during SCF. We are only interested in the
591 # final values within a given chunk.
592 if parser.has_property(cursor, lines):
593 prop = parser.parse(cursor, lines)
594 properties.update(prop)
595 return properties
598class ChunkParser(TypeParser, ABC):
599 def __init__(self, parsers, header=None):
600 super().__init__(parsers)
601 self.header = header
603 @property
604 def header(self) -> _HEADER:
605 return self._header
607 @header.setter
608 def header(self, value: Optional[_HEADER]) -> None:
609 self._header = value or {}
610 self.update_parser_headers()
612 def update_parser_headers(self) -> None:
613 """Apply the header to all available parsers"""
614 for parser in self.parsers:
615 parser.header = self.header
617 def _check_parsers(self,
618 parsers: Sequence[VaspChunkPropertyParser]) -> None:
619 """Check the parsers are of correct type 'VaspChunkPropertyParser'"""
620 if not all(
621 isinstance(parser, VaspChunkPropertyParser)
622 for parser in parsers):
623 raise TypeError(
624 'All parsers must be of type VaspChunkPropertyParser')
626 @abstractmethod
627 def build(self, lines: _CHUNK) -> Atoms:
628 """Construct an atoms object of the chunk from the parsed results"""
631class HeaderParser(TypeParser, ABC):
632 def _check_parsers(self,
633 parsers: Sequence[VaspHeaderPropertyParser]) -> None:
634 """Check the parsers are of correct type 'VaspHeaderPropertyParser'"""
635 if not all(
636 isinstance(parser, VaspHeaderPropertyParser)
637 for parser in parsers):
638 raise TypeError(
639 'All parsers must be of type VaspHeaderPropertyParser')
641 @abstractmethod
642 def build(self, lines: _CHUNK) -> _HEADER:
643 """Construct the header object from the parsed results"""
646class OutcarChunkParser(ChunkParser):
647 """Class for parsing a chunk of an OUTCAR."""
649 def __init__(self,
650 header: _HEADER = None,
651 parsers: Sequence[VaspChunkPropertyParser] = None):
652 global default_chunk_parsers
653 parsers = parsers or default_chunk_parsers.make_parsers()
654 super().__init__(parsers, header=header)
656 def build(self, lines: _CHUNK) -> Atoms:
657 """Apply outcar chunk parsers, and build an atoms object"""
658 self.update_parser_headers() # Ensure header is in sync
660 results = self.parse(lines)
661 symbols = self.header['symbols']
662 constraint = self.header.get('constraint', None)
664 atoms_kwargs = dict(symbols=symbols, constraint=constraint, pbc=True)
666 # Find some required properties in the parsed results.
667 # Raise ParseError if they are not present
668 for prop in ('positions', 'cell'):
669 try:
670 atoms_kwargs[prop] = results.pop(prop)
671 except KeyError:
672 raise ParseError(
673 'Did not find required property {} during parse.'.format(
674 prop))
675 atoms = Atoms(**atoms_kwargs)
677 kpts = results.pop('kpts', None)
678 calc = SinglePointDFTCalculator(atoms, **results)
679 if kpts is not None:
680 calc.kpts = kpts
681 calc.name = 'vasp'
682 atoms.calc = calc
683 return atoms
686class OutcarHeaderParser(HeaderParser):
687 """Class for parsing a chunk of an OUTCAR."""
689 def __init__(self,
690 parsers: Sequence[VaspHeaderPropertyParser] = None,
691 workdir: Union[str, PurePath] = None):
692 global default_header_parsers
693 parsers = parsers or default_header_parsers.make_parsers()
694 super().__init__(parsers)
695 self.workdir = workdir
697 @property
698 def workdir(self):
699 return self._workdir
701 @workdir.setter
702 def workdir(self, value):
703 if value is not None:
704 value = Path(value)
705 self._workdir = value
707 def _build_symbols(self, results: _RESULT) -> Sequence[str]:
708 if 'symbols' in results:
709 # Safeguard, in case a different parser already
710 # did this. Not currently available in a default parser
711 return results.pop('symbols')
713 # Build the symbols of the atoms
714 for required_key in ('ion_types', 'species'):
715 if required_key not in results:
716 raise ParseError(
717 'Did not find required key "{}" in parsed header results.'.
718 format(required_key))
720 ion_types = results.pop('ion_types')
721 species = results.pop('species')
722 if len(ion_types) != len(species):
723 raise ParseError(
724 ('Expected length of ion_types to be same as species, '
725 'but got ion_types={} and species={}').format(
726 len(ion_types), len(species)))
728 # Expand the symbols list
729 symbols = []
730 for n, sym in zip(ion_types, species):
731 symbols.extend(n * [sym])
732 return symbols
734 def _get_constraint(self):
735 """Try and get the constraints from the POSCAR of CONTCAR
736 since they aren't located in the OUTCAR, and thus we cannot construct an
737 OUTCAR parser which does this.
738 """
739 constraint = None
740 if self.workdir is not None:
741 constraint = read_constraints_from_file(self.workdir)
742 return constraint
744 def build(self, lines: _CHUNK) -> _RESULT:
745 """Apply the header parsers, and build the header"""
746 results = self.parse(lines)
748 # Get the symbols from the parsed results
749 # will pop the keys which we use for that purpose
750 symbols = self._build_symbols(results)
751 natoms = len(symbols)
753 constraint = self._get_constraint()
755 # Remaining results from the parse goes into the header
756 header = dict(symbols=symbols,
757 natoms=natoms,
758 constraint=constraint,
759 **results)
760 return header
763class OUTCARChunk(ImageChunk):
764 """Container class for a chunk of the OUTCAR which consists of a
765 self-contained SCF step, i.e. and image. Also contains the header_data
766 """
768 def __init__(self,
769 lines: _CHUNK,
770 header: _HEADER,
771 parser: ChunkParser = None):
772 super().__init__()
773 self.lines = lines
774 self.header = header
775 self.parser = parser or OutcarChunkParser()
777 def build(self):
778 self.parser.header = self.header # Ensure header is syncronized
779 return self.parser.build(self.lines)
782def build_header(fd: TextIO) -> _CHUNK:
783 """Build a chunk containing the header data"""
784 lines = []
785 for line in fd:
786 lines.append(line)
787 if 'Iteration' in line:
788 # Start of SCF cycle
789 return lines
791 # We never found the SCF delimiter, so the OUTCAR must be incomplete
792 raise ParseError('Incomplete OUTCAR')
795def build_chunk(fd: TextIO) -> _CHUNK:
796 """Build chunk which contains 1 complete atoms object"""
797 lines = []
798 while True:
799 line = next(fd)
800 lines.append(line)
801 if _OUTCAR_SCF_DELIM in line:
802 # Add 4 more lines to include energy
803 for _ in range(4):
804 lines.append(next(fd))
805 break
806 return lines
809def outcarchunks(fd: TextIO,
810 chunk_parser: ChunkParser = None,
811 header_parser: HeaderParser = None) -> Iterator[OUTCARChunk]:
812 """Function to build chunks of OUTCAR from a file stream"""
813 name = Path(fd.name)
814 workdir = name.parent
816 # First we get header info
817 # pass in the workdir from the fd, so we can try and get the constraints
818 header_parser = header_parser or OutcarHeaderParser(workdir=workdir)
820 lines = build_header(fd)
821 header = header_parser.build(lines)
822 assert isinstance(header, dict)
824 chunk_parser = chunk_parser or OutcarChunkParser()
826 while True:
827 try:
828 lines = build_chunk(fd)
829 except StopIteration:
830 # End of file
831 return
832 yield OUTCARChunk(lines, header, parser=chunk_parser)
835# Create the default chunk parsers
836default_chunk_parsers = DefaultParsersContainer(
837 Cell,
838 PositionsAndForces,
839 Stress,
840 Magmoms,
841 Magmom,
842 EFermi,
843 Kpoints,
844 Energy,
845)
847# Create the default header parsers
848default_header_parsers = DefaultParsersContainer(
849 SpeciesTypes,
850 IonsPerSpecies,
851 Spinpol,
852 KpointHeader,
853)