Source code for vibeqc.smearing.options

"""User-facing smearing-options dataclass.

At v0.10.x this lives Python-side only — the C++-bound
``PeriodicRHFOptions`` / ``PeriodicSCFOptions`` /
``PeriodicKSOptions`` structs keep their existing
``smearing_temperature: float`` field as the on-options-surface
single source of truth (see `docs/design_smearing.md` § Q4 for
the staging rationale). Drivers normalise the legacy float at
the top of their SCF loop via
``SmearingOptions.from_legacy_kwarg(...)`` and feed the
resulting dataclass to ``vibeqc.smearing.apply_smearing(...)``.

At v0.11.0 this dataclass becomes a C++ struct embedded on every
periodic options surface, and the legacy ``smearing_temperature``
float is removed.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional, Union

from .resolution import resolve_smearing_temperature


VALID_FLAVORS = ("fermi-dirac", "methfessel-paxton", "marzari-vanderbilt")


[docs] @dataclass(frozen=True) class SmearingOptions: """Canonical user-facing smearing configuration. ``temperature`` is the electronic energy ``k_B T`` in Hartree (matching the v0.4.0 contract on the C++ options surface). ``temperature == 0.0`` disables smearing — drivers fast-path on it. ``flavor`` selects the occupation function. ``"fermi-dirac"`` is the only flavor implemented at v0.10.x; ``"methfessel-paxton"`` (M6) and ``"marzari-vanderbilt"`` (M7) construct without erroring so user code can pre-stage settings, but ``apply_smearing`` raises ``NotImplementedError`` until the relevant milestone lands. ``mp_order`` is the Hermite-polynomial order for Methfessel-Paxton; ignored for other flavors. ``source`` and ``reason`` mirror :class:`SmearingResolution` so SCF logs can say "preset:metal" or "explicit:kelvin" rather than just a number. """ temperature: float = 0.0 flavor: str = "fermi-dirac" mp_order: int = 1 source: str = "explicit" reason: str = "" def __post_init__(self) -> None: if float(self.temperature) < 0.0: raise ValueError( "SmearingOptions.temperature must be >= 0 (Hartree); " f"got {self.temperature!r}" ) flav = str(self.flavor).strip().lower().replace("_", "-") if flav not in VALID_FLAVORS: raise ValueError( f"SmearingOptions.flavor must be one of {VALID_FLAVORS}; " f"got {self.flavor!r}" ) if flav == "methfessel-paxton": n = int(self.mp_order) if n not in (1, 2): raise ValueError( "SmearingOptions.mp_order must be 1 or 2 for " f"Methfessel-Paxton; got {self.mp_order!r}" ) # Normalise alias spelling on the stored field so downstream # comparisons never see "fermi_dirac" vs "fermi-dirac". object.__setattr__(self, "flavor", flav) @property def enabled(self) -> bool: """``True`` when smearing is active (``temperature > 0``).""" return float(self.temperature) > 0.0
[docs] @classmethod def from_legacy_kwarg( cls, smearing_temperature: float, *, flavor: str = "fermi-dirac", mp_order: int = 1, ) -> "SmearingOptions": """Build from the v0.4.0 ``smearing_temperature: float`` kwarg. This is the internal driver-side constructor that bridges the legacy C++ option field to the new Python dataclass without involving the user. No deprecation warning here — the warning fires at the user-facing kwarg boundary. """ T = float(smearing_temperature) return cls( temperature=T, flavor=flavor, mp_order=int(mp_order), source="legacy_kwarg" if T > 0.0 else "explicit", reason=( "via PeriodicRHFOptions.smearing_temperature " "(deprecated in v0.10.x)" if T > 0.0 else "smearing disabled" ), )
[docs] @classmethod def from_user( cls, value: Union[None, str, float] = 0.0, *, unit: str = "hartree", flavor: str = "fermi-dirac", mp_order: int = 1, metallic: Optional[bool] = None, band_gap_hartree: Optional[float] = None, n_electrons: Optional[float] = None, ) -> "SmearingOptions": """Resolve user-facing temperature input to canonical options. Accepts everything ``resolve_smearing_temperature`` accepts (numeric width, ``"1000 K"``, ``"0.1 eV"``, ``"metal"``, ``"auto"``, etc.). """ res = resolve_smearing_temperature( value, unit=unit, metallic=metallic, band_gap_hartree=band_gap_hartree, n_electrons=n_electrons, ) return cls( temperature=res.temperature, flavor=flavor, mp_order=int(mp_order), source=res.source, reason=res.reason, )