Coverage for /builds/alexhroom/ase/ase/io/vasp_parsers/vasp_outcar_parsers.py: 95.24%
462 statements
« prev ^ index » next coverage.py v7.5.3, created at 2024-08-05 14:37 +0000
« prev ^ index » next coverage.py v7.5.3, created at 2024-08-05 14:37 +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 magmom = np.array(list(map(float, magmom_lst)))
400 else:
401 magmom = float(magmom_lst[0])
402 return {'magmom': magmom}
405class Magmoms(VaspChunkPropertyParser):
406 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool:
407 line = lines[cursor]
408 if 'magnetization (x)' in line:
409 natoms = self.get_from_header('natoms')
410 self.non_collinear = False
411 if cursor + natoms + 9 < len(lines):
412 line_y = self.get_line(cursor + natoms + 9, lines)
413 if 'magnetization (y)' in line_y:
414 self.non_collinear = True
415 return True
416 return False
418 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
420 natoms = self.get_from_header('natoms')
421 if self.non_collinear:
422 magmoms = np.zeros((natoms, 3))
423 nskip = 4 # Skip some lines
424 for i in range(natoms):
425 line = self.get_line(cursor + i + nskip, lines)
426 magmoms[i, 0] = float(line.split()[-1])
427 nskip = natoms + 13 # Skip some lines
428 for i in range(natoms):
429 line = self.get_line(cursor + i + nskip, lines)
430 magmoms[i, 1] = float(line.split()[-1])
431 nskip = 2 * natoms + 22 # Skip some lines
432 for i in range(natoms):
433 line = self.get_line(cursor + i + nskip, lines)
434 magmoms[i, 2] = float(line.split()[-1])
435 else:
436 magmoms = np.zeros(natoms)
437 nskip = 4 # Skip some lines
438 for i in range(natoms):
439 line = self.get_line(cursor + i + nskip, lines)
440 magmoms[i] = float(line.split()[-1])
442 return {'magmoms': magmoms}
445class EFermi(SimpleVaspChunkParser):
446 LINE_DELIMITER = 'E-fermi :'
448 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
449 line = self.get_line(cursor, lines)
450 parts = line.split()
451 efermi = float(parts[2])
452 return {'efermi': efermi}
455class Energy(SimpleVaspChunkParser):
456 LINE_DELIMITER = _OUTCAR_SCF_DELIM
458 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
459 nskip = 2
460 line = self.get_line(cursor + nskip, lines)
461 parts = line.strip().split()
462 energy_free = float(parts[4]) # Force consistent
464 nskip = 4
465 line = self.get_line(cursor + nskip, lines)
466 parts = line.strip().split()
467 energy_zero = float(parts[6]) # Extrapolated to 0 K
469 return {'free_energy': energy_free, 'energy': energy_zero}
472class Kpoints(VaspChunkPropertyParser):
473 def has_property(self, cursor: _CURSOR, lines: _CHUNK) -> bool:
474 line = lines[cursor]
475 # Example line:
476 # " spin component 1" or " spin component 2"
477 # We only check spin up, as if we are spin-polarized, we'll parse that
478 # as well
479 if 'spin component 1' in line:
480 parts = line.strip().split()
481 # This string is repeated elsewhere, but not with this exact shape
482 if len(parts) == 3:
483 try:
484 # The last part of te line should be an integer, denoting
485 # spin-up or spin-down
486 int(parts[-1])
487 except ValueError:
488 pass
489 else:
490 return True
491 return False
493 def parse(self, cursor: _CURSOR, lines: _CHUNK) -> _RESULT:
494 nkpts = self.get_from_header('nkpts')
495 nbands = self.get_from_header('nbands')
496 weights = self.get_from_header('kpt_weights')
497 spinpol = self.get_from_header('spinpol')
498 nspins = 2 if spinpol else 1
500 kpts = []
501 for spin in range(nspins):
502 # for Vasp 6, they added some extra information after the
503 # spin components. so we might need to seek the spin
504 # component line
505 cursor = search_lines(f'spin component {spin + 1}', cursor, lines)
507 cursor += 2 # Skip two lines
508 for _ in range(nkpts):
509 # Skip empty lines
510 cursor = find_next_non_empty_line(cursor, lines)
512 line = self.get_line(cursor, lines)
513 # Example line:
514 # "k-point 1 : 0.0000 0.0000 0.0000"
515 parts = line.strip().split()
516 ikpt = int(parts[1]) - 1 # Make kpt idx start from 0
517 weight = weights[ikpt]
519 cursor += 2 # Move down two
520 eigenvalues = np.zeros(nbands)
521 occupations = np.zeros(nbands)
522 for n in range(nbands):
523 # Example line:
524 # " 1 -9.9948 1.00000"
525 parts = lines[cursor].strip().split()
526 eps_n, f_n = map(float, parts[1:])
527 occupations[n] = f_n
528 eigenvalues[n] = eps_n
529 cursor += 1
530 kpt = SinglePointKPoint(weight,
531 spin,
532 ikpt,
533 eps_n=eigenvalues,
534 f_n=occupations)
535 kpts.append(kpt)
537 return {'kpts': kpts}
540class DefaultParsersContainer:
541 """Container for the default OUTCAR parsers.
542 Allows for modification of the global default parsers.
544 Takes in an arbitrary number of parsers.
545 The parsers should be uninitialized,
546 as they are created on request.
547 """
549 def __init__(self, *parsers_cls):
550 self._parsers_dct = {}
551 for parser in parsers_cls:
552 self.add_parser(parser)
554 @property
555 def parsers_dct(self) -> dict:
556 return self._parsers_dct
558 def make_parsers(self):
559 """Return a copy of the internally stored parsers.
560 Parsers are created upon request."""
561 return [parser() for parser in self.parsers_dct.values()]
563 def remove_parser(self, name: str):
564 """Remove a parser based on the name.
565 The name must match the parser name exactly."""
566 self.parsers_dct.pop(name)
568 def add_parser(self, parser) -> None:
569 """Add a parser"""
570 self.parsers_dct[parser.get_name()] = parser
573class TypeParser(ABC):
574 """Base class for parsing a type, e.g. header or chunk,
575 by applying the internal attached parsers"""
577 def __init__(self, parsers):
578 self.parsers = parsers
580 @property
581 def parsers(self):
582 return self._parsers
584 @parsers.setter
585 def parsers(self, new_parsers) -> None:
586 self._check_parsers(new_parsers)
587 self._parsers = new_parsers
589 @abstractmethod
590 def _check_parsers(self, parsers) -> None:
591 """Check the parsers are of correct type"""
593 def parse(self, lines) -> _RESULT:
594 """Execute the attached paresers, and return the parsed properties"""
595 properties = {}
596 for cursor, _ in enumerate(lines):
597 for parser in self.parsers:
598 # Check if any of the parsers can extract a property
599 # from this line Note: This will override any existing
600 # properties we found, if we found it previously. This
601 # is usually correct, as some VASP settings can cause
602 # certain pieces of information to be written multiple
603 # times during SCF. We are only interested in the
604 # final values within a given chunk.
605 if parser.has_property(cursor, lines):
606 prop = parser.parse(cursor, lines)
607 properties.update(prop)
608 return properties
611class ChunkParser(TypeParser, ABC):
612 def __init__(self, parsers, header=None):
613 super().__init__(parsers)
614 self.header = header
616 @property
617 def header(self) -> _HEADER:
618 return self._header
620 @header.setter
621 def header(self, value: Optional[_HEADER]) -> None:
622 self._header = value or {}
623 self.update_parser_headers()
625 def update_parser_headers(self) -> None:
626 """Apply the header to all available parsers"""
627 for parser in self.parsers:
628 parser.header = self.header
630 def _check_parsers(self,
631 parsers: Sequence[VaspChunkPropertyParser]) -> None:
632 """Check the parsers are of correct type 'VaspChunkPropertyParser'"""
633 if not all(
634 isinstance(parser, VaspChunkPropertyParser)
635 for parser in parsers):
636 raise TypeError(
637 'All parsers must be of type VaspChunkPropertyParser')
639 @abstractmethod
640 def build(self, lines: _CHUNK) -> Atoms:
641 """Construct an atoms object of the chunk from the parsed results"""
644class HeaderParser(TypeParser, ABC):
645 def _check_parsers(self,
646 parsers: Sequence[VaspHeaderPropertyParser]) -> None:
647 """Check the parsers are of correct type 'VaspHeaderPropertyParser'"""
648 if not all(
649 isinstance(parser, VaspHeaderPropertyParser)
650 for parser in parsers):
651 raise TypeError(
652 'All parsers must be of type VaspHeaderPropertyParser')
654 @abstractmethod
655 def build(self, lines: _CHUNK) -> _HEADER:
656 """Construct the header object from the parsed results"""
659class OutcarChunkParser(ChunkParser):
660 """Class for parsing a chunk of an OUTCAR."""
662 def __init__(self,
663 header: _HEADER = None,
664 parsers: Sequence[VaspChunkPropertyParser] = None):
665 global default_chunk_parsers
666 parsers = parsers or default_chunk_parsers.make_parsers()
667 super().__init__(parsers, header=header)
669 def build(self, lines: _CHUNK) -> Atoms:
670 """Apply outcar chunk parsers, and build an atoms object"""
671 self.update_parser_headers() # Ensure header is in sync
673 results = self.parse(lines)
674 symbols = self.header['symbols']
675 constraint = self.header.get('constraint', None)
677 atoms_kwargs = dict(symbols=symbols, constraint=constraint, pbc=True)
679 # Find some required properties in the parsed results.
680 # Raise ParseError if they are not present
681 for prop in ('positions', 'cell'):
682 try:
683 atoms_kwargs[prop] = results.pop(prop)
684 except KeyError:
685 raise ParseError(
686 'Did not find required property {} during parse.'.format(
687 prop))
688 atoms = Atoms(**atoms_kwargs)
690 kpts = results.pop('kpts', None)
691 calc = SinglePointDFTCalculator(atoms, **results)
692 if kpts is not None:
693 calc.kpts = kpts
694 calc.name = 'vasp'
695 atoms.calc = calc
696 return atoms
699class OutcarHeaderParser(HeaderParser):
700 """Class for parsing a chunk of an OUTCAR."""
702 def __init__(self,
703 parsers: Sequence[VaspHeaderPropertyParser] = None,
704 workdir: Union[str, PurePath] = None):
705 global default_header_parsers
706 parsers = parsers or default_header_parsers.make_parsers()
707 super().__init__(parsers)
708 self.workdir = workdir
710 @property
711 def workdir(self):
712 return self._workdir
714 @workdir.setter
715 def workdir(self, value):
716 if value is not None:
717 value = Path(value)
718 self._workdir = value
720 def _build_symbols(self, results: _RESULT) -> Sequence[str]:
721 if 'symbols' in results:
722 # Safeguard, in case a different parser already
723 # did this. Not currently available in a default parser
724 return results.pop('symbols')
726 # Build the symbols of the atoms
727 for required_key in ('ion_types', 'species'):
728 if required_key not in results:
729 raise ParseError(
730 'Did not find required key "{}" in parsed header results.'.
731 format(required_key))
733 ion_types = results.pop('ion_types')
734 species = results.pop('species')
735 if len(ion_types) != len(species):
736 raise ParseError(
737 ('Expected length of ion_types to be same as species, '
738 'but got ion_types={} and species={}').format(
739 len(ion_types), len(species)))
741 # Expand the symbols list
742 symbols = []
743 for n, sym in zip(ion_types, species):
744 symbols.extend(n * [sym])
745 return symbols
747 def _get_constraint(self):
748 """Try and get the constraints from the POSCAR of CONTCAR
749 since they aren't located in the OUTCAR, and thus we cannot construct an
750 OUTCAR parser which does this.
751 """
752 constraint = None
753 if self.workdir is not None:
754 constraint = read_constraints_from_file(self.workdir)
755 return constraint
757 def build(self, lines: _CHUNK) -> _RESULT:
758 """Apply the header parsers, and build the header"""
759 results = self.parse(lines)
761 # Get the symbols from the parsed results
762 # will pop the keys which we use for that purpose
763 symbols = self._build_symbols(results)
764 natoms = len(symbols)
766 constraint = self._get_constraint()
768 # Remaining results from the parse goes into the header
769 header = dict(symbols=symbols,
770 natoms=natoms,
771 constraint=constraint,
772 **results)
773 return header
776class OUTCARChunk(ImageChunk):
777 """Container class for a chunk of the OUTCAR which consists of a
778 self-contained SCF step, i.e. and image. Also contains the header_data
779 """
781 def __init__(self,
782 lines: _CHUNK,
783 header: _HEADER,
784 parser: ChunkParser = None):
785 super().__init__()
786 self.lines = lines
787 self.header = header
788 self.parser = parser or OutcarChunkParser()
790 def build(self):
791 self.parser.header = self.header # Ensure header is syncronized
792 return self.parser.build(self.lines)
795def build_header(fd: TextIO) -> _CHUNK:
796 """Build a chunk containing the header data"""
797 lines = []
798 for line in fd:
799 lines.append(line)
800 if 'Iteration' in line:
801 # Start of SCF cycle
802 return lines
804 # We never found the SCF delimiter, so the OUTCAR must be incomplete
805 raise ParseError('Incomplete OUTCAR')
808def build_chunk(fd: TextIO) -> _CHUNK:
809 """Build chunk which contains 1 complete atoms object"""
810 lines = []
811 while True:
812 line = next(fd)
813 lines.append(line)
814 if _OUTCAR_SCF_DELIM in line:
815 # Add 4 more lines to include energy
816 for _ in range(4):
817 lines.append(next(fd))
818 break
819 return lines
822def outcarchunks(fd: TextIO,
823 chunk_parser: ChunkParser = None,
824 header_parser: HeaderParser = None) -> Iterator[OUTCARChunk]:
825 """Function to build chunks of OUTCAR from a file stream"""
826 name = Path(fd.name)
827 workdir = name.parent
829 # First we get header info
830 # pass in the workdir from the fd, so we can try and get the constraints
831 header_parser = header_parser or OutcarHeaderParser(workdir=workdir)
833 lines = build_header(fd)
834 header = header_parser.build(lines)
835 assert isinstance(header, dict)
837 chunk_parser = chunk_parser or OutcarChunkParser()
839 while True:
840 try:
841 lines = build_chunk(fd)
842 except StopIteration:
843 # End of file
844 return
845 yield OUTCARChunk(lines, header, parser=chunk_parser)
848# Create the default chunk parsers
849default_chunk_parsers = DefaultParsersContainer(
850 Cell,
851 PositionsAndForces,
852 Stress,
853 Magmoms,
854 Magmom,
855 EFermi,
856 Kpoints,
857 Energy,
858)
860# Create the default header parsers
861default_header_parsers = DefaultParsersContainer(
862 SpeciesTypes,
863 IonsPerSpecies,
864 Spinpol,
865 KpointHeader,
866)