NequIP / energy_volume.py
dadadaxi's picture
Upload folder using huggingface_hub
3e02ab8 verified
Raw
History Blame Contribute Delete
4.8 kB
"""Compute the ASE energy-volume curve from the official NequIP example."""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
import numpy as np
import torch
from ase.build import bulk
from onescience.utils.nequip.integrations.ase import NequIPCalculator
def default_compiled_model() -> str | None:
models_dir = os.environ.get("ONESCIENCE_MODELS_DIR")
if not models_dir:
return None
return str(Path(models_dir) / "NequIP" / "NequIP-OAM-L-0.1.nequip.pth")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--compiled-model", default=default_compiled_model())
parser.add_argument("--device", default="cuda")
parser.add_argument("--element", default="Si")
parser.add_argument("--crystal-structure", default="diamond")
parser.add_argument("--lattice-constant", type=float, default=5.43)
parser.add_argument("--supercell", type=int, default=3)
parser.add_argument("--scale-min", type=float, default=0.95)
parser.add_argument("--scale-max", type=float, default=1.05)
parser.add_argument("--num-points", type=int, default=10)
parser.add_argument("--output", default="outputs/energy_volume.json")
parser.add_argument("--plot", default="outputs/energy_volume.png")
args = parser.parse_args()
if not args.compiled_model:
parser.error("--compiled-model is required when ONESCIENCE_MODELS_DIR is unset")
compiled_model = Path(args.compiled_model).expanduser().resolve()
if not compiled_model.is_file():
parser.error(f"compiled model not found: {compiled_model}")
if args.num_points < 2:
parser.error("--num-points must be at least 2")
if args.supercell < 1:
parser.error("--supercell must be positive")
calculator = NequIPCalculator.from_compiled_model(
compile_path=str(compiled_model),
chemical_species_to_atom_type_map={args.element: args.element},
device=args.device,
)
points = []
for scale in np.linspace(args.scale_min, args.scale_max, args.num_points):
atoms = bulk(
args.element,
crystalstructure=args.crystal_structure,
a=args.lattice_constant * float(scale),
cubic=True,
)
atoms *= (args.supercell,) * 3
atoms.calc = calculator
energy = float(atoms.get_potential_energy())
forces = atoms.get_forces()
points.append(
{
"scale": float(scale),
"volume_angstrom3": float(atoms.get_volume()),
"energy_ev": energy,
"energy_ev_per_atom": energy / len(atoms),
"max_force_ev_per_angstrom": float(
np.linalg.norm(forces, axis=1).max()
),
}
)
energies = np.asarray([point["energy_ev"] for point in points])
volumes = np.asarray([point["volume_angstrom3"] for point in points])
minimum_index = int(np.argmin(energies))
result = {
"compiled_model": str(compiled_model),
"device": args.device,
"device_name": torch.cuda.get_device_name(0)
if args.device.startswith("cuda") and torch.cuda.is_available()
else "cpu",
"element": args.element,
"crystal_structure": args.crystal_structure,
"base_lattice_constant_angstrom": args.lattice_constant,
"supercell": [args.supercell] * 3,
"num_atoms": len(atoms),
"points": points,
"sampled_minimum": points[minimum_index],
}
output_path = Path(args.output).expanduser().resolve()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
if args.plot:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
plot_path = Path(args.plot).expanduser().resolve()
plot_path.parent.mkdir(parents=True, exist_ok=True)
plt.figure(figsize=(8, 6))
plt.plot(volumes, energies, marker="o", label="E-V Curve")
plt.xlabel("Volume (Angstrom^3)", fontsize=14)
plt.ylabel("Energy (eV)", fontsize=14)
plt.title(f"Energy-Volume Curve for Cubic {args.element}", fontsize=16)
plt.legend(fontsize=12)
plt.grid()
plt.tight_layout()
plt.savefig(plot_path, dpi=160)
plt.close()
result["plot"] = str(plot_path)
output_path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
print(f"points: {len(points)}")
print(f"atoms per point: {result['num_atoms']}")
print(f"sampled minimum: {result['sampled_minimum']}")
print(f"result: {output_path}")
if __name__ == "__main__":
main()