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