NequIP / single_point.py
dadadaxi's picture
Upload folder using huggingface_hub
3e02ab8 verified
Raw
History Blame Contribute Delete
6.62 kB
"""Run one NequIP energy, force, and stress prediction through ASE.
This script follows the official NequIP ASE integration style:
https://nequip.readthedocs.io/en/latest/integrations/ase.html
"""
from __future__ import annotations
import argparse
import json
import os
import warnings
from pathlib import Path
from typing import Any, Dict
warnings.filterwarnings("ignore", category=FutureWarning, module="e3nn")
from ase.build import bulk
from ase.io import read
from onescience.models.nequip.model import ModelTypeNamesFromPackage
from onescience.models.nequip.model.nequip_models import NequIPGNNModel
from onescience.utils.nequip.internal.global_state import set_global_state
from onescience.utils.nequip import build_nequip_calculator
def default_paths() -> Dict[str, str | None]:
"""Return default compiled model / checkpoint paths if env var is set."""
models_dir = os.environ.get("ONESCIENCE_MODELS_DIR")
if not models_dir:
return {"compiled_model": None, "checkpoint": None}
nequip_dir = Path(models_dir) / "NequIP"
return {
"compiled_model": str(nequip_dir / "NequIP-OAM-L-0.1.nequip.pth"),
"checkpoint": None,
}
def resolve_model_paths(
compiled_model: str | None, checkpoint: str | None
) -> Dict[str, str | None]:
"""Prefer an explicitly selected model source over environment defaults."""
if compiled_model or checkpoint:
return {"compiled_model": compiled_model, "checkpoint": checkpoint}
return default_paths()
def load_structure(path: str | None, index: int):
"""Load an ASE structure or use the built-in Cu bulk example."""
if path:
return read(path, index=index)
return bulk("Cu")
def write_workflow_result(result: Dict[str, Any], output_path: str) -> str:
"""Write a workflow result dictionary to a JSON file."""
output = Path(output_path)
output.parent.mkdir(parents=True, exist_ok=True)
with open(output, "w", encoding="utf-8") as f:
json.dump(result, f, indent=2, ensure_ascii=False)
return str(output)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
group = parser.add_mutually_exclusive_group()
group.add_argument(
"--compiled-model",
help="Path to a compiled NequIP model (.nequip.pth or .nequip.pt2).",
)
group.add_argument(
"--checkpoint",
help="Path to a NequIP checkpoint (.ckpt) or packaged model (.nequip.zip).",
)
group.add_argument(
"--demo",
action="store_true",
help="Use a small built-in demo model instead of a real checkpoint.",
)
parser.add_argument(
"--package",
help=(
"Original .nequip.zip package for a fine-tuned checkpoint; its atom "
"types are read automatically."
),
)
parser.add_argument(
"--input",
help=(
"CIF, POSCAR, XYZ, trajectory, or another ASE-readable structure; "
"defaults to the built-in periodic Cu example"
),
)
parser.add_argument(
"--index",
type=int,
default=0,
help="Zero-based frame index for trajectory inputs (default: 0).",
)
parser.add_argument("--device", default="cuda")
parser.add_argument("--output", default="outputs/single_point.json")
parser.add_argument(
"--model-type-names",
nargs="+",
default=["C", "H", "O", "Cu"],
help="Chemical species the model knows about (used for demo/checkpoint).",
)
parser.add_argument(
"--r-max",
type=float,
default=4.0,
help="Neighbor-list cutoff in Angstrom (used for demo/checkpoint models).",
)
args = parser.parse_args()
for label, path in (
("compiled model", args.compiled_model),
("checkpoint", args.checkpoint),
("package", args.package),
):
if path and not Path(path).expanduser().is_file():
parser.error(f"{label} not found: {path}")
model_paths = resolve_model_paths(args.compiled_model, args.checkpoint)
compiled_model = model_paths["compiled_model"]
checkpoint = model_paths["checkpoint"]
if args.package and not checkpoint:
parser.error("--package requires --checkpoint")
model_type_names = list(args.model_type_names)
package_for_types = args.package
if package_for_types is None and checkpoint and checkpoint.endswith(".nequip.zip"):
package_for_types = checkpoint
if package_for_types:
model_type_names = list(ModelTypeNamesFromPackage(package_for_types))
atoms = load_structure(args.input, args.index)
calc_kwargs: Dict[str, Any] = {"device": args.device}
if args.demo:
set_global_state()
calc_kwargs["model"] = NequIPGNNModel(
seed=123,
model_dtype="float32",
type_names=model_type_names,
num_layers=2,
l_max=1,
num_features=32,
r_max=args.r_max,
parity=False,
avg_num_neighbors=10.0,
)
elif compiled_model and Path(compiled_model).exists():
calc_kwargs["compiled_model"] = compiled_model
elif checkpoint and Path(checkpoint).exists():
calc_kwargs["checkpoint"] = checkpoint
calc_kwargs["model_type_names"] = model_type_names
else:
parser.error(
"no model found; pass --compiled-model, --checkpoint, or --demo"
)
atoms.calc = build_nequip_calculator(**calc_kwargs)
result = {
"formula": atoms.get_chemical_formula(),
"natoms": len(atoms),
"input": str(Path(args.input).expanduser()) if args.input else None,
"input_index": args.index if args.input else None,
"input_source": args.input or "ASE bulk Cu default",
"compiled_model": str(Path(compiled_model).expanduser()) if compiled_model else None,
"checkpoint": str(Path(checkpoint).expanduser()) if checkpoint else None,
"package": str(Path(package_for_types).expanduser()) if package_for_types else None,
"pbc": atoms.pbc.tolist(),
"cell_angstrom": atoms.cell.array.tolist(),
"energy_ev": float(atoms.get_potential_energy()),
"forces_ev_per_angstrom": atoms.get_forces().tolist(),
"stress_ev_per_angstrom_cubed_voigt": atoms.get_stress().tolist(),
}
output = write_workflow_result(result, args.output)
print("formula:", result["formula"])
print("atoms:", result["natoms"])
print("energy (eV):", result["energy_ev"])
print("result:", output)
if __name__ == "__main__":
main()