Source code for gpaw.io.log_file_reader

"""GPAW's log-file parser.

Example:

>>> d = parse('''
... a: 117
... b: hello
...
... Indented block:
...   a: (1, 2)
...
... Table
... |ignored|header|  |
... |-------|------|--|
... |a      |1     |2 |
... |b      |3     |4 |
...
... ''')
>>> for k, v in d[0].items():
...     print(k, v)
a 117
b hello
indented_block {'a': [1, 2]}
table [['a', 1, 2], ['b', 3, 4]]
"""
import re
from collections.abc import Generator, Iterable, Iterator
from pathlib import Path
from typing import Any


[docs] def parse(lines: Iterable[str] | str, keys: set[str] | None = None) -> list[dict]: """Parse GPAW's log-file to list of dicts. One dict for each atomic configuration. """ if isinstance(lines, str): lines = lines.splitlines() dicts: list[dict] = [] _parse(line_iter(lines), dicts=dicts, keys=keys) return dicts
[docs] def parse_file(filename: str | Path, keys: set[str] | None = None) -> list[dict]: with open(filename) as fd: return parse(fd, keys=keys)
def normalize_key(key: str) -> str: return key.lower().replace(' ', '_').replace('-', '_') class ConvergedFloat(float): """Special float like "-8.4c" ("c" for converged).""" def line_iter(lines: Iterable[str]) -> Generator[str, None, None]: """Yield lines from line-generator. Lines are stripped for leading and trailing spaces. Special 'DEDENT' lines are yielded everytime an indented block of lines stops. Indented blocks begin after lines ending with a colon. """ indent = 0 for rawline in lines: rawline = rawline.split('#')[0].rstrip() line = rawline.lstrip() if line: i = len(rawline) - len(line) if i > indent: continue while i < indent: yield 'DEDENT' indent -= 2 if line.endswith(':'): indent += 2 yield line while indent >= 0: yield 'DEDENT' indent -= 2 def parse_str(s: str) -> Any: """Convert str to int, float or str or list any of those or ... >>> parse_str('1.2') 1.2 >>> parse_str('AB') 'AB' >>> parse_str('( 1, 1.2, AB)') [1, 1.2, 'AB'] """ if s == '-': return float('nan') if ',' in s: s = re.sub(r',\s+', ',', s) s = re.sub(r'\(\s+', '(', s) if s.startswith('(('): return [parse_str(x) for x in s[2:-2].split('),(')] if s.startswith('('): return [parse_str(x) for x in s[1:-1].split(',')] return [parse_str(x) for x in s.split(',')] if s.startswith('\x1b'): s = re.sub(r'\x1b\[\d+m', '', s) # remove color escape-sequences if s.endswith('c'): try: x = float(s[:-1]) except ValueError: return s return ConvergedFloat(x) if len(s) == 8 and ':' in s: ho, mi, se = (int(x) for x in s.split(':')) return (ho * 60 + mi) * 60 + se try: return int(s) except ValueError: pass try: return float(s) except ValueError: pass return s def table(lines: list[str]) -> list[list]: """Parse table lines. >>> table(['| 1 |1.1 |a|', ... '| 2 |2.2 |b|', ... '|...| | |', ... '| 9 |9.01|c|']) [[1, 1.1, 'a'], [2, 2.2, 'b'], [9, 9.01, 'c']] """ rows = [] for line in lines: parts = line[1:-1].split('|') if parts[0].strip() == '...': continue rows.append([parse_str(x.strip()) for x in parts]) return rows SPECIAL = {'forces'} def _parse(lines: Iterator[str], indents: int = 0, dicts: list[dict] | None = None, keys: set[str] | None = None) -> dict: dct: dict[str, Any] = {} key = None while (line := next(lines)) != 'DEDENT': value: Any if line.endswith(':'): # indented block: key = normalize_key(line[:-1]) if keys is None or key in keys: value = _parse(lines, indents + 1) else: # skip parsing: i = 1 while i: line = next(lines) if line == 'DEDENT': i -= 1 elif line.endswith(':'): i += 1 key = None continue elif ':' in line: # "key: value" pair: key, _, val = line.partition(':') key = normalize_key(key) if keys is None or key in keys: value = parse_str(val.strip()) else: # skip parsing: key = None continue elif line.startswith('|') and key is not None: # table: key = normalize_key(key) while not next(lines).startswith('|-'): pass rows = [] while (line := next(lines)).startswith('|'): rows.append(line) if keys is None or key in keys: value = table(rows) else: # skip parsing: key = None continue else: key = line continue if dicts is not None and key in dct and key not in SPECIAL: dicts.append(dct) dct = {} if re.match('[a-zA-Z_]+', key): dct[key] = value key = None if dicts is not None: dicts.append(dct) return dct def main(): import argparse import pprint parser = argparse.ArgumentParser() parser.add_argument('filename') parser.add_argument('key', nargs='*') args = parser.parse_args() blocks = parse_file(args.filename, keys=set(args.key) if args.key else None) print('Blocks:', len(blocks)) for n, dct in enumerate(blocks): print(n, '=' * 70) pprint.pp(dct) print('=' * 70) if __name__ == '__main__': main()