Source code for atom_hifi.training.qho_sigma

"""QHO-backed sigma_thermal lookup for runtime ε_noise calibration."""
from __future__ import annotations

import json
import logging
import math
from functools import lru_cache
from importlib import resources

from ase.data import atomic_masses, atomic_numbers

from atom_hifi.training import calibration as _calibration

logger = logging.getLogger(__name__)

_AMU_KG = 1.66053906660e-27
_ANGSTROM_M = 1e-10
_EV_J = 1.602176634e-19
_HBAR = 1.054571817e-34
_KB = 1.380649e-23
_THIN_N_MATERIALS = 10
_NO_OVERRIDE = object()


[docs] @lru_cache(maxsize=1) def load_qho_table() -> dict: """Load packaged QHO frequency table. Default is the msq/kappa_stiff (stretch-mode, second-moment) estimator, not the centroid (full-DOS average) estimator — see ADR-0010: clean chemical-motif identity under centroid does not imply MLIP-training- adequate granularity, so the default now shares the stiffer moment used by the opt-in force-truth (`microstate.reference: f_tol`) path. """ path = resources.files("atom_hifi").joinpath( "data/qho_freq_table_v1_kappa_msq.json") with path.open(encoding="utf-8") as fh: return json.load(fh)
def _omega_eff_meV(entry: dict) -> float: """Effective phonon energy (meV) from a table entry, estimator-agnostic.""" omega = entry.get("omega_eff_meV", entry.get("omega_stiff_meV")) if omega is None: raise KeyError("table entry has neither 'omega_eff_meV' nor 'omega_stiff_meV'") return float(omega)
[docs] def qho_table_version() -> str: """Version tag of the packaged QHO table (``_meta.version``; ``'unknown'`` if absent). Saved in ``eps_noise_raw.npz`` and compared by the runner's ε-cache check when ``sigma_source`` is ``qho_table``. """ return str(load_qho_table().get("_meta", {}).get("version", "unknown"))
[docs] def qho_sigma_from_omega_mev(element: str, omega_eff_meV: float, T: float) -> float: """Return QHO RMS displacement σ (Å) for an element/frequency/temperature.""" if element not in atomic_numbers: raise ValueError(f"Unknown element symbol: {element!r}") if omega_eff_meV <= 0: raise ValueError(f"omega_eff_meV must be positive, got {omega_eff_meV!r}") if T <= 0: raise ValueError(f"Temperature must be positive, got {T!r}") m_kg = atomic_masses[atomic_numbers[element]] * _AMU_KG energy_j = float(omega_eff_meV) * 1e-3 * _EV_J omega = energy_j / _HBAR x = energy_j / (2.0 * _KB * float(T)) coth = 1.0 / math.tanh(x) sigma_m = math.sqrt(_HBAR / (2.0 * m_kg * omega) * coth) return float(sigma_m / _ANGSTROM_M)
[docs] def qho_sigma_thermal(element: str, T: float = 300.0) -> float: """QHO-table RMS displacement σ_thermal (Å).""" table = load_qho_table() entry = table.get("elements", {}).get(element) if entry is None: raise ValueError( f"No QHO sigma_thermal table entry for {element!r}. " "Use eps_noise.thermal.sigma_source: mass_only or set " "eps_noise.thermal.sigma_override for this element.") return qho_sigma_from_omega_mev(element, _omega_eff_meV(entry), T)
[docs] def resolve_sigma_thermal_targets( species, *, T: float, source: str = "qho_table", config_override: dict | None = None, ) -> dict[str, float]: """Resolve per-element σ targets for thermal ε calibration. Precedence (highest first): ``config_override`` (``eps_noise.thermal.sigma_override``), then the deprecated ``calibration.SIGMA_THERMAL_OVERRIDE`` (its warning is logged even when a config pin shadows it), then the ``source`` model. Pin values are exact Å for the current run temperature (not rescaled with ``T``). Explicit pins skip the QHO-table lookup, so pinned species need no table entry. Every target must be finite and positive. """ if source not in ("qho_table", "mass_only"): raise ValueError( f"eps_noise.thermal.sigma_source must be 'qho_table' or 'mass_only', " f"got {source!r}.") config_override = dict(config_override or {}) table = None out = {} for el in species: if el not in atomic_numbers: raise ValueError(f"Unknown element symbol: {el!r}") override = _calibration.SIGMA_THERMAL_OVERRIDE.get(el, _NO_OVERRIDE) if override is not _NO_OVERRIDE: logger.warning( "SIGMA_THERMAL_OVERRIDE for %s is deprecated; prefer " "eps_noise.thermal.sigma_override.", el) if el in config_override: sigma = float(config_override[el]) elif override is not _NO_OVERRIDE: sigma = float(override) elif source == "qho_table": if table is None: table = load_qho_table() entry = table.get("elements", {}).get(el) if entry is None: raise ValueError( f"No QHO sigma_thermal table entry for {el!r}. " "Use eps_noise.thermal.sigma_source: mass_only or set " "eps_noise.thermal.sigma_override for this element.") n_materials = int(entry.get("n_materials", 0)) if n_materials < _THIN_N_MATERIALS: logger.warning( "QHO sigma_thermal table entry for %s has thin provenance " "(n_materials=%d < %d); using it anyway.", el, n_materials, _THIN_N_MATERIALS) sigma = qho_sigma_from_omega_mev(el, _omega_eff_meV(entry), T) else: sigma = _calibration.sigma_thermal(el, T) if not math.isfinite(sigma) or sigma <= 0: raise ValueError(f"sigma_thermal target for {el!r} must be positive and finite.") out[el] = float(sigma) return out