Source code for ufp.adapters.ase
"""
ASE calculator integration for UFP potentials.
Use this adapter when an ``ase.Atoms`` workflow should consume UFP energies,
forces, and optional per-atom outputs.
"""
from __future__ import annotations
from typing import Iterable, Optional, Sequence, Union
from ase.calculators.calculator import (
Calculator,
PropertyNotImplementedError,
all_changes,
)
from ufp.core.potential import UFPotential
from ufp.neighbors._neighbors import NeighborListBackend
[docs]
class UFPASECalculator(Calculator):
"""
Wrap a UFP potential as an ASE calculator.
Args:
potential: Potential to evaluate.
neighbor_backend: Backend used when the potential builds neighbor lists.
"""
implemented_properties = [
"energy",
"free_energy",
"forces",
"stress",
"energies",
]
def __init__(
self,
potential: UFPotential,
*,
neighbor_backend: Union[str, NeighborListBackend] = NeighborListBackend.AUTO,
**kwargs,
) -> None:
"""Initialize the ASE calculator with one wrapped UFP potential."""
super().__init__(**kwargs)
self.potential = potential
self.neighbor_backend = NeighborListBackend(neighbor_backend)
self.parameters["neighbor_backend"] = self.neighbor_backend.value
[docs]
def calculate(
self,
atoms=None,
properties: Optional[Sequence[str]] = None,
system_changes: Optional[Iterable[str]] = all_changes,
) -> None:
"""Run the wrapped potential and populate the ASE ``results`` dictionary."""
super().calculate(atoms, properties, system_changes)
if atoms is None:
raise ValueError("`atoms` is required")
prediction = self.potential.compute(
atoms=atoms,
backend=self.neighbor_backend,
derive_forces=properties is None or "forces" in properties,
)
self.results = prediction.as_ase_results(n_atoms=len(atoms))
if properties is None:
return
missing = [name for name in properties if name not in self.results]
if missing:
raise PropertyNotImplementedError(
"the wrapped UFPotential did not provide the requested ASE "
f"properties: {', '.join(missing)}"
)