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