MACE / scripts /eval_configs.py
OneScience's picture
Upload folder using huggingface_hub
a244197 verified
Raw
History Blame Contribute Delete
5.97 kB
# Script for evaluating configurations contained in an xyz file with a trained model
import time
import argparse
import ase.io
import numpy as np
import torch
from onescience.datapipes.materials.pyg_stack.core.utils import config_from_atoms
from onescience.datapipes.materials.pyg_stack.core.atomic_data import AtomicData
from onescience.datapipes.materials.tools import torch_geometric, torch_tools, utils
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--configs", help="path to XYZ configurations", required=True)
parser.add_argument("--model", help="path to model", required=True)
parser.add_argument("--output", help="output path", required=True)
parser.add_argument(
"--device",
help="select device",
type=str,
choices=["cpu", "cuda"],
default="cpu",
)
parser.add_argument(
"--default_dtype",
help="set default dtype",
type=str,
choices=["float32", "float64"],
default="float64",
)
parser.add_argument("--batch_size", help="batch size", type=int, default=64)
parser.add_argument(
"--compute_stress",
help="compute stress",
action="store_true",
default=False,
)
parser.add_argument(
"--return_contributions",
help="model outputs energy contributions for each body order, only supported for MACE, not ScaleShiftMACE",
action="store_true",
default=False,
)
parser.add_argument(
"--info_prefix",
help="prefix for energy, forces and stress keys",
type=str,
default="MACE_",
)
parser.add_argument(
"--head",
help="Model head used for evaluation",
type=str,
required=False,
default=None,
)
return parser.parse_args()
def main() -> None:
args = parse_args()
run(args)
def run(args: argparse.Namespace) -> None:
torch_tools.set_default_dtype(args.default_dtype)
device = torch_tools.init_device(args.device)
# Load model
model = torch.load(f=args.model, map_location=args.device)
model = model.to(args.device)
model.eval() # ✅ 保持 eval,不要关掉 requires_grad
# Load data and prepare input
atoms_list = ase.io.read(args.configs, index=":")
if args.head is not None:
for atoms in atoms_list:
atoms.info["head"] = args.head
configs = [config_from_atoms(atoms) for atoms in atoms_list]
z_table = utils.AtomicNumberTable([int(z) for z in model.atomic_numbers])
try:
heads = model.heads
except AttributeError:
heads = None
data_loader = torch_geometric.dataloader.DataLoader(
dataset=[
AtomicData.from_config(
config, z_table=z_table, cutoff=float(model.r_max), heads=heads
)
for config in configs
],
batch_size=args.batch_size,
shuffle=False,
drop_last=False,
)
# Collect data
energies_list = []
contributions_list = []
stresses_list = []
forces_collection = []
total_time = 0.0
num_batches = 0
total_cfgs = len(configs)
start_total = time.time()
for batch in data_loader:
batch = batch.to(device)
if args.device == "cuda":
torch.cuda.synchronize()
start = time.time()
output = model(batch.to_dict(), compute_stress=args.compute_stress)
if args.device == "cuda":
torch.cuda.synchronize()
end = time.time()
total_time += (end - start)
num_batches += 1
energies_list.append(torch_tools.to_numpy(output["energy"]))
if args.compute_stress:
stresses_list.append(torch_tools.to_numpy(output["stress"]))
if args.return_contributions:
contributions_list.append(torch_tools.to_numpy(output["contributions"]))
forces = np.split(
torch_tools.to_numpy(output["forces"]),
indices_or_sections=batch.ptr[1:],
axis=0,
)
forces_collection.append(forces[:-1]) # drop last as its empty
end_total = time.time()
# ✅ 打印时间
if num_batches > 0 and total_cfgs > 0:
avg_time_batch = total_time / num_batches
avg_time_cfg = total_time / total_cfgs
print(f"Average inference time per batch: {avg_time_batch:.6f} s")
print(f"Average inference time per configuration: {avg_time_cfg:.6f} s")
print(f"Total inference time (only forward): {total_time:.6f} s")
print(f"Wall clock time (data+forward): {end_total - start_total:.6f} s")
else:
print("No batches were processed.")
# Process outputs
energies = np.concatenate(energies_list, axis=0)
forces_list = [
forces for forces_list in forces_collection for forces in forces_list
]
assert len(atoms_list) == len(energies) == len(forces_list)
if args.compute_stress:
stresses = np.concatenate(stresses_list, axis=0)
assert len(atoms_list) == stresses.shape[0]
if args.return_contributions:
contributions = np.concatenate(contributions_list, axis=0)
assert len(atoms_list) == contributions.shape[0]
# Store data in atoms objects
for i, (atoms, energy, forces) in enumerate(zip(atoms_list, energies, forces_list)):
atoms.calc = None # crucial
atoms.info[args.info_prefix + "energy"] = energy
atoms.arrays[args.info_prefix + "forces"] = forces
if args.compute_stress:
atoms.info[args.info_prefix + "stress"] = stresses[i]
if args.return_contributions:
atoms.info[args.info_prefix + "BO_contributions"] = contributions[i]
# Write atoms to output path
ase.io.write(args.output, images=atoms_list, format="extxyz")
if __name__ == "__main__":
main()