# SPDX-FileCopyrightText: 2023-present Rohit Goswami <rog32@hi.is>
#
# SPDX-License-Identifier: MIT
"""Shared helpers for eOn job-config authorship.
Cookbook context:
https://atomistic-cookbook.org/examples/eon-pet-neb/eon-pet-neb.html
.. versionchanged:: 1.9.x
``write_eon_config`` routes through ``eon-schema`` INI helpers (no eon-akmc).
"""
from __future__ import annotations
import logging
from collections.abc import Mapping
from pathlib import Path
from typing import Any
[docs]
def write_eon_config(
path: str | Path,
settings: Mapping[str, Mapping[str, Any]],
*,
validate: bool = False,
) -> Path:
"""Write a ``config.ini`` for eOn from a nested section dict.
Uses :func:`eon_schema.config.write_ini` (case-preserving keys, lowercase
bools). Optional *validate* flags unknown keys on L0-covered sections via
:func:`eon_schema.config.unknown_ini_keys` (pot-specific sections such as
``SocketNWChemPot`` are not flagged).
Parameters
----------
path:
File path or directory (writes ``config.ini`` inside a directory).
settings:
``{section: {key: value}}`` map.
validate:
If True, raise :class:`ValueError` when covered L0 sections contain
unknown option names.
Returns
-------
pathlib.Path
Path to the written ``config.ini``.
.. versionadded:: 0.1.0
.. versionchanged:: 1.9.x
Implemented with eon-schema; added *validate*.
"""
try:
from eon_schema.config import unknown_ini_keys, write_ini
except ImportError as exc: # pragma: no cover
raise ImportError(
"write_eon_config requires eon-schema>=0.2.\n"
" pip install 'rgpycrumbs[eon]'\n"
" # or: pip install 'eon-schema>=0.2'\n"
" # uv: uv pip install 'eon-schema>=0.2'"
) from exc
if validate:
bad = unknown_ini_keys(settings, covered_only=True)
if bad:
raise ValueError(f"unknown INI keys for covered L0 sections: {bad}")
out = write_ini(path, settings)
print(f"Wrote eOn config to '{out}'")
return out
[docs]
log = logging.getLogger(__name__)
#: neb.con frame metadata keys, in the column order the legacy neb_*.dat had.
[docs]
_PROFILE_KEYS = ("reaction_coordinate", "relative_energy", "parallel_force")
[docs]
def load_profile_rows(path: Path, mass_weighted: bool = False):
"""One NEB step as rows ``[image, rc, energy, parallel_force, eigenvalue]``.
A ``.con`` band (``neb.con``, ``neb_path_NNN.con``) is read through readcon
from its frame metadata, where eOn writes the profile, so no column order
is assumed. With ``mass_weighted`` the coordinate is the frames'
``reaction_coordinate_mw``. A legacy ``neb_*.dat`` is read by column, and
has no mass-weighted coordinate.
"""
import numpy as np
path = Path(path)
if path.suffix != ".con":
if mass_weighted:
log.warning(
"%s has no mass-weighted coordinate; using the path one", path.name
)
return np.loadtxt(path, skiprows=1).T
from readcon import read_con
rows = []
for i, frame in enumerate(read_con(str(path))):
md = frame.metadata
missing = [k for k in _PROFILE_KEYS if k not in md]
if missing:
msg = f"{path.name} frame {i} lacks {', '.join(missing)}"
raise ValueError(msg)
rc_key = "reaction_coordinate_mw" if mass_weighted else "reaction_coordinate"
if rc_key not in md:
msg = f"{path.name} frame {i} lacks {rc_key}; eOn writes it from 3.4"
raise ValueError(msg)
rows.append(
[
float(md.get("neb_bead", i)),
float(md[rc_key]),
float(md["relative_energy"]),
float(md["parallel_force"]),
float(md.get("lowest_eigenvalue", np.nan)),
]
)
return np.asarray(rows).T