Source code for httk.atomistic.integrations.vasp.io.oszicar

"""String-preserving reader for VASP OSZICAR files.

Electronic iterations are attached to the following ionic summary, as VASP
writes them. If the file ends before that summary, a final entry with
``n``, ``F``, ``E0``, ``dE``, and ``mag`` all set to ``None`` preserves the
trailing electronic block.
"""

import re
from typing import Any

from ._text import source_lines

_COLON_LINE = re.compile(r"^\s*([A-Za-z][A-Za-z0-9_-]*)\s*:\s*(.*)$")
_IONIC_START = re.compile(r"^\s*(\d+)\b")


def _issue(issues: list[str], lineno: int, message: str) -> None:
    issues.append(f"line {lineno}: {message}")


def _value(line: str, key: str) -> str | None:
    match = re.search(rf"\b{key}\s*=\s*(\S+)", line)
    return match.group(1) if match else None


def _electronic(line: str) -> dict[str, Any] | None:
    match = _COLON_LINE.match(line)
    if match is None:
        return None
    scheme, rest = match.groups()
    fields = rest.split()
    if len(fields) not in (6, 7) or not fields[0].isdigit():
        return None
    return {
        "scheme": scheme,
        "n": int(fields[0]),
        "E": fields[1],
        "dE": fields[2],
        "d_eps": fields[3],
        "ncg": fields[4],
        "rms": fields[5],
        "rms_c": fields[6] if len(fields) == 7 else None,
    }


[docs] def read_oszicar(source: Any) -> dict[str, Any]: """Read OSZICAR text without converting numeric lexemes. Electronic iterations are attached to the following ionic summary. A trailing electronic block is retained as an incomplete final entry when no summary follows it. :param source: OSZICAR filename, text stream, or iterable of source lines. :return: A neutral payload containing ionic steps and parsing issues. """ ionic_steps: list[dict[str, Any]] = [] pending: list[dict[str, Any]] = [] pending_start: int | None = None pending_last: int | None = None issues: list[str] = [] with source_lines(source) as (lines, _raw): for lineno, line in enumerate(lines, 1): stripped = line.strip() if not stripped or (stripped.startswith("N ") and "d eps" in stripped): continue ionic_match = _IONIC_START.match(line) if ionic_match: n = int(ionic_match.group(1)) boundary_pending = pending boundary_start = pending_start boundary_last = pending_last pending = [] pending_start = None pending_last = None values = {key: _value(line, key) for key in ("F", "E0")} d_e = re.search(r"\bd\s*E\s*=\s*(\S+)", line) values["dE"] = _value(line, "dE") or (d_e.group(1) if d_e else None) # MD summaries commonly omit dE while retaining T/E/F/E0. if any(values[key] is None for key in ("F", "E0")): if boundary_pending and boundary_start is not None and boundary_last is not None: _issue( issues, lineno, f"malformed ionic summary; dropped electronic iterations from lines " f"{boundary_start}-{boundary_last}", ) else: _issue(issues, lineno, "malformed ionic summary") continue ionic_steps.append( { "n": n, "F": values["F"], "E0": values["E0"], "dE": values["dE"], "mag": _value(line, "mag"), "electronic": boundary_pending, } ) pending = [] continue if _COLON_LINE.match(line): electronic = _electronic(line) if electronic is None: _issue(issues, lineno, "malformed electronic iteration") else: pending.append(electronic) if pending_start is None: pending_start = lineno pending_last = lineno continue _issue(issues, lineno, "unrecognized line") if pending: ionic_steps.append({"n": None, "F": None, "E0": None, "dE": None, "mag": None, "electronic": pending}) return {"format": "vasp-oszicar", "ionic_steps": ionic_steps, "issues": issues}