# SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
#
# SPDX-License-Identifier: MIT
"""Grammar-oriented extractors for text-heavy ORCA output sections.
Prefer structured backends (OPI) when available. This module covers sections
that remain text dumps: final single-point energies and Cartesian geometry
blocks. Foundation for VPT2/IR/populations ports from wailord.
```{versionadded} 1.9.9
```
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from chemparseplot.parse.grammar._deps import (
get_grammar_class,
get_node_visitor_class,
)
from chemparseplot.units import Q_
# Named rules are real expressions (not aliases) so visitors fire reliably.
_ENERGY_LINE_GRAMMAR_SRC = r"""
energy_line = label ws value ws?
label = "FINAL SINGLE POINT ENERGY"
value = ~r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?"
ws = ~r"[ \t]+"
"""
_CART_BLOCK_GRAMMAR_SRC = r"""
cart_block = header newline dashes newline (atom_line newline)+
header = "CARTESIAN COORDINATES (ANGSTROEM)"
dashes = ~r"-{3,}"
atom_line = ws? atype ws float_ ws float_ ws float_ ws?
atype = ~r"[A-Za-z][a-zA-Z0-9]{0,2}"
float_ = ~r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?"
newline = ~r"\n"
ws = ~r"[ \t]+"
"""
_energy_grammar: Any | None = None
_cart_grammar: Any | None = None
_ENERGY_LINE_RE = re.compile(
r"^.*FINAL SINGLE POINT ENERGY[ \t]+"
r"([+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?).*$",
re.MULTILINE,
)
_CART_BLOCK_RE = re.compile(
r"CARTESIAN COORDINATES \(ANGSTROEM\)\n"
r"-{3,}\n"
r"((?:[ \t]*[A-Za-z][a-zA-Z0-9]{0,2}[ \t]+"
r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?[ \t]+"
r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?[ \t]+"
r"[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[eE][+-]?\d+)?[ \t]*\n)+)",
re.MULTILINE,
)
_N_ATOMS_RE = re.compile(
r"Number of atoms\s*(?:\.{3,}|\.\.\.)\s*(\d+)",
)
[docs]
def _energy_g() -> Any:
global _energy_grammar
if _energy_grammar is None:
Grammar = get_grammar_class()
_energy_grammar = Grammar(_ENERGY_LINE_GRAMMAR_SRC)
return _energy_grammar
[docs]
def _cart_g() -> Any:
global _cart_grammar
if _cart_grammar is None:
Grammar = get_grammar_class()
_cart_grammar = Grammar(_CART_BLOCK_GRAMMAR_SRC)
return _cart_grammar
[docs]
def _flatten(xs: Any) -> list[Any]:
out: list[Any] = []
if isinstance(xs, list):
for x in xs:
out.extend(_flatten(x))
elif xs is not None and xs != "":
out.append(xs)
return out
[docs]
@dataclass(frozen=True, slots=True)
class OrcaAtomCoord:
"""One atom in an ORCA Cartesian (Å) block."""
symbol: str
x: float
y: float
z: float
[docs]
@dataclass(frozen=True, slots=True)
class OrcaTextSummary:
"""Structured slice of text ORCA output."""
energies_hartree: tuple[float, ...]
last_geometry: tuple[OrcaAtomCoord, ...]
n_atoms: int | None
@property
def final_energy_hartree(self) -> float | None:
if not self.energies_hartree:
return None
return self.energies_hartree[-1]
@property
def final_energy(self) -> Any:
"""Last final single-point energy as a pint Quantity (hartree)."""
e = self.final_energy_hartree
if e is None:
return Q_([], "hartree")
return Q_(e, "hartree")
[docs]
def parse_orca_text_summary(text: str) -> OrcaTextSummary:
"""Summarize final energies and last Cartesian geometry from ORCA text."""
energies = extract_final_energies_hartree(text)
blocks = extract_cartesian_angstrom_blocks(text)
last_geom: tuple[OrcaAtomCoord, ...] = ()
if blocks:
last_geom = tuple(blocks[-1])
return OrcaTextSummary(
energies_hartree=tuple(energies),
last_geometry=last_geom,
n_atoms=extract_n_atoms(text),
)
[docs]
def parse_orca_text_file(path: str | Path) -> OrcaTextSummary:
"""Read an ORCA ``.out`` and return :class:`OrcaTextSummary`."""
return parse_orca_text_summary(Path(path).read_text())