From 7d8529e5f5bab61e18d4b020daaeb4e15eae5d86 Mon Sep 17 00:00:00 2001 From: Yusuke KONISHI Date: Thu, 20 Mar 2025 22:21:18 +0900 Subject: [PATCH 1/7] add files for SevenNet, Mace, CHGNet --- .../latgas_abinitio_interface/__init__.py | 8 +- .../latgas_abinitio_interface/chgnet.py | 217 +++++++++++++ .../chgnet_trainer.py | 237 ++++++++++++++ .../latgas_abinitio_interface/mace.py | 216 +++++++++++++ .../latgas_abinitio_interface/mace_trainer.py | 165 ++++++++++ .../latgas_abinitio_interface/sevennet.py | 218 +++++++++++++ .../sevennet_trainer.py | 168 ++++++++++ abics/scripts/main_dft_latgas.py | 7 + docs/sphinx/ja/source/how_to_use/index.rst | 41 +++ .../ja/source/tutorial/other_models.rst | 301 ++++++++++++++++++ 10 files changed, 1577 insertions(+), 1 deletion(-) create mode 100644 abics/applications/latgas_abinitio_interface/chgnet.py create mode 100644 abics/applications/latgas_abinitio_interface/chgnet_trainer.py create mode 100644 abics/applications/latgas_abinitio_interface/mace.py create mode 100644 abics/applications/latgas_abinitio_interface/mace_trainer.py create mode 100644 abics/applications/latgas_abinitio_interface/sevennet.py create mode 100644 abics/applications/latgas_abinitio_interface/sevennet_trainer.py diff --git a/abics/applications/latgas_abinitio_interface/__init__.py b/abics/applications/latgas_abinitio_interface/__init__.py index 04ce27ba..03d268b6 100644 --- a/abics/applications/latgas_abinitio_interface/__init__.py +++ b/abics/applications/latgas_abinitio_interface/__init__.py @@ -26,8 +26,14 @@ register_solver("aenetPyLammps", "AenetPyLammpsSolver", "abics.applications.latgas_abinitio_interface.aenet_pylammps") register_solver("nequip", "NequipSolver", "abics.applications.latgas_abinitio_interface.nequip") register_solver("mlip_3", "MLIP3Solver", "abics.applications.latgas_abinitio_interface.mlip_3") +register_solver("sevennet", "SevennetSolver", "abics.applications.latgas_abinitio_interface.sevennet") +register_solver("mace", "MaceSolver", "abics.applications.latgas_abinitio_interface.mace") +register_solver("chgnet", "CHGNetSolver", "abics.applications.latgas_abinitio_interface.chgnet") register_solver("User", "UserFunctionSolver", "abics.applications.latgas_abinitio_interface.user_function_solver") register_trainer("aenet", "AenetTrainer", "abics.applications.latgas_abinitio_interface.aenet_trainer") register_trainer("nequip", "NequipTrainer", "abics.applications.latgas_abinitio_interface.nequip_trainer") -register_trainer("mlip_3", "MLIP3Trainer", "abics.applications.latgas_abinitio_interface.mlip_3_trainer") \ No newline at end of file +register_trainer("mlip_3", "MLIP3Trainer", "abics.applications.latgas_abinitio_interface.mlip_3_trainer") +register_trainer("sevennet", "SevennetTrainer", "abics.applications.latgas_abinitio_interface.sevennet_trainer") +register_trainer("mace", "MaceTrainer", "abics.applications.latgas_abinitio_interface.mace_trainer") +register_trainer("chgnet", "CHGNetTrainer", "abics.applications.latgas_abinitio_interface.chgnet_trainer") \ No newline at end of file diff --git a/abics/applications/latgas_abinitio_interface/chgnet.py b/abics/applications/latgas_abinitio_interface/chgnet.py new file mode 100644 index 00000000..7e8a07ed --- /dev/null +++ b/abics/applications/latgas_abinitio_interface/chgnet.py @@ -0,0 +1,217 @@ +# ab-Initio Configuration Sampling tool kit (abICS) +# Copyright (C) 2019- The University of Tokyo +# +# abICS wrapper of CHGNet +# Yusuke Konishi (Academeia Co., Ltd.) 2025 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. + +""" +energy calculator using sevennet python interface +""" + +from __future__ import annotations + +import os.path +from collections import namedtuple +from pymatgen.core import Structure +import torch +from ase import Atoms +from ase.calculators.calculator import Calculator +from nequip.utils import Config +from chgnet.model.model import CHGNet +from chgnet.model import StructOptimizer + +from .base_solver import SolverBase, register_solver +from .params import ALParams, DFTParams + +class CHGNetSolver(SolverBase): + """ + Nequip solver + + Attributes + ---------- + path_to_solver : str + Path to the solver + input : NequipSolver.Input + Input manager + output : NequipSolver.Output + Output manager + """ + + def __init__(self, ignore_species, use_pretrained, relax, fmax, device): + """ + Initialize the solver. + + """ + + super(CHGNetSolver, self).__init__("") + self.path_to_solver = self.calculate_energy + self.input = CHGNetSolver.Input(ignore_species, use_pretrained, relax, fmax, device) + self.output = CHGNetSolver.Output() + + def name(self): + return "chgnet" + + def calculate_energy(self, fi, output_dir): + st = self.input.st + + if self.input.relax: + result = self.input.calculator.relax(atoms=st, fmax=self.input.fmax) + # Get predicted energy + ene = result["trajectory"].energies[-1] + else: + comp = st.composition.as_dict() + num_atoms = sum([comp[key] for key in comp.keys()]) + ene = self.input.calculator.predict_structure(st)["e"]*num_atoms + + self.output.st = st + self.output.ene = ene + + class Input(object): + """ + Input manager for Mock + + Attributes + ---------- + st : pymatgen.Structure + structure + """ + + st: Structure + + def __init__(self, ignore_species=None, use_pretrained=True, relax=True, fmax=0.1, device="cpu"): + self.ignore_species = ignore_species + self.use_pretrained = use_pretrained + self.relax = relax + self.fmax = fmax + self.device = device + # self.st = Structure() + + def from_directory(self, base_input_dir): + """ + + Parameters + ---------- + base_input_dir : str + Path to the directory including base input files. + """ + self.base_input_dir = base_input_dir + model_file = os.path.join(base_input_dir, "deployed.pth.tar") + if not self.use_pretrained: + if self.relax: + model = CHGNet.from_file(model_file) + self.calculator = StructOptimizer(model=model, optimizer_class="BFGS") + else: + self.calculator = CHGNet.from_file(model_file) + else: + if self.relax: + model = CHGNet.load(use_device=self.device) + self.calculator = StructOptimizer(model=model, optimizer_class="BFGS") + else: + self.calculator = CHGNet.load(use_device=self.device) + + def update_info_by_structure(self, structure): + """ + Update information by structure file + + Parameters + ---------- + structure : pymatgen.Structure + Atomic structure + """ + self.st = structure.copy() + if self.ignore_species is not None: + self.st.remove_species(self.ignore_species) + + def update_info_from_files(self, workdir, rerun): + """ + Do nothing + """ + pass + + def write_input(self, output_dir): + """ + Generate input files of the solver program. + + Parameters + ---------- + workdir : str + Path to working directory. + """ + if not os.path.exists(output_dir): + import shutil + + shutil.copytree(self.base_input_dir, output_dir) + + # self.st.to("POSCAR", os.path.join(output_dir, "structure.vasp")) + + def cl_args(self, nprocs, nthreads, workdir): + """ + Generate command line argument of the solver program. + + Parameters + ---------- + nprocs : int + The number of processes. + nthreads : int + The number of threads. + workdir : str + Path to the working directory. + + Returns + ------- + args : list[str] + Arguments of command + """ + return [workdir] + + class Output(object): + """ + Output manager. + """ + + def __init__(self): + pass + + def get_results(self, workdir): + """ + Get energy and structure obtained by the solver program. + + Parameters + ---------- + workdir : str + Path to the working directory. + + Returns + ------- + phys : named_tuple("energy", "structure") + Total energy and atomic structure. + The energy is measured in the units of eV + and coodinates is measured in the units of Angstrom. + """ + Phys = namedtuple("PhysVaules", ("energy", "structure")) + return Phys(self.ene, self.st) + + def solver_run_schemes(self): + return ("function",) + + @classmethod + def create(cls, params: ALParams | DFTParams): + ignore_species = params.ignore_species + use_pretrained = params.use_pretrained + relax = params.relax + fmax = params.fmax + device = params.device + return cls(ignore_species, use_pretrained, relax, fmax, device) diff --git a/abics/applications/latgas_abinitio_interface/chgnet_trainer.py b/abics/applications/latgas_abinitio_interface/chgnet_trainer.py new file mode 100644 index 00000000..6196af3d --- /dev/null +++ b/abics/applications/latgas_abinitio_interface/chgnet_trainer.py @@ -0,0 +1,237 @@ +# ab-Initio Configuration Sampling tool kit (abICS) +# Copyright (C) 2019- The University of Tokyo +# +# abICS wrapper of CHGNet solver +# Masashi Noda, Yusuke Konishi (Academeia Co., Ltd.) 2025 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. + +from __future__ import annotations +from typing import Sequence, Dict + +import numpy as np +import os, pathlib, shutil, subprocess, shlex +import time + +from pymatgen.core import Structure + +from abics.util import expand_cmd_path +from abics.applications.latgas_abinitio_interface.base_trainer import TrainerBase +from abics.applications.latgas_abinitio_interface.util import structure_to_XSF + +import ase +from ase import io +from ase.calculators.singlepoint import SinglePointCalculator +from ase.calculators.morse import MorsePotential + +from chgnet.model.model import CHGNet +from chgnet.data.dataset import StructureData, get_train_val_test_loader +from chgnet.trainer import Trainer + +import yaml + +def xsf_to_ase(xsf): + ase_xsf = ase.io.read(xsf) + with open(xsf) as f: + lines = f.readlines() + + tot_energy = float(lines[0].split()[4]) + ase_xsf.calc = SinglePointCalculator(energy=tot_energy, atoms=ase_xsf) + ase_xsf.calc = MorsePotential() + forces = ase_xsf.get_forces() + ase_xsf.calc = SinglePointCalculator(energy=tot_energy, forces=forces, atoms=ase_xsf) + return ase_xsf + +class CHGNetTrainer(TrainerBase): + def __init__( + self, + structures: Sequence[Structure], + energies: Sequence[float], + generate_inputdir: os.PathLike, + train_inputdir: os.PathLike, + predict_inputdir: os.PathLike, + execute_commands: Dict, + # trainer_type: str, + ): + self.structures = structures + self.energies = energies + self.generate_inputdir = generate_inputdir + self.train_inputdir = train_inputdir + self.predict_inputdir = predict_inputdir + #train_exe = execute_commands["train"] + #self.train_exe = [expand_cmd_path(e) for e in shlex.split(train_exe)] + assert len(self.structures) == len(self.energies) + self.numdata = len(self.structures) + self.is_prepared = False + self.is_trained = False + self.generate_outputdir = None + self.chgnet_params = { + "finetuning" : True, + "batch_size" : 4, + "train_ratio" : 0.9, + "val_ratio" : 0.05, + "learning_rate" : 1e-2, + "epochs" : 5, + "model_params" : { + "atom_fea_dim" : 64 + } + } + yaml_file = os.path.join(train_inputdir, "input.yaml") + with open(yaml_file, "r") as f: + self.chgnet_params.update(yaml.safe_load(f)) + if "mlp_hidden_dims" in self.chgnet_params["model_params"].keys(): + self.chgnet_params["model_params"]["mlp_hidden_dims"] = tuple(self.chgnet_params["model_params"]["mlp_hidden_dims"]) + # self.trainer_type = trainer_type + + def prepare(self, latgas_mode = True, st_dir = "chgnetXSF"): + rootdir = os.getcwd() + xsfdir = os.path.join(rootdir, st_dir) + + # prepare XSF files for nequip + os.makedirs(xsfdir, exist_ok=True) + os.chdir(xsfdir) + xsfdir = os.getcwd() + if latgas_mode: + for i, st in enumerate(self.structures): + xsf_string = structure_to_XSF(st, write_force_zero=False) + xsf_string = ( + "# total energy = {} eV\n\n".format(self.energies[i]) + xsf_string + ) + with open("structure.{}.xsf".format(i), "w") as fi: + fi.write(xsf_string) + else: + for i, st in enumerate(self.structures): + xsf_string = structure_to_XSF(st, write_force_zero=False) + xsf_string = ( + "# total energy = {} eV\n\n".format(self.energies[i]) + xsf_string + ) + with open("structure.{}.xsf".format(i), "w") as fi: + fi.write(xsf_string) + + os.chdir(rootdir) + + def generate_run(self, xsfdir="chgnetXSF", generate_dir="generate"): + # prepare generate + xsfdir = str(pathlib.Path(xsfdir).resolve()) + if os.path.exists(generate_dir): + shutil.rmtree(generate_dir) + shutil.copytree(self.generate_inputdir, generate_dir) + os.makedirs(generate_dir, exist_ok=True) + self.generate_dir = generate_dir + os.chdir(generate_dir) + xsf_paths = [ + os.path.join(xsfdir, "structure.{}.xsf".format(i)) + for i in range(self.numdata) + ] + ases = [xsf_to_ase(xsf) for xsf in xsf_paths] + #generate structure.xyz + ase.io.write("structure.xyz", ases) + self.generate_outputdir = os.getcwd() + os.chdir(pathlib.Path(os.getcwd()).parent) + + def generate_wait(self): + interval = 0.1 # sec + self.is_prepared = False + if os.path.exists(os.path.join(self.generate_outputdir, "structure.xyz")): + self.is_prepared = True + time.sleep(interval) + if not self.is_prepared: + raise RuntimeError(f"{self.generate_outputdir}") + + def train(self, train_dir = "train"): + if not self.is_prepared: + raise RuntimeError("you have to prepare the trainer before training!") + if os.path.exists(train_dir): + shutil.rmtree(train_dir) + shutil.copytree(self.train_inputdir, train_dir) + os.chdir(train_dir) + + os.rename( + os.path.join(self.generate_outputdir, "structure.xyz"), + os.path.join(os.getcwd(), "structure.xyz"), + ) + + # read structure.xyz as ase + atoms_list = io.read("structure.xyz", index=":") + # make Structure list and energy list + structures = [] + energies = [] + forces = [] + + for atoms in atoms_list: + # Convert ASE Atoms -> Pymatgen Structure + structure = Structure( + lattice=atoms.get_cell(), + species=atoms.get_chemical_symbols(), + coords=atoms.get_positions(), + coords_are_cartesian=True + ) + structures.append(structure) + + comp = structure.composition.as_dict() + num_atoms = sum([comp[key] for key in comp.keys()]) + + # Get energy and force + energy = atoms.get_potential_energy() + force = atoms.get_forces() + energies.append(energy/num_atoms) + forces.append(force) + + dataset = StructureData( + structures=structures, + energies=energies, + forces = forces, + ) + train_loader, val_loader, test_loader = get_train_val_test_loader( + dataset, + batch_size=self.chgnet_params["batch_size"], + train_ratio=self.chgnet_params["train_ratio"], + val_ratio=self.chgnet_params["val_ratio"] + ) + if self.chgnet_params["finetuning"]: + chgnet = CHGNet(atom_graph_cutoff=7.5, bond_graph_cutoff=6.0) + chgnet = chgnet.load() + else: + chgnet = CHGNet( + **self.chgnet_params["model_params"] + ) + + trainer = Trainer( + model=chgnet, + targets="ef", + force_loss_ratio=0.0, + optimizer="Adam", + criterion="MSE", + learning_rate=self.chgnet_params["learning_rate"], + epochs=self.chgnet_params["epochs"], + ) + trainer.train(train_loader, val_loader, test_loader, save_dir="chgnet_out", train_composition_model=True) + + os.chdir(pathlib.Path(os.getcwd()).parent) + self.is_trained = True + + def new_baseinput(self, baseinput_dir, train_dir = "train"): + try: + assert self.is_trained + except AssertionError as e: + e.args += "you have to train before getting results!" + + baseinput = str(pathlib.Path(baseinput_dir).resolve()) + os.makedirs(baseinput, exist_ok=True) + shutil.copy(os.path.join(train_dir,"input.yaml"),baseinput) + os.chdir(train_dir) + # Search for bestE_*.pth.tar in the chgnet_out directory and copy the 0th as deployed.pth.tar + bestE_files = [f for f in os.listdir("chgnet_out") if f.startswith("bestE")] + shutil.copy(os.path.join("chgnet_out",bestE_files[0]),os.path.join(baseinput,"deployed.pth.tar")) + os.chdir(pathlib.Path(os.getcwd()).parent) diff --git a/abics/applications/latgas_abinitio_interface/mace.py b/abics/applications/latgas_abinitio_interface/mace.py new file mode 100644 index 00000000..a5b3cb7d --- /dev/null +++ b/abics/applications/latgas_abinitio_interface/mace.py @@ -0,0 +1,216 @@ +# ab-Initio Configuration Sampling tool kit (abICS) +# Copyright (C) 2019- The University of Tokyo +# +# abICS wrapper of Mace +# Masashi Noda, Yusuke Konishi (Academeia Co., Ltd.) 2025 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. + +""" +energy calculator using mace python interface +""" + +from __future__ import annotations + +import os.path +from collections import namedtuple +import numpy as np +from pymatgen.core import Structure +import torch +from ase import Atoms +from ase.optimize import BFGS +from nequip.data import AtomicDataDict, AtomicData +from nequip.utils import Config +from mace.calculators import mace_mp, MACECalculator + +from .base_solver import SolverBase, register_solver +from .params import ALParams, DFTParams + + +class MaceSolver(SolverBase): + """ + Nequip solver + + Attributes + ---------- + path_to_solver : str + Path to the solver + input : NequipSolver.Input + Input manager + output : NequipSolver.Output + Output manager + """ + + def __init__(self, ignore_species, use_pretrained, relax, fmax, device): + """ + Initialize the solver. + + """ + + super(MaceSolver, self).__init__("") + self.path_to_solver = self.calculate_energy + self.input = MaceSolver.Input(ignore_species, use_pretrained, relax, fmax, device) + self.output = MaceSolver.Output() + + def name(self): + return "mace" + + def calculate_energy(self, fi, output_dir): + st = self.input.st + symbols = [site.specie.symbol for site in st] + positions = [site.coords for site in st] + pbc = (True, True, True) + cell = st.lattice.matrix + atoms = Atoms(symbols=symbols, positions=positions, pbc=pbc, cell=cell) + + atoms.calc = self.input.calculator + + if self.input.relax: + opt = BFGS(atoms) + opt.run(fmax=self.input.fmax) + + # Get predicted energy + ene = atoms.get_total_energy() + + self.output.st = st + self.output.ene = ene + + class Input(object): + """ + Input manager for Mock + + Attributes + ---------- + st : pymatgen.Structure + structure + """ + + st: Structure + + def __init__(self, ignore_species=None, use_pretrained=False, relax=False, fmax=0.1, device="cpu"): + self.ignore_species = ignore_species + self.use_pretrained = use_pretrained + self.relax = relax + self.fmax = fmax + self.device = device + # self.st = Structure() + + def from_directory(self, base_input_dir): + """ + + Parameters + ---------- + base_input_dir : str + Path to the directory including base input files. + """ + self.base_input_dir = base_input_dir + if not self.use_pretrained: + self.model = os.path.join(base_input_dir, "deployed.model") + self.calculator = MACECalculator(model_paths=self.model, device=self.device, default_dtype="float64") + else: + self.calculator = mace_mp(device=self.device, default_dtype="float64") + + def update_info_by_structure(self, structure): + """ + Update information by structure file + + Parameters + ---------- + structure : pymatgen.Structure + Atomic structure + """ + self.st = structure.copy() + if self.ignore_species is not None: + self.st.remove_species(self.ignore_species) + + def update_info_from_files(self, workdir, rerun): + """ + Do nothing + """ + pass + + def write_input(self, output_dir): + """ + Generate input files of the solver program. + + Parameters + ---------- + workdir : str + Path to working directory. + """ + if not os.path.exists(output_dir): + import shutil + + shutil.copytree(self.base_input_dir, output_dir) + + # self.st.to("POSCAR", os.path.join(output_dir, "structure.vasp")) + + def cl_args(self, nprocs, nthreads, workdir): + """ + Generate command line argument of the solver program. + + Parameters + ---------- + nprocs : int + The number of processes. + nthreads : int + The number of threads. + workdir : str + Path to the working directory. + + Returns + ------- + args : list[str] + Arguments of command + """ + return [workdir] + + class Output(object): + """ + Output manager. + """ + + def __init__(self): + pass + + def get_results(self, workdir): + """ + Get energy and structure obtained by the solver program. + + Parameters + ---------- + workdir : str + Path to the working directory. + + Returns + ------- + phys : named_tuple("energy", "structure") + Total energy and atomic structure. + The energy is measured in the units of eV + and coodinates is measured in the units of Angstrom. + """ + Phys = namedtuple("PhysVaules", ("energy", "structure")) + return Phys(self.ene, self.st) + + def solver_run_schemes(self): + return ("function",) + + @classmethod + def create(cls, params: ALParams | DFTParams): + ignore_species = params.ignore_species + use_pretrained = params.use_pretrained + relax = params.relax + fmax = params.fmax + device = params.device + return cls(ignore_species, use_pretrained, relax, fmax, device) diff --git a/abics/applications/latgas_abinitio_interface/mace_trainer.py b/abics/applications/latgas_abinitio_interface/mace_trainer.py new file mode 100644 index 00000000..58ce7ba9 --- /dev/null +++ b/abics/applications/latgas_abinitio_interface/mace_trainer.py @@ -0,0 +1,165 @@ +# ab-Initio Configuration Sampling tool kit (abICS) +# Copyright (C) 2019- The University of Tokyo +# +# abICS wrapper of Mace solver +# Masashi Noda, Yusuke Konishi (Academeia Co., Ltd.) 2025 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. + +from __future__ import annotations +from typing import Sequence, Dict + +import numpy as np +import os, pathlib, shutil, subprocess, shlex +import time + +from pymatgen.core import Structure + +from abics.util import expand_cmd_path +from abics.applications.latgas_abinitio_interface.base_trainer import TrainerBase +from abics.applications.latgas_abinitio_interface.util import structure_to_XSF + +import ase +from ase import io +from ase.calculators.singlepoint import SinglePointCalculator + +import yaml + +def xsf_to_ase(xsf): + ase_xsf = ase.io.read(xsf) + with open(xsf) as f: + lines = f.readlines() + + tot_energy = float(lines[0].split()[4]) + ase_xsf.calc = SinglePointCalculator(energy=tot_energy, atoms=ase_xsf) + return ase_xsf + +class MaceTrainer(TrainerBase): + def __init__( + self, + structures: Sequence[Structure], + energies: Sequence[float], + generate_inputdir: os.PathLike, + train_inputdir: os.PathLike, + predict_inputdir: os.PathLike, + execute_commands: Dict, + # trainer_type: str, + ): + self.structures = structures + self.energies = energies + self.generate_inputdir = generate_inputdir + self.train_inputdir = train_inputdir + self.predict_inputdir = predict_inputdir + train_exe = execute_commands["train"] + self.train_exe = [expand_cmd_path(e) for e in shlex.split(train_exe)] + self.train_exe.append("--config=input.yaml") + assert len(self.structures) == len(self.energies) + self.numdata = len(self.structures) + self.is_prepared = False + self.is_trained = False + self.generate_outputdir = None + # self.trainer_type = trainer_type + + def prepare(self, latgas_mode = True, st_dir = "maceXSF"): + rootdir = os.getcwd() + xsfdir = os.path.join(rootdir, st_dir) + + # prepare XSF files for nequip + os.makedirs(xsfdir, exist_ok=True) + os.chdir(xsfdir) + xsfdir = os.getcwd() + if latgas_mode: + for i, st in enumerate(self.structures): + xsf_string = structure_to_XSF(st, write_force_zero=False) + xsf_string = ( + "# total energy = {} eV\n\n".format(self.energies[i]) + xsf_string + ) + with open("structure.{}.xsf".format(i), "w") as fi: + fi.write(xsf_string) + else: + for i, st in enumerate(self.structures): + xsf_string = structure_to_XSF(st, write_force_zero=False) + xsf_string = ( + "# total energy = {} eV\n\n".format(self.energies[i]) + xsf_string + ) + with open("structure.{}.xsf".format(i), "w") as fi: + fi.write(xsf_string) + + os.chdir(rootdir) + + def generate_run(self, xsfdir="maceXSF", generate_dir="generate"): + # prepare generate + xsfdir = str(pathlib.Path(xsfdir).resolve()) + if os.path.exists(generate_dir): + shutil.rmtree(generate_dir) + shutil.copytree(self.generate_inputdir, generate_dir) + os.makedirs(generate_dir, exist_ok=True) + self.generate_dir = generate_dir + os.chdir(generate_dir) + xsf_paths = [ + os.path.join(xsfdir, "structure.{}.xsf".format(i)) + for i in range(self.numdata) + ] + ases = [xsf_to_ase(xsf) for xsf in xsf_paths] + for ase_ in ases: + ase_.info["energy_mace"] = ase_.get_potential_energy() + #generate structure.xyz + ase.io.write("structure.xyz", ases) + self.generate_outputdir = os.getcwd() + os.chdir(pathlib.Path(os.getcwd()).parent) + + def generate_wait(self): + interval = 0.1 # sec + self.is_prepared = False + if os.path.exists(os.path.join(self.generate_outputdir, "structure.xyz")): + self.is_prepared = True + time.sleep(interval) + if not self.is_prepared: + raise RuntimeError(f"{self.generate_outputdir}") + + def train(self, train_dir = "train"): + if not self.is_prepared: + raise RuntimeError("you have to prepare the trainer before training!") + if os.path.exists(train_dir): + shutil.rmtree(train_dir) + shutil.copytree(self.train_inputdir, train_dir) + os.chdir(train_dir) + + os.rename( + os.path.join(self.generate_outputdir, "structure.xyz"), + os.path.join(os.getcwd(), "structure.xyz"), + ) + + with open(os.path.join(os.getcwd(), "stdout"), "w") as fi: + subprocess.run( + self.train_exe, stdout=fi, stderr=subprocess.STDOUT, check=True + ) + os.chdir(pathlib.Path(os.getcwd()).parent) + self.is_trained = True + + def new_baseinput(self, baseinput_dir, train_dir = "train"): + try: + assert self.is_trained + except AssertionError as e: + e.args += "you have to train before getting results!" + + baseinput = str(pathlib.Path(baseinput_dir).resolve()) + os.makedirs(baseinput, exist_ok=True) + shutil.copy(os.path.join(train_dir,"input.yaml"),baseinput) + os.chdir(train_dir) + with open("input.yaml", "r") as yml: + yaml_dic = yaml.safe_load(yml) + runname = yaml_dic["name"] + shutil.copy("{}.model".format(runname),os.path.join(baseinput,"deployed.model")) + os.chdir(pathlib.Path(os.getcwd()).parent) diff --git a/abics/applications/latgas_abinitio_interface/sevennet.py b/abics/applications/latgas_abinitio_interface/sevennet.py new file mode 100644 index 00000000..c2248c58 --- /dev/null +++ b/abics/applications/latgas_abinitio_interface/sevennet.py @@ -0,0 +1,218 @@ +# ab-Initio Configuration Sampling tool kit (abICS) +# Copyright (C) 2019- The University of Tokyo +# +# abICS wrapper of Sevennet +# Masashi Noda, Yusuke Konishi (Academeia Co., Ltd.) 2025 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. + +""" +energy calculator using sevennet python interface +""" + +from __future__ import annotations + +import os.path +from collections import namedtuple +import numpy as np +from pymatgen.core import Structure +import torch +from ase import Atoms +from ase.optimize import BFGS +from nequip.data import AtomicDataDict, AtomicData +from nequip.utils import Config +from sevenn.sevennet_calculator import SevenNetCalculator + +from .base_solver import SolverBase, register_solver +from .params import ALParams, DFTParams + +import tomli + +class SevennetSolver(SolverBase): + """ + Nequip solver + + Attributes + ---------- + path_to_solver : str + Path to the solver + input : NequipSolver.Input + Input manager + output : NequipSolver.Output + Output manager + """ + + def __init__(self, ignore_species, use_pretrained, relax, fmax, device, pretrained): + """ + Initialize the solver. + + """ + + super(SevennetSolver, self).__init__("") + self.path_to_solver = self.calculate_energy + self.input = SevennetSolver.Input(ignore_species, use_pretrained, relax, fmax, device, pretrained) + self.output = SevennetSolver.Output() + + def name(self): + return "sevennet" + + def calculate_energy(self, fi, output_dir): + st = self.input.st + symbols = [site.specie.symbol for site in st] + positions = [site.coords for site in st] + pbc = (True, True, True) + cell = st.lattice.matrix + atoms = Atoms(symbols=symbols, positions=positions, pbc=pbc, cell=cell) + + atoms.calc = self.input.calculator + + if self.input.relax: + opt = BFGS(atoms) + opt.run(fmax=self.input.fmax) + + # Get predicted energy + ene = atoms.get_total_energy() + + self.output.st = st + self.output.ene = ene + + class Input(object): + """ + Input manager for Mock + + Attributes + ---------- + st : pymatgen.Structure + structure + """ + + st: Structure + + def __init__(self, ignore_species=None, use_pretrained=False, relax=False, fmax=0.1, device="cpu", pretrained="7net-0"): + self.ignore_species = ignore_species + self.use_pretrained = use_pretrained + self.relax = relax + self.fmax = fmax + self.device = device + self.pretrained = pretrained + # self.st = Structure() + + def from_directory(self, base_input_dir): + """ + + Parameters + ---------- + base_input_dir : str + Path to the directory including base input files. + """ + self.base_input_dir = base_input_dir + if not self.use_pretrained: + self.calculator = SevenNetCalculator(os.path.join(base_input_dir, "deployed.pth"), device=self.device) + else: + self.calculator = SevenNetCalculator(model=self.pretrained, device=self.device) + + def update_info_by_structure(self, structure): + """ + Update information by structure file + + Parameters + ---------- + structure : pymatgen.Structure + Atomic structure + """ + self.st = structure.copy() + if self.ignore_species is not None: + self.st.remove_species(self.ignore_species) + + def update_info_from_files(self, workdir, rerun): + """ + Do nothing + """ + pass + + def write_input(self, output_dir): + """ + Generate input files of the solver program. + + Parameters + ---------- + workdir : str + Path to working directory. + """ + if not os.path.exists(output_dir): + import shutil + + shutil.copytree(self.base_input_dir, output_dir) + + # self.st.to("POSCAR", os.path.join(output_dir, "structure.vasp")) + + def cl_args(self, nprocs, nthreads, workdir): + """ + Generate command line argument of the solver program. + + Parameters + ---------- + nprocs : int + The number of processes. + nthreads : int + The number of threads. + workdir : str + Path to the working directory. + + Returns + ------- + args : list[str] + Arguments of command + """ + return [workdir] + + class Output(object): + """ + Output manager. + """ + + def __init__(self): + pass + + def get_results(self, workdir): + """ + Get energy and structure obtained by the solver program. + + Parameters + ---------- + workdir : str + Path to the working directory. + + Returns + ------- + phys : named_tuple("energy", "structure") + Total energy and atomic structure. + The energy is measured in the units of eV + and coodinates is measured in the units of Angstrom. + """ + Phys = namedtuple("PhysVaules", ("energy", "structure")) + return Phys(self.ene, self.st) + + def solver_run_schemes(self): + return ("function",) + + @classmethod + def create(cls, params: ALParams | DFTParams): + ignore_species = params.ignore_species + use_pretrained = params.use_pretrained + relax = params.relax + fmax = params.fmax + device = params.device + pretrained = params.pretrained + return cls(ignore_species, use_pretrained, relax, fmax, device, pretrained) diff --git a/abics/applications/latgas_abinitio_interface/sevennet_trainer.py b/abics/applications/latgas_abinitio_interface/sevennet_trainer.py new file mode 100644 index 00000000..4d39b783 --- /dev/null +++ b/abics/applications/latgas_abinitio_interface/sevennet_trainer.py @@ -0,0 +1,168 @@ +# ab-Initio Configuration Sampling tool kit (abICS) +# Copyright (C) 2019- The University of Tokyo +# +# abICS wrapper of SevenNet solver +# Masashi Noda, Yusuke Konishi (Academeia Co., Ltd.) 2025 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. + +from __future__ import annotations +from typing import Sequence, Dict + +import numpy as np +import os, pathlib, shutil, subprocess, shlex +import time + +from pymatgen.core import Structure + +from abics.util import expand_cmd_path +from abics.applications.latgas_abinitio_interface.base_trainer import TrainerBase +from abics.applications.latgas_abinitio_interface.util import structure_to_XSF + +import ase +from ase import io +from ase.calculators.singlepoint import SinglePointCalculator +from ase.calculators.morse import MorsePotential + +def xsf_to_ase(xsf): + ase_xsf = ase.io.read(xsf) + with open(xsf) as f: + lines = f.readlines() + + tot_energy = float(lines[0].split()[4]) + ase_xsf.calc = SinglePointCalculator(energy=tot_energy, atoms=ase_xsf) + ase_xsf.calc = MorsePotential() + forces = ase_xsf.get_forces() + ase_xsf.calc = SinglePointCalculator(energy=tot_energy, forces=forces, atoms=ase_xsf) + return ase_xsf + +class SevennetTrainer(TrainerBase): + def __init__( + self, + structures: Sequence[Structure], + energies: Sequence[float], + generate_inputdir: os.PathLike, + train_inputdir: os.PathLike, + predict_inputdir: os.PathLike, + execute_commands: Dict, + # trainer_type: str, + ): + self.structures = structures + self.energies = energies + self.generate_inputdir = generate_inputdir + self.train_inputdir = train_inputdir + self.predict_inputdir = predict_inputdir + train_exe = execute_commands["train"] + self.train_exe = [expand_cmd_path(e) for e in shlex.split(train_exe)] + self.train_exe.append("input.yaml") + assert len(self.structures) == len(self.energies) + self.numdata = len(self.structures) + self.is_prepared = False + self.is_trained = False + self.generate_outputdir = None + # self.trainer_type = trainer_type + + def prepare(self, latgas_mode = True, st_dir = "sevennetXSF"): + rootdir = os.getcwd() + xsfdir = os.path.join(rootdir, st_dir) + + # prepare XSF files for nequip + os.makedirs(xsfdir, exist_ok=True) + os.chdir(xsfdir) + xsfdir = os.getcwd() + if latgas_mode: + for i, st in enumerate(self.structures): + xsf_string = structure_to_XSF(st, write_force_zero=False) + xsf_string = ( + "# total energy = {} eV\n\n".format(self.energies[i]) + xsf_string + ) + with open("structure.{}.xsf".format(i), "w") as fi: + fi.write(xsf_string) + else: + for i, st in enumerate(self.structures): + xsf_string = structure_to_XSF(st, write_force_zero=False) + xsf_string = ( + "# total energy = {} eV\n\n".format(self.energies[i]) + xsf_string + ) + with open("structure.{}.xsf".format(i), "w") as fi: + fi.write(xsf_string) + + os.chdir(rootdir) + + def generate_run(self, xsfdir="sevennetXSF", generate_dir="generate"): + # prepare generate + xsfdir = str(pathlib.Path(xsfdir).resolve()) + if os.path.exists(generate_dir): + shutil.rmtree(generate_dir) + # shutil.copytree(self.generate_inputdir, generate_dir) + os.makedirs(generate_dir, exist_ok=True) + self.generate_dir = generate_dir + os.chdir(generate_dir) + xsf_paths = [ + os.path.join(xsfdir, "structure.{}.xsf".format(i)) + for i in range(self.numdata) + ] + ases = [xsf_to_ase(xsf) for xsf in xsf_paths] + #generate structure.xyz + ase.io.write("structure.xyz", ases) + self.generate_outputdir = os.getcwd() + os.chdir(pathlib.Path(os.getcwd()).parent) + + def generate_wait(self): + interval = 0.1 # sec + self.is_prepared = False + if os.path.exists(os.path.join(self.generate_outputdir, "structure.xyz")): + self.is_prepared = True + time.sleep(interval) + if not self.is_prepared: + raise RuntimeError(f"{self.generate_outputdir}") + + def train(self, train_dir = "train"): + if not self.is_prepared: + raise RuntimeError("you have to prepare the trainer before training!") + if os.path.exists(train_dir): + shutil.rmtree(train_dir) + shutil.copytree(self.train_inputdir, train_dir) + os.chdir(train_dir) + + os.rename( + os.path.join(self.generate_outputdir, "structure.xyz"), + os.path.join(os.getcwd(), "structure.xyz"), + ) + + with open(os.path.join(os.getcwd(), "stdout"), "w") as fi: + subprocess.run( + self.train_exe, stdout=fi, stderr=subprocess.STDOUT, check=True + ) + os.chdir(pathlib.Path(os.getcwd()).parent) + self.is_trained = True + + def new_baseinput(self, baseinput_dir, train_dir = "train"): + try: + assert self.is_trained + except AssertionError as e: + e.args += "you have to train before getting results!" + + baseinput = str(pathlib.Path(baseinput_dir).resolve()) + os.makedirs(baseinput, exist_ok=True) + shutil.copy(os.path.join(train_dir,"input.yaml"),baseinput) + os.chdir(train_dir) + checkpoint_files = [f for f in os.listdir() if f.startswith("checkpoint_") and f.endswith(".pth")] + checkpoint_nums = [int(f.replace("checkpoint_", "").replace(".pth", "")) for f in checkpoint_files] + max_checkpoint_num = max(checkpoint_nums) + checkpoint_file = f"checkpoint_{max_checkpoint_num}.pth" + #subprocess.run(["sevenn_get_model", checkpoint_file], check=True) + #shutil.copy("deployed_serial.pt", os.path.join(baseinput, "deployed.pt")) + shutil.copy(checkpoint_file, os.path.join(baseinput, "deployed.pth")) + os.chdir(pathlib.Path(os.getcwd()).parent) diff --git a/abics/scripts/main_dft_latgas.py b/abics/scripts/main_dft_latgas.py index ff5d43f8..03a1564c 100644 --- a/abics/scripts/main_dft_latgas.py +++ b/abics/scripts/main_dft_latgas.py @@ -295,6 +295,13 @@ def main_dft_latgas(params_root: MutableMapping): ALrun = exists_on_all_nodes(commAll, "ALloop.progress") + # if use_pretrained is True, make directory dftparams.base_input_dir + if dftparams.use_pretrained: + if comm.Get_rank() == 0: + for dir in dftparams.base_input_dir: + if not os.path.exists(dir): + os.mkdir(dir) + # Active learning mode if ALrun: logger.info(f"-Running in active learning mode.") diff --git a/docs/sphinx/ja/source/how_to_use/index.rst b/docs/sphinx/ja/source/how_to_use/index.rst index 707fc1a3..06514582 100644 --- a/docs/sphinx/ja/source/how_to_use/index.rst +++ b/docs/sphinx/ja/source/how_to_use/index.rst @@ -180,6 +180,47 @@ MLIP-3 - MLIP-3用の入力ファイル ``input.almtp`` を ``[train]`` セクションの ``base_input_dir`` で設定したディレクトリ内の ``train`` ディレクトリに設置してください。 +SevenNet +*********** + +- URL : https://github.com/MDIL-SNU/SevenNet + +- SevenNet 0.10.1 で動作確認済。 + +- 参照ファイル(参照ファイルの具体例についてはチュートリアル参照) + + - 学習済みモデルを使用する場合: abicsのinput.tomlだけで問題ありません + + - 学習を行う場合: SevenNetのinputファイル ``input.yaml`` を ``[train]`` セクションの ``base_input_dir`` で設定したディレクトリ内の ``train`` ディレクトリに設置してください。 + +Mace +*********** + +- URL : https://github.com/ACEsuit/mace + +- Mace 0.3.8 で動作確認済。 + +- 参照ファイル(参照ファイルの具体例についてはチュートリアル参照) + + - 学習済みモデルを使用する場合: abicsのinput.tomlだけで問題ありません + + - 学習を行う場合: Maceのinputファイル ``input.yaml`` を ``[train]`` セクションの ``base_input_dir`` で設定したディレクトリ内の ``train`` ディレクトリに設置してください。 + +CHGNet +*********** + +- URL : https://chgnet.lbl.gov/ + +- CHGNet 0.3.8 で動作確認済。 + +- 参照ファイル(参照ファイルの具体例についてはチュートリアル参照) + + - 学習済みモデルを使用する場合: abicsのinput.tomlだけで問題ありません + + - 学習を行う場合: inputファイル ``input.yaml`` を ``[train]`` セクションの ``base_input_dir`` で設定したディレクトリ内の ``train`` ディレクトリに設置してください。 + + - こちらの ``input.yaml`` はCHGNet側のinputファイルとは異なり、abICS側で定義された形式となっております。詳しくはチュートリアルを参照してください。 + 学習データの作成 ------------------- diff --git a/docs/sphinx/ja/source/tutorial/other_models.rst b/docs/sphinx/ja/source/tutorial/other_models.rst index b1320d83..4dccfaf7 100644 --- a/docs/sphinx/ja/source/tutorial/other_models.rst +++ b/docs/sphinx/ja/source/tutorial/other_models.rst @@ -294,3 +294,304 @@ MLIP-3の実行ファイル ``mlp`` のパスを指定します。お使いの alpha_moment_mapping = {0, 4, 5, 6, 7} モデル学習、サンプリングの方法に関してはaenetと同様です。 + +SevenNetを利用したサンプリング +---------------------------------------------- + +SevenNetのインストール +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``sevennet`` の利用には、 SevenNetのインストールが必要です。 + +下記コマンドにてインストールします。 + +.. code-block:: bash + + $ python3 -m pip install sevenn + +学習済みモデルの利用 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +SevenNetでは、モデルを学習してからサンプリングを行う事以外に、 +学習済みモデルを利用してサンプリングを行う事も可能です。 + +学習済みモデルを用いる場合は、 ``[sanmping.solver]`` セクションを下記のように設定します。 + +.. code-block:: toml + + [sampling.solver] + type = 'sevennet' + perturb = 0.0 + +サンプリングの方法については、aenetと同様です。 + +モデル学習から実行する場合 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +モデルの学習から実行する場合は、 ``[train]`` セクションを適切に設定の上で、 +``[sanmping.solver]`` セクションに、 +``use_pretrained = false`` を追加する必要があります。 +``relax = false`` とする事で、構造の最適化を行わずにサンプリングを行う事も可能です。 + +.. code-block:: toml + + [sampling.solver] + type = 'sevennet' + perturb = 0.0 + base_input_dir = './baseinput_sevennet' + use_pretrained = false + + [train] + type = 'sevennet' + base_input_dir = './sevennet_train_input' + exe_command = ['', 'sevenn'] + vac_map = [] + restart = false + +また、SevenNetのインプットファイル ``input.yaml`` を ``sevennet_train_input/train`` ディレクトリに作成します。 +ここではコマンドsevennのインプットファイルを作成しています。 +各パラメータの詳しい説明はSevenNetのドキュメントを参照してください。 + +.. code-block:: yaml + + model: # model keys should be consistent except for train_* keys + chemical_species: 'Auto' + cutoff: 5.0 + channel: 128 + is_parity: False + lmax: 2 + num_convolution_layer: 5 + irreps_manual: + - "128x0e" + - "128x0e+64x1e+32x2e" + - "128x0e+64x1e+32x2e" + - "128x0e+64x1e+32x2e" + - "128x0e+64x1e+32x2e" + - "128x0e" + + weight_nn_hidden_neurons: [64, 64] + radial_basis: + radial_basis_name: 'bessel' + bessel_basis_num: 8 + cutoff_function: + cutoff_function_name: 'XPLOR' + cutoff_on: 4.5 + self_connection_type: 'linear' + + train_shift_scale: False # customizable (True | False) + train_denominator: False # customizable (True | False) + + train: # Customizable + random_seed: 1 + is_train_stress: False + epoch: 5 + + optimizer: 'adam' + optim_param: + lr: 0.004 + scheduler: 'exponentiallr' + scheduler_param: + gamma: 0.99 + + force_loss_weight: 0.1 + stress_loss_weight: 1e-06 + + per_epoch: 1 # Generate checkpoints every this epoch + + # ['target y', 'metric'] + # Target y: TotalEnergy, Energy, Force, Stress, Stress_GPa, TotalLoss + # Metric : RMSE, MAE, or Loss + error_record: + - ['Energy', 'RMSE'] + - ['TotalLoss', 'None'] + + continue: + reset_optimizer: True + reset_scheduler: True + reset_epoch: True + checkpoint: 'SevenNet-0_11July2024' + + data: # Customizable + batch_size: 4 + data_divide_ratio: 0.1 + + # SevenNet automatically matches data format from its filename. + # For those not `structure_list` or `.pt` files, assumes it is ASE readable + # In this case, below arguments are directly passed to `ase.io.read` + data_format_args: + index: ':' # see `https://wiki.fysik.dtu.dk/ase/ase/io/io.html` for more valid arguments + + # validset is needed if you want '_best.pth' during training. If not, both validset and testset is optional. + load_trainset_path: ['./structure.xyz'] # Example of using ase as data_format, support multiple files and expansion(*) + +モデル学習、サンプリングの方法に関してはaenetと同様です。 + +Maceを利用したサンプリング +---------------------------------------------- + +Maceのインストール +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``mace`` の利用には、 Maceのインストールが必要です。 + +下記コマンドにてインストールします。 + +.. code-block:: bash + + $ python3 -m pip install mace + +モデル学習から実行する場合 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Maceでは、モデルを学習してからサンプリングを行う事以外に、 +学習済みモデルを利用してサンプリングを行う事も可能です。 + +学習済みモデルを用いる場合は、 ``[sanmping.solver]`` セクションを下記のように設定します。 + +.. code-block:: toml + + [sampling.solver] + type = 'mace' + perturb = 0.0 + +サンプリングの方法については、aenetと同様です。 + +モデル学習から実行する場合 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +モデルの学習から実行する場合は、 ``[train]`` セクションを適切に設定の上で、 +``[sanmping.solver]`` セクションに、 +``use_pretrained = false`` を追加する必要があります。 +``relax = false`` とする事で、構造の最適化を行わずにサンプリングを行う事も可能です。 + +.. code-block:: toml + + [sampling.solver] + type = 'mace' + perturb = 0.0 + base_input_dir = './baseinput_mace' + use_pretrained = false + + [train] + type = 'mace' + base_input_dir = './mace_train_input' + exe_command = ['', 'mace_run_train'] + vac_map = [] + restart = false + +また、Maceのインプットファイル ``input.yaml`` を ``mace_train_input/train`` ディレクトリに作成します。 +ここではコマンドmace_run_trainのインプットファイルを作成しています。 +各パラメータの詳しい説明はMaceのドキュメントを参照してください。 + +.. code-block:: yaml + + name: spinel + foundation_model: "small" + seed: 2024 + train_file: structure.xyz + swa: yes + start_swa: 1200 + max_num_epochs: 5 + device: cpu + E0s: + 8: -2042.0 + 12: -1750.0 + 13: -1750.0 + energy_weight: 1.0 + forces_weight: 0.0 + stress_weight: 0.0 + +モデル学習、サンプリングの方法に関してはaenetと同様です。 + +CHGNetを利用したサンプリング +---------------------------------------------- + +CHGNetのインストール +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``chgnet`` の利用には、 CHGNetのインストールが必要です。 + +下記コマンドにてインストールします。 + +.. code-block:: bash + + $ python3 -m pip install chgnet + +学習済みモデルの利用 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +CHGNetでは、モデルを学習してからサンプリングを行う事以外に、 +学習済みモデルを利用してサンプリングを行う事も可能です。 + +学習済みモデルを用いる場合は、 ``[sanmping.solver]`` セクションを下記のように設定します。 + +.. code-block:: toml + + [sampling.solver] + type = 'chgnet' + perturb = 0.0 + +サンプリングの方法については、aenetと同様です。 + +モデル学習から実行する場合 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +モデルの学習から実行する場合は、 ``[train]`` セクションを適切に設定の上で、 +``[sanmping.solver]`` セクションに、 +``use_pretrained = false`` を追加する必要があります。 +``relax = false`` とする事で、構造の最適化を行わずにサンプリングを行う事も可能です。 + +.. code-block:: toml + + [sampling.solver] + type = 'chgnet' + perturb = 0.0 + base_input_dir = './baseinput_chgnet' + use_pretrained = false + + [train] + type = 'chgnet' + base_input_dir = './chgnet_train_input' + exe_command = ['', 'chgnet'] + vac_map = [] + restart = false + +また、CHGNetのインプットファイル ``input.yaml`` を ``chgnet_train_input/train`` ディレクトリに作成します。 + +.. code-block:: yaml + + finetuning : False + batch_size : 4 + train_ratio : 0.9 + val_ratio : 0.05 + learning_rate : 0.004 + epochs : 100 + model_params: + atom_fea_dim : 8 + bond_fea_dim : 8 + angle_fea_dim : 8 + num_radial : 9 + num_angular : 9 + num_conv : 2 + atom_conv_hidden_dim : 4 + bond_conv_hidden_dim : 4 + mlp_hidden_dims : + - 16 + - 16 + atom_graph_cutoff : 7.5 + bond_graph_cutoff : 6.0 + +このインプットファイルは、CHGNetの学習に必要なパラメータを設定するものであり、 +abICS側で定義されているものとなります。 +各ファイルのパラメータは下記の通りとなります。 + +- finetuning : ファインチューニングを行うかどうか。デフォルト値: True +- batch_size : バッチサイズ。デフォルト値: 4 +- train_ratio : 学習データの割合。デフォルト値: 0.9 +- val_ratio : 検証データの割合。デフォルト値: 0.05 +- learning_rate : 学習率。デフォルト値: 0.01 +- epochs : エポック数。デフォルト値: 5 +- model_params: finetuningがFalseの時に、CHGNetのパラメータとして使用されます。パラメータに関しては https://chgnet.lbl.gov/api#class-chgnet を参照してください。 + +モデル学習、サンプリングの方法に関してはaenetと同様です。 + From 936ac714d9fa3caa69f245ac938d32f8a807b921 Mon Sep 17 00:00:00 2001 From: Yusuke KONISHI Date: Fri, 21 Mar 2025 09:06:56 +0900 Subject: [PATCH 2/7] update params.py and train.py --- .../applications/latgas_abinitio_interface/params.py | 12 ++++++++++++ abics/scripts/train.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/abics/applications/latgas_abinitio_interface/params.py b/abics/applications/latgas_abinitio_interface/params.py index 7bbd106c..93656d37 100644 --- a/abics/applications/latgas_abinitio_interface/params.py +++ b/abics/applications/latgas_abinitio_interface/params.py @@ -33,6 +33,11 @@ def __init__(self): self.ensemble = False self.par_ensemble = False self.use_tmpdir = False + self.use_pretrained = True + self.relax = True + self.fmax = 0.1 + self.dvice = "cpu" + self.pretrained = None @classmethod def from_dict(cls, d): @@ -64,6 +69,9 @@ def from_dict(cls, d): else: params.path = "" + if params.solver == "sevennet": + params.pretrained = d.get("pretrained", "7net-0") + params.perturb = d.get("perturb", 0.1) params.ignore_species = d.get("ignore_species", None) params.constraint_module = d.get("constraint_module", None) @@ -71,6 +79,10 @@ def from_dict(cls, d): params.ensemble = d.get("ensemble", False) params.par_ensemble = d.get("par_ensemble", False) params.use_tmpdir = d.get("use_tmpdir", True) + params.use_pretrained = d.get("use_pretrained", True) + params.relax = d.get("relax", True) + params.fmax = d.get("fmax", 0.1) + params.device = d.get("device", "cpu") params.properties = d return params diff --git a/abics/scripts/train.py b/abics/scripts/train.py index eb9c0081..5fe9b7c3 100644 --- a/abics/scripts/train.py +++ b/abics/scripts/train.py @@ -83,7 +83,7 @@ def main_impl(params_root: MutableMapping): - if trainer_type not in ["aenet", "allegro", "nequip", "mlip_3"]: + if trainer_type not in ["aenet", "allegro", "nequip", "mlip_3", "sevennet", "mace", "chgnet"]: logger.error("Unknown trainer: ", trainer_type) sys.exit(1) From ccb718c4666fd381e05192207f892307e6948855 Mon Sep 17 00:00:00 2001 From: Kazuyoshi Yoshimi Date: Tue, 16 Jun 2026 15:32:20 +0900 Subject: [PATCH 3/7] Fix issues in SevenNet/Mace/CHGNet support (review of #76) - params.py: fix typo self.dvice -> self.device (device was never set on the default DFTParams path) - main_dft_latgas.py: restrict the pretrained base_input_dir creation to the MLIP solvers (sevennet/mace/chgnet) so existing solvers are not affected; use makedirs(exist_ok=True) - sevennet/mace/chgnet solvers: drop unused imports (numpy, torch, tomli and the nequip imports that pulled nequip in as a hard dependency) - chgnet.py: correct copy-pasted module docstring - *_trainer.py: replace broken 'e.args += str' handling (TypeError) in new_baseinput with a proper RuntimeError - __init__.py: add trailing newline Co-Authored-By: Claude Opus 4.8 (1M context) --- abics/applications/latgas_abinitio_interface/__init__.py | 2 +- abics/applications/latgas_abinitio_interface/chgnet.py | 6 +----- .../latgas_abinitio_interface/chgnet_trainer.py | 8 +++----- abics/applications/latgas_abinitio_interface/mace.py | 4 ---- .../latgas_abinitio_interface/mace_trainer.py | 8 +++----- abics/applications/latgas_abinitio_interface/params.py | 4 ++-- abics/applications/latgas_abinitio_interface/sevennet.py | 5 ----- .../latgas_abinitio_interface/sevennet_trainer.py | 8 +++----- abics/scripts/main_dft_latgas.py | 7 ++++--- 9 files changed, 17 insertions(+), 35 deletions(-) diff --git a/abics/applications/latgas_abinitio_interface/__init__.py b/abics/applications/latgas_abinitio_interface/__init__.py index 03d268b6..2c58d28f 100644 --- a/abics/applications/latgas_abinitio_interface/__init__.py +++ b/abics/applications/latgas_abinitio_interface/__init__.py @@ -36,4 +36,4 @@ register_trainer("mlip_3", "MLIP3Trainer", "abics.applications.latgas_abinitio_interface.mlip_3_trainer") register_trainer("sevennet", "SevennetTrainer", "abics.applications.latgas_abinitio_interface.sevennet_trainer") register_trainer("mace", "MaceTrainer", "abics.applications.latgas_abinitio_interface.mace_trainer") -register_trainer("chgnet", "CHGNetTrainer", "abics.applications.latgas_abinitio_interface.chgnet_trainer") \ No newline at end of file +register_trainer("chgnet", "CHGNetTrainer", "abics.applications.latgas_abinitio_interface.chgnet_trainer") diff --git a/abics/applications/latgas_abinitio_interface/chgnet.py b/abics/applications/latgas_abinitio_interface/chgnet.py index 7e8a07ed..690d8f29 100644 --- a/abics/applications/latgas_abinitio_interface/chgnet.py +++ b/abics/applications/latgas_abinitio_interface/chgnet.py @@ -18,7 +18,7 @@ # along with this program. If not, see http://www.gnu.org/licenses/. """ -energy calculator using sevennet python interface +energy calculator using chgnet python interface """ from __future__ import annotations @@ -26,10 +26,6 @@ import os.path from collections import namedtuple from pymatgen.core import Structure -import torch -from ase import Atoms -from ase.calculators.calculator import Calculator -from nequip.utils import Config from chgnet.model.model import CHGNet from chgnet.model import StructOptimizer diff --git a/abics/applications/latgas_abinitio_interface/chgnet_trainer.py b/abics/applications/latgas_abinitio_interface/chgnet_trainer.py index 6196af3d..9b2e2798 100644 --- a/abics/applications/latgas_abinitio_interface/chgnet_trainer.py +++ b/abics/applications/latgas_abinitio_interface/chgnet_trainer.py @@ -222,11 +222,9 @@ def train(self, train_dir = "train"): self.is_trained = True def new_baseinput(self, baseinput_dir, train_dir = "train"): - try: - assert self.is_trained - except AssertionError as e: - e.args += "you have to train before getting results!" - + if not self.is_trained: + raise RuntimeError("you have to train before getting results!") + baseinput = str(pathlib.Path(baseinput_dir).resolve()) os.makedirs(baseinput, exist_ok=True) shutil.copy(os.path.join(train_dir,"input.yaml"),baseinput) diff --git a/abics/applications/latgas_abinitio_interface/mace.py b/abics/applications/latgas_abinitio_interface/mace.py index a5b3cb7d..ed1cd8ea 100644 --- a/abics/applications/latgas_abinitio_interface/mace.py +++ b/abics/applications/latgas_abinitio_interface/mace.py @@ -25,13 +25,9 @@ import os.path from collections import namedtuple -import numpy as np from pymatgen.core import Structure -import torch from ase import Atoms from ase.optimize import BFGS -from nequip.data import AtomicDataDict, AtomicData -from nequip.utils import Config from mace.calculators import mace_mp, MACECalculator from .base_solver import SolverBase, register_solver diff --git a/abics/applications/latgas_abinitio_interface/mace_trainer.py b/abics/applications/latgas_abinitio_interface/mace_trainer.py index 58ce7ba9..b4d54759 100644 --- a/abics/applications/latgas_abinitio_interface/mace_trainer.py +++ b/abics/applications/latgas_abinitio_interface/mace_trainer.py @@ -149,11 +149,9 @@ def train(self, train_dir = "train"): self.is_trained = True def new_baseinput(self, baseinput_dir, train_dir = "train"): - try: - assert self.is_trained - except AssertionError as e: - e.args += "you have to train before getting results!" - + if not self.is_trained: + raise RuntimeError("you have to train before getting results!") + baseinput = str(pathlib.Path(baseinput_dir).resolve()) os.makedirs(baseinput, exist_ok=True) shutil.copy(os.path.join(train_dir,"input.yaml"),baseinput) diff --git a/abics/applications/latgas_abinitio_interface/params.py b/abics/applications/latgas_abinitio_interface/params.py index 93656d37..2a827817 100644 --- a/abics/applications/latgas_abinitio_interface/params.py +++ b/abics/applications/latgas_abinitio_interface/params.py @@ -36,7 +36,7 @@ def __init__(self): self.use_pretrained = True self.relax = True self.fmax = 0.1 - self.dvice = "cpu" + self.device = "cpu" self.pretrained = None @classmethod @@ -70,7 +70,7 @@ def from_dict(cls, d): params.path = "" if params.solver == "sevennet": - params.pretrained = d.get("pretrained", "7net-0") + params.pretrained = d.get("pretrained", "7net-0") params.perturb = d.get("perturb", 0.1) params.ignore_species = d.get("ignore_species", None) diff --git a/abics/applications/latgas_abinitio_interface/sevennet.py b/abics/applications/latgas_abinitio_interface/sevennet.py index c2248c58..d64b62b0 100644 --- a/abics/applications/latgas_abinitio_interface/sevennet.py +++ b/abics/applications/latgas_abinitio_interface/sevennet.py @@ -25,19 +25,14 @@ import os.path from collections import namedtuple -import numpy as np from pymatgen.core import Structure -import torch from ase import Atoms from ase.optimize import BFGS -from nequip.data import AtomicDataDict, AtomicData -from nequip.utils import Config from sevenn.sevennet_calculator import SevenNetCalculator from .base_solver import SolverBase, register_solver from .params import ALParams, DFTParams -import tomli class SevennetSolver(SolverBase): """ diff --git a/abics/applications/latgas_abinitio_interface/sevennet_trainer.py b/abics/applications/latgas_abinitio_interface/sevennet_trainer.py index 4d39b783..54d878e9 100644 --- a/abics/applications/latgas_abinitio_interface/sevennet_trainer.py +++ b/abics/applications/latgas_abinitio_interface/sevennet_trainer.py @@ -149,11 +149,9 @@ def train(self, train_dir = "train"): self.is_trained = True def new_baseinput(self, baseinput_dir, train_dir = "train"): - try: - assert self.is_trained - except AssertionError as e: - e.args += "you have to train before getting results!" - + if not self.is_trained: + raise RuntimeError("you have to train before getting results!") + baseinput = str(pathlib.Path(baseinput_dir).resolve()) os.makedirs(baseinput, exist_ok=True) shutil.copy(os.path.join(train_dir,"input.yaml"),baseinput) diff --git a/abics/scripts/main_dft_latgas.py b/abics/scripts/main_dft_latgas.py index 03a1564c..568632df 100644 --- a/abics/scripts/main_dft_latgas.py +++ b/abics/scripts/main_dft_latgas.py @@ -295,12 +295,13 @@ def main_dft_latgas(params_root: MutableMapping): ALrun = exists_on_all_nodes(commAll, "ALloop.progress") - # if use_pretrained is True, make directory dftparams.base_input_dir - if dftparams.use_pretrained: + # For pretrained MLIP solvers no base input files are supplied, so create + # the (empty) base_input_dir that write_input() will copy from. + if dftparams.use_pretrained and dftparams.solver in ("sevennet", "mace", "chgnet"): if comm.Get_rank() == 0: for dir in dftparams.base_input_dir: if not os.path.exists(dir): - os.mkdir(dir) + os.makedirs(dir, exist_ok=True) # Active learning mode if ALrun: From 151c07fc147efbbdc40ed5df54667a3db327a2a5 Mon Sep 17 00:00:00 2001 From: Kazuyoshi Yoshimi Date: Sun, 21 Jun 2026 14:44:38 +0900 Subject: [PATCH 4/7] Add CHGNet pretrained sampling smoke test Add a lightweight integration test that runs abics_sampling with the pretrained CHGNet solver (use_pretrained=true, relax=false) over a small MgAl2O4 cell for a few RXMC steps, and checks that sampling produces finite, physically plausible energies. CHGNet ships its weights, so the test needs no network at runtime. Wired into the CI matrix as the SamplingCHGNet testname with install_chgnet.sh. chgnet requires Python >=3.10, so it runs only on Python 3.13 (chgnet supports numpy 2, unlike the pinned nequip). Verified locally (Python 3.13, chgnet 0.4.2 / torch 2.12.1): the 2-replica sampling run completes and check.py passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/Test_abICS.yml | 9 +- .../integration/sampling_chgnet/MgAl2O4.vasp | 64 +++++++++++ tests/integration/sampling_chgnet/check.py | 18 ++++ tests/integration/sampling_chgnet/input.toml | 101 ++++++++++++++++++ .../sampling_chgnet/install_chgnet.sh | 13 +++ tests/integration/sampling_chgnet/run.sh | 12 +++ 6 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 tests/integration/sampling_chgnet/MgAl2O4.vasp create mode 100644 tests/integration/sampling_chgnet/check.py create mode 100644 tests/integration/sampling_chgnet/input.toml create mode 100644 tests/integration/sampling_chgnet/install_chgnet.sh create mode 100644 tests/integration/sampling_chgnet/run.sh diff --git a/.github/workflows/Test_abICS.yml b/.github/workflows/Test_abICS.yml index 14225472..adf7898d 100644 --- a/.github/workflows/Test_abICS.yml +++ b/.github/workflows/Test_abICS.yml @@ -17,7 +17,7 @@ jobs: matrix: python-version: ['3.9', '3.13'] numpy-version: ['1.20.0', '1.26.4', 'latest'] - testname: [Unit, Sampling, ActiveLearnAenet, ActiveLearnNequip, ActiveLearnMLIP-3] + testname: [Unit, Sampling, ActiveLearnAenet, ActiveLearnNequip, ActiveLearnMLIP-3, SamplingCHGNet] exclude: - python-version: '3.13' numpy-version: '1.20.0' @@ -33,6 +33,10 @@ jobs: # incompatible rewrite). See the nequip cap in pyproject.toml. - python-version: '3.13' testname: ActiveLearnNequip + # chgnet requires Python >=3.10, so the CHGNet pretrained smoke test + # runs only on Python 3.13. Unlike nequip, chgnet supports numpy 2. + - python-version: '3.9' + testname: SamplingCHGNet fail-fast: false steps: @@ -76,5 +80,8 @@ jobs: ActiveLearnMLIP-3 ) cd tests/integration/active_learn_mlip3 sh ./install_mlip3.sh sh ./run.sh ;; + SamplingCHGNet ) cd tests/integration/sampling_chgnet + sh ./install_chgnet.sh + sh ./run.sh ;; * ) echo "Unknown testname";; esac diff --git a/tests/integration/sampling_chgnet/MgAl2O4.vasp b/tests/integration/sampling_chgnet/MgAl2O4.vasp new file mode 100644 index 00000000..596dd685 --- /dev/null +++ b/tests/integration/sampling_chgnet/MgAl2O4.vasp @@ -0,0 +1,64 @@ +Al2 Mg O4 +1.0 + 8.1135997772 0.0000000000 0.0000000000 + 0.0000000000 8.1135997772 0.0000000000 + 0.0000000000 0.0000000000 8.1135997772 + O Al Mg + 32 16 8 +Direct + 0.237399980 0.237399980 0.237399980 + 0.762599945 0.762599945 0.762599945 + 0.512599945 0.012600004 0.737399936 + 0.487399966 0.987399936 0.262599975 + 0.012600004 0.737399936 0.512599945 + 0.987399936 0.262599975 0.487399966 + 0.737399936 0.512599945 0.012600004 + 0.262599975 0.487399966 0.987399936 + 0.987399936 0.487399966 0.262599975 + 0.012600004 0.512599945 0.737399936 + 0.487399966 0.262599975 0.987399936 + 0.512599945 0.737399936 0.012600004 + 0.262599975 0.987399936 0.487399966 + 0.737399936 0.012600004 0.512599945 + 0.237399980 0.737399936 0.737399936 + 0.762599945 0.262599975 0.262599975 + 0.512599945 0.512599945 0.237399980 + 0.487399966 0.487399966 0.762599945 + 0.012600004 0.237399980 0.012600004 + 0.987399936 0.762599945 0.987399936 + 0.987399936 0.987399936 0.762599945 + 0.012600004 0.012600004 0.237399980 + 0.487399966 0.762599945 0.487399966 + 0.512599945 0.237399980 0.512599945 + 0.737399936 0.237399980 0.737399936 + 0.262599975 0.762599945 0.262599975 + 0.237399980 0.512599945 0.512599945 + 0.762599945 0.487399966 0.487399966 + 0.762599945 0.987399936 0.987399936 + 0.237399980 0.012600004 0.012600004 + 0.737399936 0.737399936 0.237399980 + 0.262599975 0.262599975 0.762599945 + 0.000000000 0.000000000 0.000000000 + 0.749999940 0.249999985 0.499999970 + 0.249999985 0.749999940 0.499999970 + 0.249999985 0.499999970 0.749999940 + 0.749999940 0.499999970 0.249999985 + 0.499999970 0.749999940 0.249999985 + 0.499999970 0.249999985 0.749999940 + 0.000000000 0.499999970 0.499999970 + 0.749999940 0.749999940 0.000000000 + 0.249999985 0.249999985 0.000000000 + 0.249999985 0.000000000 0.249999985 + 0.749999940 0.000000000 0.749999940 + 0.499999970 0.000000000 0.499999970 + 0.000000000 0.749999940 0.749999940 + 0.000000000 0.249999985 0.249999985 + 0.499999970 0.499999970 0.000000000 + 0.374999970 0.374999970 0.374999970 + 0.624999940 0.624999940 0.624999940 + 0.374999970 0.874999940 0.874999940 + 0.624999940 0.124999993 0.124999993 + 0.874999940 0.874999940 0.374999970 + 0.124999993 0.124999993 0.624999940 + 0.874999940 0.374999970 0.874999940 + 0.124999993 0.624999940 0.124999993 diff --git a/tests/integration/sampling_chgnet/check.py b/tests/integration/sampling_chgnet/check.py new file mode 100644 index 00000000..3bf04928 --- /dev/null +++ b/tests/integration/sampling_chgnet/check.py @@ -0,0 +1,18 @@ +import os +import numpy as np + +# The run must have produced the RXMC temperature file and per-replica observables. +assert os.path.exists("kTs.npy"), "kTs.npy was not produced" + +obs = np.loadtxt(os.path.join("0", "obs.dat")) +obs = np.atleast_2d(obs) +assert obs.shape[0] >= 1, "no samples were recorded in 0/obs.dat" + +# Column 3 is the total energy (see obs.dat header). For the pretrained CHGNet +# smoke test we do not hard-code values (they depend on the model version); we +# only check the pipeline produced finite, physically plausible energies. +energy = obs[:, 3] +assert np.all(np.isfinite(energy)), "non-finite energies in obs.dat" +assert np.all(energy < 0.0), "expected negative total energies for MgAl2O4" + +print("OK") diff --git a/tests/integration/sampling_chgnet/input.toml b/tests/integration/sampling_chgnet/input.toml new file mode 100644 index 00000000..6da442d8 --- /dev/null +++ b/tests/integration/sampling_chgnet/input.toml @@ -0,0 +1,101 @@ +[sampling] +sampler = "RXMC" +nreplicas = 2 +nprocs_per_replica = 1 +kTstart = 1200.0 +kTend = 1500.0 +nsteps = 8 +RXtrial_frequency = 4 +sample_frequency = 2 +print_frequency = 2 +reload = false +seed = 12345 + +[sampling.solver] +type = 'chgnet' +base_input_dir = './baseinput' +use_pretrained = true +relax = false +perturb = 0.0 +seed = 31415 + +[config] +unitcell = [[8.1135997772, 0.0000000000, 0.0000000000], + [0.0000000000, 8.1135997772, 0.0000000000], + [0.0000000000, 0.0000000000, 8.1135997772]] +supercell = [1,1,1] + +[[config.base_structure]] +type = "O" +coords = [ + [0.237399980, 0.237399980, 0.237399980], + [0.762599945, 0.762599945, 0.762599945], + [0.512599945, 0.012600004, 0.737399936], + [0.487399966, 0.987399936, 0.262599975], + [0.012600004, 0.737399936, 0.512599945], + [0.987399936, 0.262599975, 0.487399966], + [0.737399936, 0.512599945, 0.012600004], + [0.262599975, 0.487399966, 0.987399936], + [0.987399936, 0.487399966, 0.262599975], + [0.012600004, 0.512599945, 0.737399936], + [0.487399966, 0.262599975, 0.987399936], + [0.512599945, 0.737399936, 0.012600004], + [0.262599975, 0.987399936, 0.487399966], + [0.737399936, 0.012600004, 0.512599945], + [0.237399980, 0.737399936, 0.737399936], + [0.762599945, 0.262599975, 0.262599975], + [0.512599945, 0.512599945, 0.237399980], + [0.487399966, 0.487399966, 0.762599945], + [0.012600004, 0.237399980, 0.012600004], + [0.987399936, 0.762599945, 0.987399936], + [0.987399936, 0.987399936, 0.762599945], + [0.012600004, 0.012600004, 0.237399980], + [0.487399966, 0.762599945, 0.487399966], + [0.512599945, 0.237399980, 0.512599945], + [0.737399936, 0.237399980, 0.737399936], + [0.262599975, 0.762599945, 0.262599975], + [0.237399980, 0.512599945, 0.512599945], + [0.762599945, 0.487399966, 0.487399966], + [0.762599945, 0.987399936, 0.987399936], + [0.237399980, 0.012600004, 0.012600004], + [0.737399936, 0.737399936, 0.237399980], + [0.262599975, 0.262599975, 0.762599945], + ] + +[[config.defect_structure]] +coords = [ + [0.000000000, 0.000000000, 0.000000000], + [0.749999940, 0.249999985, 0.499999970], + [0.249999985, 0.749999940, 0.499999970], + [0.249999985, 0.499999970, 0.749999940], + [0.749999940, 0.499999970, 0.249999985], + [0.499999970, 0.749999940, 0.249999985], + [0.499999970, 0.249999985, 0.749999940], + [0.000000000, 0.499999970, 0.499999970], + [0.749999940, 0.749999940, 0.000000000], + [0.249999985, 0.249999985, 0.000000000], + [0.249999985, 0.000000000, 0.249999985], + [0.749999940, 0.000000000, 0.749999940], + [0.499999970, 0.000000000, 0.499999970], + [0.000000000, 0.749999940, 0.749999940], + [0.000000000, 0.249999985, 0.249999985], + [0.499999970, 0.499999970, 0.000000000], + [0.374999970, 0.374999970, 0.374999970], + [0.624999940, 0.624999940, 0.624999940], + [0.374999970, 0.874999940, 0.874999940], + [0.624999940, 0.124999993, 0.124999993], + [0.874999940, 0.874999940, 0.374999970], + [0.124999993, 0.124999993, 0.624999940], + [0.874999940, 0.374999970, 0.874999940], + [0.124999993, 0.624999940, 0.124999993], + ] +[[config.defect_structure.groups]] +name = 'Al' +# species = ['Al'] # default +# coords = [[[0,0,0]]] # default +num = 16 #432 #16000 +[[config.defect_structure.groups]] +name = 'Mg' +# species = ['Mg'] # default +# coords = [[[0,0,0]]] # default +num = 8 #216 #8000 diff --git a/tests/integration/sampling_chgnet/install_chgnet.sh b/tests/integration/sampling_chgnet/install_chgnet.sh new file mode 100644 index 00000000..a4f88783 --- /dev/null +++ b/tests/integration/sampling_chgnet/install_chgnet.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# This script installs CHGNet into the python3 environment. +# CHGNet ships a pretrained model, so the smoke test needs no network at runtime. + +set -ue + +echo "python3 points to the following:" +which python3 + +echo + +python3 -m pip install chgnet diff --git a/tests/integration/sampling_chgnet/run.sh b/tests/integration/sampling_chgnet/run.sh new file mode 100644 index 00000000..14ddbae2 --- /dev/null +++ b/tests/integration/sampling_chgnet/run.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +export OMP_NUM_THREADS=1 + +set -e + +rm -f kTs.npy +rm -rf 0 1 baseinput + +mpiexec -np 2 --oversubscribe abics_sampling input.toml + +python3 ./check.py From fa12c6cbf107a21b550bae955049f6a0e1fae54d Mon Sep 17 00:00:00 2001 From: Kazuyoshi Yoshimi Date: Sun, 21 Jun 2026 16:58:03 +0900 Subject: [PATCH 5/7] Add MACE pretrained sampling smoke test Mirror the CHGNet smoke test for the MACE solver: run abics_sampling with the pretrained MACE-MP foundation model (use_pretrained=true, relax=false) over a small MgAl2O4 cell and check the energies are finite and physically plausible. The MACE-MP model is downloaded on first use (no model file committed). Wired into the CI matrix as SamplingMace with install_mace.sh, on Python 3.13. Verified locally (mace-torch 0.3.16): the sampling run completes, check.py passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/Test_abICS.yml | 10 +- tests/integration/sampling_mace/MgAl2O4.vasp | 64 +++++++++++ tests/integration/sampling_mace/check.py | 18 ++++ tests/integration/sampling_mace/input.toml | 101 ++++++++++++++++++ .../integration/sampling_mace/install_mace.sh | 13 +++ tests/integration/sampling_mace/run.sh | 12 +++ 6 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 tests/integration/sampling_mace/MgAl2O4.vasp create mode 100644 tests/integration/sampling_mace/check.py create mode 100644 tests/integration/sampling_mace/input.toml create mode 100644 tests/integration/sampling_mace/install_mace.sh create mode 100644 tests/integration/sampling_mace/run.sh diff --git a/.github/workflows/Test_abICS.yml b/.github/workflows/Test_abICS.yml index adf7898d..1f55365b 100644 --- a/.github/workflows/Test_abICS.yml +++ b/.github/workflows/Test_abICS.yml @@ -17,7 +17,7 @@ jobs: matrix: python-version: ['3.9', '3.13'] numpy-version: ['1.20.0', '1.26.4', 'latest'] - testname: [Unit, Sampling, ActiveLearnAenet, ActiveLearnNequip, ActiveLearnMLIP-3, SamplingCHGNet] + testname: [Unit, Sampling, ActiveLearnAenet, ActiveLearnNequip, ActiveLearnMLIP-3, SamplingCHGNet, SamplingMace] exclude: - python-version: '3.13' numpy-version: '1.20.0' @@ -37,6 +37,11 @@ jobs: # runs only on Python 3.13. Unlike nequip, chgnet supports numpy 2. - python-version: '3.9' testname: SamplingCHGNet + # The MACE pretrained smoke test runs only on Python 3.13 to keep the + # job light (mace-torch supports 3.9 too, but these new MLIP solvers + # are exercised on the numpy-2 / Python 3.13 target). + - python-version: '3.9' + testname: SamplingMace fail-fast: false steps: @@ -83,5 +88,8 @@ jobs: SamplingCHGNet ) cd tests/integration/sampling_chgnet sh ./install_chgnet.sh sh ./run.sh ;; + SamplingMace ) cd tests/integration/sampling_mace + sh ./install_mace.sh + sh ./run.sh ;; * ) echo "Unknown testname";; esac diff --git a/tests/integration/sampling_mace/MgAl2O4.vasp b/tests/integration/sampling_mace/MgAl2O4.vasp new file mode 100644 index 00000000..596dd685 --- /dev/null +++ b/tests/integration/sampling_mace/MgAl2O4.vasp @@ -0,0 +1,64 @@ +Al2 Mg O4 +1.0 + 8.1135997772 0.0000000000 0.0000000000 + 0.0000000000 8.1135997772 0.0000000000 + 0.0000000000 0.0000000000 8.1135997772 + O Al Mg + 32 16 8 +Direct + 0.237399980 0.237399980 0.237399980 + 0.762599945 0.762599945 0.762599945 + 0.512599945 0.012600004 0.737399936 + 0.487399966 0.987399936 0.262599975 + 0.012600004 0.737399936 0.512599945 + 0.987399936 0.262599975 0.487399966 + 0.737399936 0.512599945 0.012600004 + 0.262599975 0.487399966 0.987399936 + 0.987399936 0.487399966 0.262599975 + 0.012600004 0.512599945 0.737399936 + 0.487399966 0.262599975 0.987399936 + 0.512599945 0.737399936 0.012600004 + 0.262599975 0.987399936 0.487399966 + 0.737399936 0.012600004 0.512599945 + 0.237399980 0.737399936 0.737399936 + 0.762599945 0.262599975 0.262599975 + 0.512599945 0.512599945 0.237399980 + 0.487399966 0.487399966 0.762599945 + 0.012600004 0.237399980 0.012600004 + 0.987399936 0.762599945 0.987399936 + 0.987399936 0.987399936 0.762599945 + 0.012600004 0.012600004 0.237399980 + 0.487399966 0.762599945 0.487399966 + 0.512599945 0.237399980 0.512599945 + 0.737399936 0.237399980 0.737399936 + 0.262599975 0.762599945 0.262599975 + 0.237399980 0.512599945 0.512599945 + 0.762599945 0.487399966 0.487399966 + 0.762599945 0.987399936 0.987399936 + 0.237399980 0.012600004 0.012600004 + 0.737399936 0.737399936 0.237399980 + 0.262599975 0.262599975 0.762599945 + 0.000000000 0.000000000 0.000000000 + 0.749999940 0.249999985 0.499999970 + 0.249999985 0.749999940 0.499999970 + 0.249999985 0.499999970 0.749999940 + 0.749999940 0.499999970 0.249999985 + 0.499999970 0.749999940 0.249999985 + 0.499999970 0.249999985 0.749999940 + 0.000000000 0.499999970 0.499999970 + 0.749999940 0.749999940 0.000000000 + 0.249999985 0.249999985 0.000000000 + 0.249999985 0.000000000 0.249999985 + 0.749999940 0.000000000 0.749999940 + 0.499999970 0.000000000 0.499999970 + 0.000000000 0.749999940 0.749999940 + 0.000000000 0.249999985 0.249999985 + 0.499999970 0.499999970 0.000000000 + 0.374999970 0.374999970 0.374999970 + 0.624999940 0.624999940 0.624999940 + 0.374999970 0.874999940 0.874999940 + 0.624999940 0.124999993 0.124999993 + 0.874999940 0.874999940 0.374999970 + 0.124999993 0.124999993 0.624999940 + 0.874999940 0.374999970 0.874999940 + 0.124999993 0.624999940 0.124999993 diff --git a/tests/integration/sampling_mace/check.py b/tests/integration/sampling_mace/check.py new file mode 100644 index 00000000..3bf04928 --- /dev/null +++ b/tests/integration/sampling_mace/check.py @@ -0,0 +1,18 @@ +import os +import numpy as np + +# The run must have produced the RXMC temperature file and per-replica observables. +assert os.path.exists("kTs.npy"), "kTs.npy was not produced" + +obs = np.loadtxt(os.path.join("0", "obs.dat")) +obs = np.atleast_2d(obs) +assert obs.shape[0] >= 1, "no samples were recorded in 0/obs.dat" + +# Column 3 is the total energy (see obs.dat header). For the pretrained CHGNet +# smoke test we do not hard-code values (they depend on the model version); we +# only check the pipeline produced finite, physically plausible energies. +energy = obs[:, 3] +assert np.all(np.isfinite(energy)), "non-finite energies in obs.dat" +assert np.all(energy < 0.0), "expected negative total energies for MgAl2O4" + +print("OK") diff --git a/tests/integration/sampling_mace/input.toml b/tests/integration/sampling_mace/input.toml new file mode 100644 index 00000000..ea333c9d --- /dev/null +++ b/tests/integration/sampling_mace/input.toml @@ -0,0 +1,101 @@ +[sampling] +sampler = "RXMC" +nreplicas = 2 +nprocs_per_replica = 1 +kTstart = 1200.0 +kTend = 1500.0 +nsteps = 8 +RXtrial_frequency = 4 +sample_frequency = 2 +print_frequency = 2 +reload = false +seed = 12345 + +[sampling.solver] +type = 'mace' +base_input_dir = './baseinput' +use_pretrained = true +relax = false +perturb = 0.0 +seed = 31415 + +[config] +unitcell = [[8.1135997772, 0.0000000000, 0.0000000000], + [0.0000000000, 8.1135997772, 0.0000000000], + [0.0000000000, 0.0000000000, 8.1135997772]] +supercell = [1,1,1] + +[[config.base_structure]] +type = "O" +coords = [ + [0.237399980, 0.237399980, 0.237399980], + [0.762599945, 0.762599945, 0.762599945], + [0.512599945, 0.012600004, 0.737399936], + [0.487399966, 0.987399936, 0.262599975], + [0.012600004, 0.737399936, 0.512599945], + [0.987399936, 0.262599975, 0.487399966], + [0.737399936, 0.512599945, 0.012600004], + [0.262599975, 0.487399966, 0.987399936], + [0.987399936, 0.487399966, 0.262599975], + [0.012600004, 0.512599945, 0.737399936], + [0.487399966, 0.262599975, 0.987399936], + [0.512599945, 0.737399936, 0.012600004], + [0.262599975, 0.987399936, 0.487399966], + [0.737399936, 0.012600004, 0.512599945], + [0.237399980, 0.737399936, 0.737399936], + [0.762599945, 0.262599975, 0.262599975], + [0.512599945, 0.512599945, 0.237399980], + [0.487399966, 0.487399966, 0.762599945], + [0.012600004, 0.237399980, 0.012600004], + [0.987399936, 0.762599945, 0.987399936], + [0.987399936, 0.987399936, 0.762599945], + [0.012600004, 0.012600004, 0.237399980], + [0.487399966, 0.762599945, 0.487399966], + [0.512599945, 0.237399980, 0.512599945], + [0.737399936, 0.237399980, 0.737399936], + [0.262599975, 0.762599945, 0.262599975], + [0.237399980, 0.512599945, 0.512599945], + [0.762599945, 0.487399966, 0.487399966], + [0.762599945, 0.987399936, 0.987399936], + [0.237399980, 0.012600004, 0.012600004], + [0.737399936, 0.737399936, 0.237399980], + [0.262599975, 0.262599975, 0.762599945], + ] + +[[config.defect_structure]] +coords = [ + [0.000000000, 0.000000000, 0.000000000], + [0.749999940, 0.249999985, 0.499999970], + [0.249999985, 0.749999940, 0.499999970], + [0.249999985, 0.499999970, 0.749999940], + [0.749999940, 0.499999970, 0.249999985], + [0.499999970, 0.749999940, 0.249999985], + [0.499999970, 0.249999985, 0.749999940], + [0.000000000, 0.499999970, 0.499999970], + [0.749999940, 0.749999940, 0.000000000], + [0.249999985, 0.249999985, 0.000000000], + [0.249999985, 0.000000000, 0.249999985], + [0.749999940, 0.000000000, 0.749999940], + [0.499999970, 0.000000000, 0.499999970], + [0.000000000, 0.749999940, 0.749999940], + [0.000000000, 0.249999985, 0.249999985], + [0.499999970, 0.499999970, 0.000000000], + [0.374999970, 0.374999970, 0.374999970], + [0.624999940, 0.624999940, 0.624999940], + [0.374999970, 0.874999940, 0.874999940], + [0.624999940, 0.124999993, 0.124999993], + [0.874999940, 0.874999940, 0.374999970], + [0.124999993, 0.124999993, 0.624999940], + [0.874999940, 0.374999970, 0.874999940], + [0.124999993, 0.624999940, 0.124999993], + ] +[[config.defect_structure.groups]] +name = 'Al' +# species = ['Al'] # default +# coords = [[[0,0,0]]] # default +num = 16 #432 #16000 +[[config.defect_structure.groups]] +name = 'Mg' +# species = ['Mg'] # default +# coords = [[[0,0,0]]] # default +num = 8 #216 #8000 diff --git a/tests/integration/sampling_mace/install_mace.sh b/tests/integration/sampling_mace/install_mace.sh new file mode 100644 index 00000000..2fd001e2 --- /dev/null +++ b/tests/integration/sampling_mace/install_mace.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# This script installs MACE into the python3 environment. +# The pretrained MACE-MP foundation model is downloaded on first use. + +set -ue + +echo "python3 points to the following:" +which python3 + +echo + +python3 -m pip install mace-torch diff --git a/tests/integration/sampling_mace/run.sh b/tests/integration/sampling_mace/run.sh new file mode 100644 index 00000000..14ddbae2 --- /dev/null +++ b/tests/integration/sampling_mace/run.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +export OMP_NUM_THREADS=1 + +set -e + +rm -f kTs.npy +rm -rf 0 1 baseinput + +mpiexec -np 2 --oversubscribe abics_sampling input.toml + +python3 ./check.py From 6262e3f4958b8e10dd28cd1fe7d1bc5270d25a77 Mon Sep 17 00:00:00 2001 From: Kazuyoshi Yoshimi Date: Sun, 21 Jun 2026 19:48:55 +0900 Subject: [PATCH 6/7] Add SevenNet pretrained sampling smoke test Mirror the CHGNet/MACE smoke tests for the SevenNet solver: run abics_sampling with the pretrained 7net-0 potential (use_pretrained=true, relax=false) over a small MgAl2O4 cell and check the energies are finite and physically plausible. The 7net-0 potential ships with the sevenn package. Wired into the CI matrix as SamplingSevenNet with install_sevennet.sh, on Python 3.13. Verified locally (sevenn 0.13.0): the sampling run completes, check.py passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/Test_abICS.yml | 9 +- .../sampling_sevennet/MgAl2O4.vasp | 64 +++++++++++ tests/integration/sampling_sevennet/check.py | 18 ++++ .../integration/sampling_sevennet/input.toml | 101 ++++++++++++++++++ .../sampling_sevennet/install_sevennet.sh | 13 +++ tests/integration/sampling_sevennet/run.sh | 12 +++ 6 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 tests/integration/sampling_sevennet/MgAl2O4.vasp create mode 100644 tests/integration/sampling_sevennet/check.py create mode 100644 tests/integration/sampling_sevennet/input.toml create mode 100644 tests/integration/sampling_sevennet/install_sevennet.sh create mode 100644 tests/integration/sampling_sevennet/run.sh diff --git a/.github/workflows/Test_abICS.yml b/.github/workflows/Test_abICS.yml index 1f55365b..99bb69e0 100644 --- a/.github/workflows/Test_abICS.yml +++ b/.github/workflows/Test_abICS.yml @@ -17,7 +17,7 @@ jobs: matrix: python-version: ['3.9', '3.13'] numpy-version: ['1.20.0', '1.26.4', 'latest'] - testname: [Unit, Sampling, ActiveLearnAenet, ActiveLearnNequip, ActiveLearnMLIP-3, SamplingCHGNet, SamplingMace] + testname: [Unit, Sampling, ActiveLearnAenet, ActiveLearnNequip, ActiveLearnMLIP-3, SamplingCHGNet, SamplingMace, SamplingSevenNet] exclude: - python-version: '3.13' numpy-version: '1.20.0' @@ -42,6 +42,10 @@ jobs: # are exercised on the numpy-2 / Python 3.13 target). - python-version: '3.9' testname: SamplingMace + # The SevenNet pretrained smoke test runs only on Python 3.13 (same + # rationale as MACE; sevenn supports numpy 2). + - python-version: '3.9' + testname: SamplingSevenNet fail-fast: false steps: @@ -91,5 +95,8 @@ jobs: SamplingMace ) cd tests/integration/sampling_mace sh ./install_mace.sh sh ./run.sh ;; + SamplingSevenNet ) cd tests/integration/sampling_sevennet + sh ./install_sevennet.sh + sh ./run.sh ;; * ) echo "Unknown testname";; esac diff --git a/tests/integration/sampling_sevennet/MgAl2O4.vasp b/tests/integration/sampling_sevennet/MgAl2O4.vasp new file mode 100644 index 00000000..596dd685 --- /dev/null +++ b/tests/integration/sampling_sevennet/MgAl2O4.vasp @@ -0,0 +1,64 @@ +Al2 Mg O4 +1.0 + 8.1135997772 0.0000000000 0.0000000000 + 0.0000000000 8.1135997772 0.0000000000 + 0.0000000000 0.0000000000 8.1135997772 + O Al Mg + 32 16 8 +Direct + 0.237399980 0.237399980 0.237399980 + 0.762599945 0.762599945 0.762599945 + 0.512599945 0.012600004 0.737399936 + 0.487399966 0.987399936 0.262599975 + 0.012600004 0.737399936 0.512599945 + 0.987399936 0.262599975 0.487399966 + 0.737399936 0.512599945 0.012600004 + 0.262599975 0.487399966 0.987399936 + 0.987399936 0.487399966 0.262599975 + 0.012600004 0.512599945 0.737399936 + 0.487399966 0.262599975 0.987399936 + 0.512599945 0.737399936 0.012600004 + 0.262599975 0.987399936 0.487399966 + 0.737399936 0.012600004 0.512599945 + 0.237399980 0.737399936 0.737399936 + 0.762599945 0.262599975 0.262599975 + 0.512599945 0.512599945 0.237399980 + 0.487399966 0.487399966 0.762599945 + 0.012600004 0.237399980 0.012600004 + 0.987399936 0.762599945 0.987399936 + 0.987399936 0.987399936 0.762599945 + 0.012600004 0.012600004 0.237399980 + 0.487399966 0.762599945 0.487399966 + 0.512599945 0.237399980 0.512599945 + 0.737399936 0.237399980 0.737399936 + 0.262599975 0.762599945 0.262599975 + 0.237399980 0.512599945 0.512599945 + 0.762599945 0.487399966 0.487399966 + 0.762599945 0.987399936 0.987399936 + 0.237399980 0.012600004 0.012600004 + 0.737399936 0.737399936 0.237399980 + 0.262599975 0.262599975 0.762599945 + 0.000000000 0.000000000 0.000000000 + 0.749999940 0.249999985 0.499999970 + 0.249999985 0.749999940 0.499999970 + 0.249999985 0.499999970 0.749999940 + 0.749999940 0.499999970 0.249999985 + 0.499999970 0.749999940 0.249999985 + 0.499999970 0.249999985 0.749999940 + 0.000000000 0.499999970 0.499999970 + 0.749999940 0.749999940 0.000000000 + 0.249999985 0.249999985 0.000000000 + 0.249999985 0.000000000 0.249999985 + 0.749999940 0.000000000 0.749999940 + 0.499999970 0.000000000 0.499999970 + 0.000000000 0.749999940 0.749999940 + 0.000000000 0.249999985 0.249999985 + 0.499999970 0.499999970 0.000000000 + 0.374999970 0.374999970 0.374999970 + 0.624999940 0.624999940 0.624999940 + 0.374999970 0.874999940 0.874999940 + 0.624999940 0.124999993 0.124999993 + 0.874999940 0.874999940 0.374999970 + 0.124999993 0.124999993 0.624999940 + 0.874999940 0.374999970 0.874999940 + 0.124999993 0.624999940 0.124999993 diff --git a/tests/integration/sampling_sevennet/check.py b/tests/integration/sampling_sevennet/check.py new file mode 100644 index 00000000..3bf04928 --- /dev/null +++ b/tests/integration/sampling_sevennet/check.py @@ -0,0 +1,18 @@ +import os +import numpy as np + +# The run must have produced the RXMC temperature file and per-replica observables. +assert os.path.exists("kTs.npy"), "kTs.npy was not produced" + +obs = np.loadtxt(os.path.join("0", "obs.dat")) +obs = np.atleast_2d(obs) +assert obs.shape[0] >= 1, "no samples were recorded in 0/obs.dat" + +# Column 3 is the total energy (see obs.dat header). For the pretrained CHGNet +# smoke test we do not hard-code values (they depend on the model version); we +# only check the pipeline produced finite, physically plausible energies. +energy = obs[:, 3] +assert np.all(np.isfinite(energy)), "non-finite energies in obs.dat" +assert np.all(energy < 0.0), "expected negative total energies for MgAl2O4" + +print("OK") diff --git a/tests/integration/sampling_sevennet/input.toml b/tests/integration/sampling_sevennet/input.toml new file mode 100644 index 00000000..8d09f362 --- /dev/null +++ b/tests/integration/sampling_sevennet/input.toml @@ -0,0 +1,101 @@ +[sampling] +sampler = "RXMC" +nreplicas = 2 +nprocs_per_replica = 1 +kTstart = 1200.0 +kTend = 1500.0 +nsteps = 8 +RXtrial_frequency = 4 +sample_frequency = 2 +print_frequency = 2 +reload = false +seed = 12345 + +[sampling.solver] +type = 'sevennet' +base_input_dir = './baseinput' +use_pretrained = true +relax = false +perturb = 0.0 +seed = 31415 + +[config] +unitcell = [[8.1135997772, 0.0000000000, 0.0000000000], + [0.0000000000, 8.1135997772, 0.0000000000], + [0.0000000000, 0.0000000000, 8.1135997772]] +supercell = [1,1,1] + +[[config.base_structure]] +type = "O" +coords = [ + [0.237399980, 0.237399980, 0.237399980], + [0.762599945, 0.762599945, 0.762599945], + [0.512599945, 0.012600004, 0.737399936], + [0.487399966, 0.987399936, 0.262599975], + [0.012600004, 0.737399936, 0.512599945], + [0.987399936, 0.262599975, 0.487399966], + [0.737399936, 0.512599945, 0.012600004], + [0.262599975, 0.487399966, 0.987399936], + [0.987399936, 0.487399966, 0.262599975], + [0.012600004, 0.512599945, 0.737399936], + [0.487399966, 0.262599975, 0.987399936], + [0.512599945, 0.737399936, 0.012600004], + [0.262599975, 0.987399936, 0.487399966], + [0.737399936, 0.012600004, 0.512599945], + [0.237399980, 0.737399936, 0.737399936], + [0.762599945, 0.262599975, 0.262599975], + [0.512599945, 0.512599945, 0.237399980], + [0.487399966, 0.487399966, 0.762599945], + [0.012600004, 0.237399980, 0.012600004], + [0.987399936, 0.762599945, 0.987399936], + [0.987399936, 0.987399936, 0.762599945], + [0.012600004, 0.012600004, 0.237399980], + [0.487399966, 0.762599945, 0.487399966], + [0.512599945, 0.237399980, 0.512599945], + [0.737399936, 0.237399980, 0.737399936], + [0.262599975, 0.762599945, 0.262599975], + [0.237399980, 0.512599945, 0.512599945], + [0.762599945, 0.487399966, 0.487399966], + [0.762599945, 0.987399936, 0.987399936], + [0.237399980, 0.012600004, 0.012600004], + [0.737399936, 0.737399936, 0.237399980], + [0.262599975, 0.262599975, 0.762599945], + ] + +[[config.defect_structure]] +coords = [ + [0.000000000, 0.000000000, 0.000000000], + [0.749999940, 0.249999985, 0.499999970], + [0.249999985, 0.749999940, 0.499999970], + [0.249999985, 0.499999970, 0.749999940], + [0.749999940, 0.499999970, 0.249999985], + [0.499999970, 0.749999940, 0.249999985], + [0.499999970, 0.249999985, 0.749999940], + [0.000000000, 0.499999970, 0.499999970], + [0.749999940, 0.749999940, 0.000000000], + [0.249999985, 0.249999985, 0.000000000], + [0.249999985, 0.000000000, 0.249999985], + [0.749999940, 0.000000000, 0.749999940], + [0.499999970, 0.000000000, 0.499999970], + [0.000000000, 0.749999940, 0.749999940], + [0.000000000, 0.249999985, 0.249999985], + [0.499999970, 0.499999970, 0.000000000], + [0.374999970, 0.374999970, 0.374999970], + [0.624999940, 0.624999940, 0.624999940], + [0.374999970, 0.874999940, 0.874999940], + [0.624999940, 0.124999993, 0.124999993], + [0.874999940, 0.874999940, 0.374999970], + [0.124999993, 0.124999993, 0.624999940], + [0.874999940, 0.374999970, 0.874999940], + [0.124999993, 0.624999940, 0.124999993], + ] +[[config.defect_structure.groups]] +name = 'Al' +# species = ['Al'] # default +# coords = [[[0,0,0]]] # default +num = 16 #432 #16000 +[[config.defect_structure.groups]] +name = 'Mg' +# species = ['Mg'] # default +# coords = [[[0,0,0]]] # default +num = 8 #216 #8000 diff --git a/tests/integration/sampling_sevennet/install_sevennet.sh b/tests/integration/sampling_sevennet/install_sevennet.sh new file mode 100644 index 00000000..98fdcf34 --- /dev/null +++ b/tests/integration/sampling_sevennet/install_sevennet.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +# This script installs SevenNet into the python3 environment. +# The pretrained 7net-0 potential ships with the sevenn package. + +set -ue + +echo "python3 points to the following:" +which python3 + +echo + +python3 -m pip install sevenn diff --git a/tests/integration/sampling_sevennet/run.sh b/tests/integration/sampling_sevennet/run.sh new file mode 100644 index 00000000..14ddbae2 --- /dev/null +++ b/tests/integration/sampling_sevennet/run.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +export OMP_NUM_THREADS=1 + +set -e + +rm -f kTs.npy +rm -rf 0 1 baseinput + +mpiexec -np 2 --oversubscribe abics_sampling input.toml + +python3 ./check.py From 15342d0411db424af93c0091b8ad7c5f7d2c385b Mon Sep 17 00:00:00 2001 From: Kazuyoshi Yoshimi Date: Mon, 22 Jun 2026 09:10:58 +0900 Subject: [PATCH 7/7] Pre-cache MACE-MP model before the MPI run to avoid CI hang The SamplingMace job hung until the 6-hour CI limit: mace_mp downloads the foundation model on first use, and when that download failed in one of the two MPI ranks, that rank crashed while the other deadlocked on the replica-exchange MPI Recv. Download the model once in a single process before mpiexec so the ranks load it from cache with no network access (and a download failure now fails fast instead of hanging). Verified locally with the mace-pinned e3nn==0.4.4: pre-cache succeeds and the 2-replica sampling run completes, check.py passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/integration/sampling_mace/run.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/integration/sampling_mace/run.sh b/tests/integration/sampling_mace/run.sh index 14ddbae2..e7760a68 100644 --- a/tests/integration/sampling_mace/run.sh +++ b/tests/integration/sampling_mace/run.sh @@ -7,6 +7,14 @@ set -e rm -f kTs.npy rm -rf 0 1 baseinput +# Pre-cache the pretrained MACE-MP model in a single process before launching +# the MPI run. mace_mp downloads the model on first use; if that download is +# left to the parallel ranks, a failure in one rank crashes only that process +# and the surviving rank deadlocks on the replica-exchange MPI Recv (the run +# then hangs until the CI job's hard timeout). Downloading once up front makes +# the subsequent ranks load the cached model with no network access. +python3 -c "from mace.calculators import mace_mp; mace_mp(device='cpu', default_dtype='float64')" + mpiexec -np 2 --oversubscribe abics_sampling input.toml python3 ./check.py