diff --git a/autoplex/auto/phonons/flows.py b/autoplex/auto/phonons/flows.py index 3467dea92..a8dacaa93 100644 --- a/autoplex/auto/phonons/flows.py +++ b/autoplex/auto/phonons/flows.py @@ -150,6 +150,7 @@ class CompleteDFTvsMLBenchmarkWorkflow(Maker): atomwise_regularization_list: list | None = None soap_delta_list: list | None = None n_sparse_list: list | None = None + benchmark_kwargs: dict = field(default_factory=dict) def make( self, @@ -263,7 +264,7 @@ def make( mlip_hyper=ml_hyper, ).make( species_list=isoatoms.output["species"], - isolated_atoms_energy=isoatoms.output["energies"], + isolated_atoms_energies=isoatoms.output["energies"], fit_input=fit_input, split_ratio=split_ratio, f_max=f_max, @@ -311,6 +312,7 @@ def make( symprec=self.symprec, phonon_displacement_maker=self.phonon_displacement_maker, dft_references=dft_references, + **self.benchmark_kwargs, ) flows.append(complete_bm) bm_outputs.append(complete_bm.output) @@ -343,7 +345,7 @@ def make( mlip_type=ml_model, mlip_hyper=ml_hyper ).make( species_list=isoatoms.output["species"], - isolated_atoms_energy=isoatoms.output["energies"], + isolated_atoms_energies=isoatoms.output["energies"], fit_input=fit_input, split_ratio=split_ratio, f_max=f_max, @@ -379,6 +381,7 @@ def make( symprec=self.symprec, phonon_displacement_maker=self.phonon_displacement_maker, dft_references=dft_references, + **self.benchmark_kwargs, ) flows.append(complete_bm) bm_outputs.append(complete_bm.output) diff --git a/autoplex/auto/phonons/jobs.py b/autoplex/auto/phonons/jobs.py index 93b11a6b6..c00e4256e 100644 --- a/autoplex/auto/phonons/jobs.py +++ b/autoplex/auto/phonons/jobs.py @@ -36,6 +36,9 @@ def complete_benchmark( # this function was put here to prevent circular import symprec, phonon_displacement_maker: BaseVaspMaker, dft_references=None, + relax_maker_kwargs: dict | None = None, + static_maker_kwargs: dict | None = None, + **ml_phonon_maker_kwargs, ): """ Construct a complete flow for benchmarking the MLIP fit quality using a DFT based phonon structure. @@ -76,6 +79,12 @@ def complete_benchmark( # this function was put here to prevent circular import Maker used to compute the forces for a supercell. dft_references: a list of DFT reference files containing the PhononBSDOCDoc object. Default None. + relax_maker_kwargs: dict + Keyword arguments that can be passed to the RelaxMaker. + static_maker_kwargs: dict + Keyword arguments that can be passed to the StaticMaker. + ml_phonon_maker_kwargs: dict + Keyword arguments that can be passed to the MLPhononMaker. """ jobs = [] collect_output = [] @@ -84,16 +93,34 @@ def complete_benchmark( # this function was put here to prevent circular import if min_length >= 18: phonon_displacement_maker = TightDFTStaticMakerBigSupercells() for suffix in ["", "_wo_sigma", "_phonon", "_rand_struc"]: - if Path(Path(ml_path) / f"gap_file{suffix}.xml").exists(): - # TODO: this needs to beextended for the other MLIPs + # _wo_sigma", "_phonon", "_rand_struc" only available for GAP at the moment + if ml_model == "GAP": + ml_potential = Path(ml_path) / f"gap_file{suffix}.xml" + elif ml_model == "J-ACE": + raise UserWarning("No atomate2 ACE.jl PhononMaker implemented.") + elif ml_model in ["M3GNET"]: + ml_potential = Path(ml_path.join(suffix)) / "training" + # M3GNet requires path + # also need to find a different solution for separated fit then + elif ml_model in ["NEQUIP"]: + ml_potential = Path(ml_path) / f"deployed_nequip_model{suffix}.pth" + else: # MACE + ml_potential = Path(ml_path) / f"MACE_model{suffix}.model" + + if Path(ml_potential).exists(): add_data_ml_phonon = MLPhononMaker( min_length=min_length, + relax_maker_kwargs=relax_maker_kwargs, + static_maker_kwargs=static_maker_kwargs, ).make_from_ml_model( structure=benchmark_structure, - ml_model=ml_path, - suffix=suffix, + ml_model=ml_model, + potential_file=ml_potential, + **ml_phonon_maker_kwargs, ) jobs.append(add_data_ml_phonon) + + # DFT benchmark reference preparations if dft_references is None and benchmark_mp_ids is not None: if ( benchmark_mp_ids[ibenchmark_structure] in mp_ids @@ -322,6 +349,7 @@ def get_iso_atom(structure_list: list[Structure]): list of pymatgen Structure objects """ jobs = [] + iso_atoms_dict = {} all_species = list( {specie for s in structure_list for specie in s.types_of_species} ) @@ -329,11 +357,14 @@ def get_iso_atom(structure_list: list[Structure]): isoatoms = IsoAtomMaker().make(all_species=all_species) jobs.append(isoatoms) + for i, species in enumerate(all_species): + iso_atoms_dict.update({species.number: isoatoms.output["energies"][i]}) + flow = Flow( jobs, { "species": all_species, - "energies": isoatoms.output["energies"], + "energies": iso_atoms_dict, "dirs": isoatoms.output["dirs"], }, ) diff --git a/autoplex/data/phonons/flows.py b/autoplex/data/phonons/flows.py index bfd56589f..b18512723 100644 --- a/autoplex/data/phonons/flows.py +++ b/autoplex/data/phonons/flows.py @@ -17,6 +17,12 @@ ForceFieldStaticMaker, GAPRelaxMaker, GAPStaticMaker, + M3GNetRelaxMaker, + M3GNetStaticMaker, + MACERelaxMaker, + MACEStaticMaker, + NequipRelaxMaker, + NequipStaticMaker, ) from atomate2.vasp.flows.core import DoubleRelaxMaker from atomate2.vasp.flows.phonons import PhononMaker @@ -27,6 +33,7 @@ from pymatgen.core import Molecule, Site from autoplex.data.common.jobs import generate_randomized_structures +from autoplex.data.phonons.utils import ml_phonon_maker_preparation __all__ = [ "DFTPhononMaker", @@ -332,17 +339,17 @@ class MLPhononMaker(FFPhononMaker): High-throughput electronic band structure calculations: Challenges and tools. Computational Materials Science, 49(2), 299-312. doi:10.1016/j.commatsci.2010.05.010. - We will however use seekpath and primitive structures + We will, however, use seekpath and primitive structures as determined by from phonopy to compute the phonon band structure - bulk_relax_maker : .ForceFieldRelaxMaker or None + bulk_relax_maker: .ForceFieldRelaxMaker or None A maker to perform a tight relaxation on the bulk. Set to ``None`` to skip the bulk relaxation - static_energy_maker : .ForceFieldStaticMaker or None + static_energy_maker: .ForceFieldStaticMaker or None A maker to perform the computation of the DFT energy on the bulk. Set to ``None`` to skip the static energy computation - phonon_displacement_maker : .ForceFieldStaticMaker or None + phonon_displacement_maker: .ForceFieldStaticMaker or None Maker used to compute the forces for a supercell. generate_frequencies_eigenvectors_kwargs : dict Keyword arguments passed to :obj:`generate_frequencies_eigenvectors`. @@ -364,6 +371,10 @@ class MLPhononMaker(FFPhononMaker): in the future store_force_constants: bool if True, force constants will be stored + relax_maker_kwargs: dict + Keyword arguments that can be passed to the RelaxMaker. + static_maker_kwargs: dict + Keyword arguments that can be passed to the StaticMaker. """ name: str = "ml phonon" @@ -374,7 +385,7 @@ class MLPhononMaker(FFPhononMaker): ) ) phonon_displacement_maker: ForceFieldStaticMaker | None = field( - default_factory=lambda: GAPStaticMaker(name="ml phonon static") + default_factory=lambda: GAPStaticMaker(name="gap phonon static") ) static_energy_maker: ForceFieldStaticMaker | None = field( default_factory=lambda: GAPStaticMaker() @@ -383,11 +394,18 @@ class MLPhononMaker(FFPhononMaker): generate_frequencies_eigenvectors_kwargs: dict = field( default_factory=lambda: {"units": "THz", "tol_imaginary_modes": 1e-1} ) - relax_maker_kwargs: dict = field(default_factory=dict) - static_maker_kwargs: dict = field(default_factory=dict) + relax_maker_kwargs: dict | None = field(default_factory=dict) + static_maker_kwargs: dict | None = field(default_factory=dict) @job - def make_from_ml_model(self, structure, ml_model, suffix: str = "", **make_kwargs): + def make_from_ml_model( + self, + structure, + potential_file, + ml_model: str = "GAP", + calculator_kwargs: dict | None = None, + **make_kwargs, + ): """ Maker for GAP phonon jobs. @@ -397,10 +415,13 @@ def make_from_ml_model(self, structure, ml_model, suffix: str = "", **make_kwarg A pymatgen structure. Please start with a structure that is nearly fully optimized as the internal optimizers have very strict settings! - ml_model : str - Complete path to MLIP file(s). - suffix: - Train, test and MLIP suffix ("", "_wo_sigma", "_phonon", "_rand_struc"). + ml_model: str + ML model to be used. Default is GAP. + potential_file : + Complete path to MLIP file(s) + Train, test and MLIP files (+ suffixes "", "_wo_sigma", "_phonon", "_rand_struc"). + calculator_kwargs : + Keyword arguments for the ASE Calculator. make_kwargs : Keyword arguments for the PhononMaker. @@ -409,41 +430,93 @@ def make_from_ml_model(self, structure, ml_model, suffix: str = "", **make_kwarg PhononMaker jobs. """ - ml_model = ml_model + f"/gap_file{suffix}.xml" - if self.bulk_relax_maker is not None: - br = self.bulk_relax_maker - self.bulk_relax_maker = br.update_kwargs( - update={ - "calculator_kwargs": { - "args_str": "IP GAP", - "param_filename": str(ml_model), - }, - **self.relax_maker_kwargs, + if ml_model == "GAP": + if calculator_kwargs is None: + calculator_kwargs = { + "args_str": "IP GAP", + "param_filename": str(potential_file), } + + ml_prep = ml_phonon_maker_preparation( + bulk_relax_maker=self.bulk_relax_maker, + phonon_displacement_maker=self.phonon_displacement_maker, + static_energy_maker=self.static_energy_maker, + calculator_kwargs=calculator_kwargs, + relax_maker_kwargs=self.relax_maker_kwargs, + static_maker_kwargs=self.static_maker_kwargs, ) - if self.phonon_displacement_maker is not None: - ph_disp = self.phonon_displacement_maker - self.phonon_displacement_maker = ph_disp.update_kwargs( - update={ - "calculator_kwargs": { - "args_str": "IP GAP", - "param_filename": str(ml_model), - }, - **self.static_maker_kwargs, + + elif ml_model == "J-ACE": + raise UserWarning("No atomate2 ACE.jl PhononMaker implemented.") + + elif ml_model == "NEQUIP": + if calculator_kwargs is None: + calculator_kwargs = { + "model_path": str(potential_file), + "device": "cuda", } + else: + calculator_kwargs.update({"model_path": str(potential_file)}) + + ml_prep = ml_phonon_maker_preparation( + bulk_relax_maker=NequipRelaxMaker( + relax_cell=True, relax_kwargs={"interval": 500} + ), + phonon_displacement_maker=NequipStaticMaker( + name="nequip phonon static" + ), + static_energy_maker=NequipStaticMaker(), + calculator_kwargs=calculator_kwargs, + relax_maker_kwargs=self.relax_maker_kwargs, + static_maker_kwargs=self.static_maker_kwargs, ) - if self.static_energy_maker is not None: - stat_en = self.static_energy_maker - self.static_energy_maker = stat_en.update_kwargs( - update={ - "calculator_kwargs": { - "args_str": "IP GAP", - "param_filename": str(ml_model), - }, - **self.static_maker_kwargs, - } + + elif ml_model == "M3GNET": + if calculator_kwargs is None: + calculator_kwargs = {"path": str(potential_file)} + + ml_prep = ml_phonon_maker_preparation( + bulk_relax_maker=M3GNetRelaxMaker( + relax_cell=True, relax_kwargs={"interval": 500} + ), + phonon_displacement_maker=M3GNetStaticMaker( + name="m3gnet phonon static" + ), + static_energy_maker=M3GNetStaticMaker(), + calculator_kwargs=calculator_kwargs, + relax_maker_kwargs=self.relax_maker_kwargs, + static_maker_kwargs=self.static_maker_kwargs, ) + else: # MACE + if calculator_kwargs is None: + calculator_kwargs = {"model": str(potential_file), "device": "cuda"} + elif "model" in calculator_kwargs: + calculator_kwargs.update( + {"default_dtype": "float64"} + ) # Use float64 for geometry optimization. + else: + calculator_kwargs.update( + {"model": str(potential_file), "default_dtype": "float64"} + ) + + ml_prep = ml_phonon_maker_preparation( + bulk_relax_maker=MACERelaxMaker( + relax_cell=True, relax_kwargs={"interval": 500} + ), + phonon_displacement_maker=MACEStaticMaker(name="mace phonon static"), + static_energy_maker=MACEStaticMaker(), + calculator_kwargs=calculator_kwargs, + relax_maker_kwargs=self.relax_maker_kwargs, + static_maker_kwargs=self.static_maker_kwargs, + ) + + ( + self.bulk_relax_maker, + self.phonon_displacement_maker, + self.static_energy_maker, + ) = ml_prep + flow = self.make(structure=structure, **make_kwargs) return Response(replace=flow, output=flow.output) diff --git a/autoplex/data/phonons/utils.py b/autoplex/data/phonons/utils.py index 368bbd20f..0ac58b380 100644 --- a/autoplex/data/phonons/utils.py +++ b/autoplex/data/phonons/utils.py @@ -1 +1,66 @@ """Utility functions for data generation jobs.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from atomate2.forcefields.jobs import ( + ForceFieldRelaxMaker, + ForceFieldStaticMaker, + ) + + +def ml_phonon_maker_preparation( + calculator_kwargs: dict, + relax_maker_kwargs: dict | None, + static_maker_kwargs: dict | None, + bulk_relax_maker: ForceFieldRelaxMaker, + phonon_displacement_maker: ForceFieldStaticMaker, + static_energy_maker: ForceFieldStaticMaker, +): + """ + Prepare the MLPhononMaker for the respective MLIP model. + + bulk_relax_maker: .ForceFieldRelaxMaker or None + A maker to perform a tight relaxation on the bulk. + Set to ``None`` to skip the + bulk relaxation + static_energy_maker: .ForceFieldStaticMaker or None + A maker to perform the computation of the DFT energy on the bulk. + Set to ``None`` to skip the + static energy computation + phonon_displacement_maker: .ForceFieldStaticMaker or None + Maker used to compute the forces for a supercell. + relax_maker_kwargs: dict + Keyword arguments that can be passed to the RelaxMaker. + static_maker_kwargs: dict + Keyword arguments that can be passed to the StaticMaker. + """ + if bulk_relax_maker is not None: + bulk_relax_maker = bulk_relax_maker.update_kwargs( + update={"calculator_kwargs": calculator_kwargs} + ) + if relax_maker_kwargs is not None: + bulk_relax_maker = bulk_relax_maker.update_kwargs( + update={**relax_maker_kwargs} + ) + + if phonon_displacement_maker is not None: + phonon_displacement_maker = phonon_displacement_maker.update_kwargs( + update={"calculator_kwargs": calculator_kwargs} + ) + if static_maker_kwargs is not None: + phonon_displacement_maker = phonon_displacement_maker.update_kwargs( + {**static_maker_kwargs} + ) + if static_energy_maker is not None: + static_energy_maker = static_energy_maker.update_kwargs( + update={"calculator_kwargs": calculator_kwargs} + ) + if static_maker_kwargs is not None: + static_energy_maker = static_energy_maker.update_kwargs( + update={**static_maker_kwargs} + ) + + return bulk_relax_maker, phonon_displacement_maker, static_energy_maker diff --git a/autoplex/fitting/common/utils.py b/autoplex/fitting/common/utils.py index c14597568..d0c93df4b 100644 --- a/autoplex/fitting/common/utils.py +++ b/autoplex/fitting/common/utils.py @@ -28,6 +28,7 @@ from ase.neighborlist import NeighborList, natural_cutoffs from atomate2.utils.path import strip_hostname from dgl.data.utils import split_dataset +from matgl.apps.pes import Potential from matgl.ext.pymatgen import Structure2Graph, get_element_list from matgl.graph.data import MGLDataLoader, MGLDataset, collate_fn_pes from matgl.models import M3GNet @@ -850,7 +851,9 @@ def m3gnet_fitting( # save trained model model_export_path = os.path.join(results_dir, exp_name) - model.save(model_export_path) + # model.save(model_export_path) + potential = Potential(model=model) + potential.save(model_export_path) sys.stdout = original_stdout sys.stderr = original_stderr diff --git a/docs/user/fitting/fitting.md b/docs/user/fitting/fitting.md index 42e013f0e..1aba34c74 100644 --- a/docs/user/fitting/fitting.md +++ b/docs/user/fitting/fitting.md @@ -4,15 +4,24 @@ # Fitting potentials -This tutorial will show you how to control the MLIP fit settings with the `autoplex` workflow. The choice of the correct fit setup and hyperparameter settings has a significant influence on the final result. +This tutorial will show you how to control the MLIP fit settings with the `autoplex` workflow. +The choice of the correct fit setup and hyperparameter settings has a significant influence on the final result. ## General settings -There are two categories of fit settings that you can change. The first type concerns the general fit setup, that will affect the fit regardless of the chosen MLIP method, and e.g. changes database specific settings (like the split-up into training and test data). The other type of settings influences the MLIP specific setup like e.g. the choice of hyperparameters. - -In case of the general settings, you can pass the MLIP model you want to use with the `ml_models` parameter list and set the basic hyperparameters using the `mlip_hyper` list. -You can set the maximum force threshold `f_max` for filtering the data ("distillation") in the MLIP fit preprocess step. In principle, the distillation step be turned off by passing `"distillation": False` in the `fit_kwargs` keyword arguments, but it is strongly advised to filter out too high force data points. -Further parameters can be passed using `fit_kwargs` (or `**{...}`), like e.g. you can set the `split_ratio` to split the database up into a training and a test set, or adjust the number of processes `num_processes`. +There are two categories of fit settings that you can change. The first type concerns the general fit setup, +that will affect the fit regardless of the chosen MLIP method, and e.g. changes database specific settings +(like the split-up into training and test data). The other type of settings influences the MLIP specific setup +like e.g. the choice of hyperparameters. + +In case of the general settings, you can pass the MLIP model you want to use with the `ml_models` parameter list +and set the basic hyperparameters using the `mlip_hyper` list. +You can set the maximum force threshold `f_max` for filtering the data ("distillation") in the MLIP fit preprocess step. +In principle, the distillation step can be turned off by passing `"distillation": False` in the `fit_kwargs` keyword arguments, +but it is strongly advised to filter out too high force data points. +Further parameters can be passed using `fit_kwargs` (or `**{...}`), +like e.g. you can set the `split_ratio` to split the database up into a training and a test set, +or adjust the number of processes `num_processes`. ```python complete_flow = CompleteDFTvsMLBenchmarkWorkflow( ml_models=["GAP", "MACE"], mlip_hyper=[{...}, {...}]).make(..., f_max=40.0, @@ -22,16 +31,26 @@ complete_flow = CompleteDFTvsMLBenchmarkWorkflow( }) ``` -The MLIP model specific settings and hyperparameters setup varies from model to model and is demonstrated in the next sections. Note that `autoplex` provides the most comprehensive features for **GAP** and more features for the other models will follow in future versions. +The MLIP model specific settings and hyperparameters setup varies from model to model and is demonstrated in the next +sections. +> ℹ️ Note that `autoplex` provides the most comprehensive features for **GAP**, and more features for the other models will +follow in future versions. ## GAP There are several overall settings for the GAP fit that will change the mode in which `autoplex` runs. -When `hyper_para_loop` is set to `True`, `autoplex` wil automatically iterate through a set of several hyperparameters (`atomwise_regularization_list`, `soap_delta_list` and `n_sparse_list`) and repeat the GAP fit for each combination. -More information on the atom-wise regularization parameter can be found in [J. Chem. Phys. 153, 044104 (2020)](https://pubs.aip.org/aip/jcp/article/153/4/044104/1056348/Combining-phonon-accuracy-with-high) and a comprehensive list GAP hyperparameters can be found in the [QUIP/GAP user guide](https://libatoms.github.io/GAP/gap_fit.html#command-line-example). -The other keywords to change `autoplex`'s mode are `glue_xml` (use glue.xml core potential instead of 2b/3b terms), `regularization` (use a sigma regularization) and `separated` (repeat the GAP fit for the combined database and each separated subset). -The parameter `atom_wise_regularization` can turn the atom-wise regularization on and off, `atomwise_regularization_parameter` is the value that shall be set and `f_min` is the lower bound cutoff of forces taken into account for the atom-wise regularization or otherwise be replaced by the f_min value. -`auto_delta` let's you decide if you want to pass a fixed delta value for the 2b, 3b and SOAP terms or let `autoplex` automatically determine a suitable delta value based on the database's energies. +When `hyper_para_loop` is set to `True`, `autoplex` wil automatically iterate through a set of several hyperparameters +(`atomwise_regularization_list`, `soap_delta_list` and `n_sparse_list`) and repeat the GAP fit for each combination. +More information on the atom-wise regularization parameter can be found in [J. Chem. Phys. 153, 044104 (2020)](https://pubs.aip.org/aip/jcp/article/153/4/044104/1056348/Combining-phonon-accuracy-with-high) +and a comprehensive list GAP hyperparameters can be found in the [QUIP/GAP user guide](https://libatoms.github.io/GAP/gap_fit.html#command-line-example). +The other keywords to change `autoplex`'s mode are `glue_xml` (use glue.xml core potential instead of 2b/3b terms), +`regularization` (use a sigma regularization) and `separated` (repeat the GAP fit for the combined database and each +separated subset). +The parameter `atom_wise_regularization` can turn the atom-wise regularization on and off, +`atomwise_regularization_parameter` is the value that shall be set and `f_min` is the lower bound cutoff of forces +taken into account for the atom-wise regularization or otherwise be replaced by the f_min value. +`auto_delta` let's you decide if you want to pass a fixed delta value for the 2b, 3b and SOAP terms or let `autoplex` +automatically determine a suitable delta value based on the database's energies. ```python complete_flow = CompleteDFTvsMLBenchmarkWorkflow( ml_models=["GAP"], @@ -54,9 +73,12 @@ complete_flow = CompleteDFTvsMLBenchmarkWorkflow( "soap": {"delta": 1.0, "l_max": 12, "n_max": 10,...}, }) ``` -`autoplex` provides a JSON dict file containing default GAP fit settings in *autoplex/fitting/common/gap-defaults.json*, that can be overwritten using the fit keyword arguments as demonstrated in the code snippet. +`autoplex` provides a JSON dict file containing default GAP fit settings in +*autoplex/fitting/common/gap-defaults.json*, +that can be overwritten using the fit keyword arguments as demonstrated in the code snippet. -`autoplex` follows a certain convention for naming files and labelling the data (see *autoplex/fitting/common/gap-defaults.json*). +`autoplex` follows a certain convention for naming files and labelling the data +(see *autoplex/fitting/common/gap-defaults.json*). ```json "general": { "at_file": "train.extxyz", @@ -72,7 +94,8 @@ You can either adapt to the `autoplex` conventions or change by passing your pre ## ACE -For fitting and validating ACE potentials, one needs to install **julia** as `autoplex` relies on [ACEpotentials.jl](https://acesuit.github.io/ACEpotentials.jl/dev/gettingstarted/installation/) which support fitting of linear ACE. Currently no python package exists for the same. +For fitting and validating ACE potentials, one needs to install **julia** as `autoplex` relies on +[ACEpotentials.jl](https://acesuit.github.io/ACEpotentials.jl/dev/gettingstarted/installation/) which support fitting of linear ACE. Currently no python package exists for the same. ```python complete_flow = CompleteDFTvsMLBenchmarkWorkflow( @@ -157,7 +180,8 @@ complete_flow = CompleteDFTvsMLBenchmarkWorkflow( ## Running a MLIP fit only -The following script shows an example of how you can run a sole GAP fit with `autoplex` using `run_locally` from `jobflow` for the job management. +The following script shows an example of how you can run a sole GAP fit with `autoplex` using `run_locally` from +`jobflow` for the job management. ```python #!/usr/bin/env python @@ -217,7 +241,7 @@ run_locally(mlip_fit, create_folders=True, store=store) ``` Additional fit settings can again be passed using `fit_kwargs` or `**{...}`. -> ℹ️ Note that in the current setup of `autoplex`, you need to pass a `fit_input_dict` to the `MLIPFitMaker` containing -> at least one entry for "rand_struc_dir", "phonon_dir" and "isolated_atom" **VASP** calculations, +> ℹ️ Note that in the current setup of `autoplex`, you need to pass a `fit_input_dict` to the `MLIPFitMaker` +> containing at least one entry for "rand_struc_dir", "phonon_dir" and "isolated_atom" **VASP** calculations, > otherwise the code will not finish successfully. \ No newline at end of file diff --git a/tests/auto/test_auto_flows.py b/tests/auto/test_auto_flows.py index e6a6164d7..88885a91a 100644 --- a/tests/auto/test_auto_flows.py +++ b/tests/auto/test_auto_flows.py @@ -141,7 +141,7 @@ def fake_run_vasp_kwargs4(): } -def test_complete_dft_vs_ml_benchmark_workflow( +def test_complete_dft_vs_ml_benchmark_workflow_gap( vasp_test_dir, mock_vasp, test_dir, memory_jobstore, ref_paths4, fake_run_vasp_kwargs4, clean_dir ): from jobflow import run_locally @@ -169,12 +169,183 @@ def test_complete_dft_vs_ml_benchmark_workflow( store=memory_jobstore, ) + print("RMSE: ", responses[complete_workflow.jobs[-1].output.uuid][1].output[0][0]["benchmark_phonon_rmse"]) + assert complete_workflow.jobs[4].name == "complete_benchmark" assert responses[complete_workflow.jobs[-1].output.uuid][1].output[0][0]["benchmark_phonon_rmse"] == pytest.approx( 2.002641337594289, abs=1.0 # it's kinda fluctuating because of the little data ) +def test_complete_dft_vs_ml_benchmark_workflow_m3gnet( + vasp_test_dir, mock_vasp, test_dir, memory_jobstore, ref_paths4, fake_run_vasp_kwargs4, clean_dir +): + from jobflow import run_locally + + path_to_struct = vasp_test_dir / "dft_ml_data_generation" / "POSCAR" + structure = Structure.from_file(path_to_struct) + + complete_workflow_m3gnet = CompleteDFTvsMLBenchmarkWorkflow( + ml_models=["M3GNET"], + mlip_hyper=[{ + "exp_name": "training", + "results_dir": "m3gnet_results", + "cutoff": 3.0, + "threebody_cutoff": 2.0, + "batch_size": 1, + "max_epochs": 3, + "include_stresses": True, + "hidden_dim": 8, + "num_units": 8, + "max_l": 4, + "max_n": 4, + "device": "cpu", + "test_equal_to_val": True, + }], + symprec=1e-2, min_length=8, displacements=[0.01], + volume_custom_scale_factors=[0.975, 1.0, 1.025, 1.05], + ).make( + structure_list=[structure], + mp_ids=["test"], + benchmark_mp_ids=["mp-22905"], + benchmark_structures=[structure], + pre_xyz_files=["vasp_ref.extxyz"], + pre_database_dir=test_dir / "fitting" / "ref_files", + ) + + # automatically use fake VASP and write POTCAR.spec during the test + mock_vasp(ref_paths4, fake_run_vasp_kwargs4) + + # run the flow or job and ensure that it finished running successfully + try: + responses = run_locally( + complete_workflow_m3gnet, + create_folders=True, + ensure_success=False, + store=memory_jobstore, + ) + except ValueError: + print("\nWe need to fix some jobflow error.") + + assert complete_workflow_m3gnet.jobs[4].name == "complete_benchmark" + #assert responses[complete_workflow_m3gnet.jobs[-1].output.uuid][1].output[0][0][ + # "benchmark_phonon_rmse"] == pytest.approx( + # 1.162641337594289, abs=1.0 # it's kinda fluctuating because of the little data + #) + + +def test_complete_dft_vs_ml_benchmark_workflow_mace( + vasp_test_dir, mock_vasp, test_dir, memory_jobstore, ref_paths4, fake_run_vasp_kwargs4, clean_dir +): + from jobflow import run_locally + + path_to_struct = vasp_test_dir / "dft_ml_data_generation" / "POSCAR" + structure = Structure.from_file(path_to_struct) + + complete_workflow_mace = CompleteDFTvsMLBenchmarkWorkflow( + ml_models=["MACE"], + mlip_hyper=[{ + "model": "MACE", + "config_type_weights": '{"Default":1.0}', + "hidden_irreps": "32x0e + 32x1o", + "r_max": 3.0, + "batch_size": 5, + "max_num_epochs": 10, + "start_swa": 5, + "ema_decay": 0.99, + "correlation": 3, + "loss": "huber", + "default_dtype": "float32", + "device": "cpu", + }], + symprec=1e-2, min_length=8, displacements=[0.01], + volume_custom_scale_factors=[0.975, 1.0, 1.025, 1.05], + benchmark_kwargs={"calculator_kwargs": {"device": "cpu"}} + ).make( + structure_list=[structure], + mp_ids=["test"], + benchmark_mp_ids=["mp-22905"], + benchmark_structures=[structure], + pre_xyz_files=["vasp_ref.extxyz"], + pre_database_dir=test_dir / "fitting" / "ref_files", + ) + + # automatically use fake VASP and write POTCAR.spec during the test + mock_vasp(ref_paths4, fake_run_vasp_kwargs4) + + # run the flow or job and ensure that it finished running successfully + responses = run_locally( + complete_workflow_mace, + create_folders=True, + ensure_success=True, + store=memory_jobstore, + ) + + assert complete_workflow_mace.jobs[4].name == "complete_benchmark" + assert responses[complete_workflow_mace.jobs[-1].output.uuid][1].output[0][0][ + "benchmark_phonon_rmse"] == pytest.approx( + 5.391879137001022, abs=3.0 + # result is so bad because hyperparameter quality is reduced to a minimum to save time + # and too little data + ) + + +def test_complete_dft_vs_ml_benchmark_workflow_nequip( + vasp_test_dir, mock_vasp, test_dir, memory_jobstore, ref_paths4, fake_run_vasp_kwargs4, clean_dir +): + from jobflow import run_locally + + path_to_struct = vasp_test_dir / "dft_ml_data_generation" / "POSCAR" + structure = Structure.from_file(path_to_struct) + + complete_workflow_nequip = CompleteDFTvsMLBenchmarkWorkflow( + ml_models=["NEQUIP"], + mlip_hyper=[{ + "r_max": 4.0, + "num_layers": 4, + "l_max": 2, + "num_features": 32, + "num_basis": 8, + "invariant_layers": 2, + "invariant_neurons": 64, + "batch_size": 1, + "learning_rate": 0.005, + "max_epochs": 1, # reduced to 1 to minimize the test execution time + "default_dtype": "float32", + "device": "cpu", + }], + symprec=1e-2, min_length=8, displacements=[0.01], + volume_custom_scale_factors=[0.975, 1.0, 1.025, 1.05], + benchmark_kwargs={"calculator_kwargs": {"device": "cpu"}} + ).make( + structure_list=[structure], + mp_ids=["test"], + benchmark_mp_ids=["mp-22905"], + benchmark_structures=[structure], + pre_xyz_files=["vasp_ref.extxyz"], + pre_database_dir=test_dir / "fitting" / "ref_files", + ) + + # automatically use fake VASP and write POTCAR.spec during the test + mock_vasp(ref_paths4, fake_run_vasp_kwargs4) + + # run the flow or job and ensure that it finished running successfully + responses = run_locally( + complete_workflow_nequip, + create_folders=True, + ensure_success=True, + store=memory_jobstore, + ) + + assert complete_workflow_nequip.jobs[4].name == "complete_benchmark" + assert responses[complete_workflow_nequip.jobs[-1].output.uuid][1].output[0][0][ + "benchmark_phonon_rmse"] == pytest.approx( + 5.633069137001022, abs=3.0 + # result is so bad because hyperparameter quality is reduced to a minimum to save time + # and too little data + ) + + def test_complete_dft_vs_ml_benchmark_workflow_two_mpids( vasp_test_dir, mock_vasp, test_dir, memory_jobstore, ref_paths4, fake_run_vasp_kwargs4, clean_dir ): @@ -442,6 +613,47 @@ def test_complete_dft_vs_ml_benchmark_workflow_separated( ) +def test_complete_dft_vs_ml_benchmark_workflow_separated_sigma_reg_hploop_three_mpids( + vasp_test_dir, mock_vasp, test_dir, memory_jobstore, ref_paths4, fake_run_vasp_kwargs4, clean_dir +): + from jobflow import run_locally + + path_to_struct = vasp_test_dir / "dft_ml_data_generation" / "POSCAR" + structure = Structure.from_file(path_to_struct) + + complete_workflow_sep_3 = CompleteDFTvsMLBenchmarkWorkflow(symprec=1e-2, min_length=8, displacements=[0.01], + volume_custom_scale_factors=[0.975, 1.0, 1.025, 1.05], + hyper_para_loop=True, + atomwise_regularization_list=[0.01], + n_sparse_list=[3000, 5000], + soap_delta_list=[1.0], + ).make( + structure_list=[structure, structure, structure], + mp_ids=["test", "test2", "test3"], + benchmark_mp_ids=["mp-22905"], + benchmark_structures=[structure], + pre_xyz_files=["vasp_ref.extxyz"], + pre_database_dir=test_dir / "fitting" / "ref_files", + **{"regularization": True, "separated": True}, + ) + + # automatically use fake VASP and write POTCAR.spec during the test + mock_vasp(ref_paths4, fake_run_vasp_kwargs4) + + # run the flow or job and ensure that it finished running successfully + responses = run_locally( + complete_workflow_sep_3, + create_folders=True, + ensure_success=True, + store=memory_jobstore, + ) + + assert responses[complete_workflow_sep_3.jobs[-1].output.uuid][1].output[0][0][ + "benchmark_phonon_rmse"] == pytest.approx( + 0.8709764794814768, abs=0.5 + ) + + def test_complete_dft_vs_ml_benchmark_workflow_separated_sigma_reg_hploop( vasp_test_dir, mock_vasp, test_dir, memory_jobstore, ref_paths4, fake_run_vasp_kwargs4, clean_dir ): diff --git a/tests/data/test_datagen_flows.py b/tests/data/test_datagen_flows.py index 8b7a82c79..c874e36cb 100644 --- a/tests/data/test_datagen_flows.py +++ b/tests/data/test_datagen_flows.py @@ -5,6 +5,11 @@ from atomate2.vasp.powerups import update_user_incar_settings from atomate2.common.schemas.phonons import PhononBSDOSDoc from pymatgen.core.structure import Structure +from atomate2.forcefields.jobs import ( + GAPRelaxMaker, + GAPStaticMaker, + +) from autoplex.data.common.flows import GenerateTrainingDataForTesting from autoplex.data.phonons.flows import IsoAtomMaker, RandomStructuresDataGenerator, MLPhononMaker @@ -16,12 +21,17 @@ def test_ml_phonon_maker(test_dir, clean_dir, memory_jobstore): from jobflow import run_locally - potential_file_dir = test_dir / "fitting" / "ref_files" + potential_file = test_dir / "fitting" / "ref_files" / "gap_file.xml" path_to_struct = test_dir / "fitting" / "ref_files" / "POSCAR" structure = Structure.from_file(path_to_struct) - gap_phonon_jobs = MLPhononMaker(min_length=20).make_from_ml_model( - structure=structure, ml_model=str(potential_file_dir), + gap_phonon_jobs = MLPhononMaker( + min_length=20, + bulk_relax_maker=GAPRelaxMaker(relax_cell=True, relax_kwargs={"interval": 500}), + phonon_displacement_maker=GAPStaticMaker(name="gap phonon static"), + static_energy_maker=GAPStaticMaker(), + ).make_from_ml_model( + structure=structure, potential_file=potential_file, ) responses = run_locally( diff --git a/tests/fitting/test_fitting_flows.py b/tests/fitting/test_fitting_flows.py index 3da126bf6..bd1a30996 100644 --- a/tests/fitting/test_fitting_flows.py +++ b/tests/fitting/test_fitting_flows.py @@ -148,7 +148,7 @@ def test_mlip_fit_maker_with_kwargs( def test_mlip_fit_maker_with_pre_database_dir( - test_dir, memory_jobstore, vasp_test_dir, clean_dir, fit_input_dict + test_dir, memory_jobstore, vasp_test_dir, fit_input_dict, clean_dir ): import os import shutil @@ -184,7 +184,7 @@ def test_mlip_fit_maker_with_pre_database_dir( def test_mlip_fit_maker_jace( - test_dir, memory_jobstore, vasp_test_dir, clean_dir, fit_input_dict + test_dir, memory_jobstore, vasp_test_dir, fit_input_dict, clean_dir ): import os import shutil @@ -230,7 +230,7 @@ def test_mlip_fit_maker_jace( def test_mlip_fit_maker_nequip( - test_dir, memory_jobstore, vasp_test_dir, clean_dir, fit_input_dict + test_dir, memory_jobstore, vasp_test_dir, fit_input_dict, clean_dir ): import os import shutil @@ -284,7 +284,7 @@ def test_mlip_fit_maker_nequip( def test_mlip_fit_maker_m3gnet( - test_dir, memory_jobstore, vasp_test_dir, clean_dir, fit_input_dict + test_dir, memory_jobstore, vasp_test_dir, fit_input_dict, clean_dir ): import os import shutil @@ -339,7 +339,7 @@ def test_mlip_fit_maker_m3gnet( def test_mlip_fit_maker_mace( - test_dir, memory_jobstore, vasp_test_dir, clean_dir, fit_input_dict + test_dir, memory_jobstore, vasp_test_dir, fit_input_dict, clean_dir ): import os import shutil diff --git a/tutorials/README.md b/tutorials/README.md deleted file mode 100644 index 09eb53d4c..000000000 --- a/tutorials/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# This is a Tutorial for autoplex - -The example submission file can be simply run in a terminal with `./example_submission.py`. \ No newline at end of file diff --git a/tutorials/example_submission.py b/tutorials/example_submission.py deleted file mode 100755 index 787a4a3d7..000000000 --- a/tutorials/example_submission.py +++ /dev/null @@ -1,40 +0,0 @@ -from atomate2.vasp.jobs.base import BaseVaspMaker -from atomate2.vasp.sets.core import StaticSetGenerator -from autoplex.auto.flows import CompleteDFTvsMLBenchmarkWorkflow -from fireworks import LaunchPad -from jobflow.core.flow import Flow -from jobflow.managers.fireworks import flow_to_workflow -from jobflow.utils.graph import to_mermaid -from mp_api.client import MPRester - -# Please be aware that you need to use your new API key here. -mpr = MPRester(api_key="your MP API key") -# generate the structure list by using Materials Project IDs -struc_list = [] -mpids = ["mp-149", "mp-165"] -for mpid in mpids: - struc = mpr.get_structure_by_material_id(mpid) - struc_list.append(struc) - -# accuracy setting (grid_density, n_struc, symprec) are very low -phonon_stat = BaseVaspMaker( - input_set_generator=StaticSetGenerator( - user_kpoints_settings={"grid_density": 1}, - ) -) # reduced the accuracy for test calculations -complete_flow = CompleteDFTvsMLBenchmarkWorkflow( - n_struc=1, displacements=[0.01], symprec=1e-4, uc=False -).make(structure_list=struc_list, mp_ids=mpids, phonon_displacement_maker=phonon_stat) - -autoplex_flow = Flow( - [complete_flow], output=None, name="Si-AutoPLEX-Flow", uuid=None, hosts=None -) - -graph_source = to_mermaid(autoplex_flow, show_flow_boxes=True) -print(graph_source) # print text to generate a pretty mermaid graph (mermaid.live) - -wf = flow_to_workflow(autoplex_flow) - -# submit the workflow to the FireWorks launchpad -lpad = LaunchPad.auto_load() -lpad.add_wf(wf) diff --git a/tutorials/startFromExistingPotential.py b/tutorials/startFromExistingPotential.py deleted file mode 100644 index ee607f5f7..000000000 --- a/tutorials/startFromExistingPotential.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python - -from fireworks import LaunchPad -from jobflow.managers.fireworks import flow_to_workflow -from jobflow.core.flow import Flow -from autoplex.auto.jobs import PhononMLCalculationJob -from pymatgen.core import Structure - -# tutorial for calc with existing potential - -structure = Structure.from_file("POSCAR") # or e.g. cif or xyz format -name = "NameOfYourWorkflow" - -existing_pot = PhononMLCalculationJob( - structure=structure, - min_length=20, - ml_dir="/path/to/your/GAP/pot/gap.xml", -) - -tutorial_flow = Flow( - [existing_pot], output=None, name=name + "-AutoPLEX-Flow", uuid=None, hosts=None -) -wf = flow_to_workflow(tutorial_flow) - -# submit the workflow to the FireWorks launchpad -lpad = LaunchPad.auto_load() -lpad.add_wf(wf)