From f36c6e6c77472cfef8dd2f6b4a6319e1eb9f558a Mon Sep 17 00:00:00 2001 From: MoseyQAQ Date: Mon, 30 Sep 2024 02:27:04 +0800 Subject: [PATCH 01/13] interfaced with deepmd --- pyproject.toml | 1 + src/atomate2/forcefields/__init__.py | 1 + src/atomate2/forcefields/jobs.py | 80 +++++++++++++++++++ src/atomate2/forcefields/md.py | 16 ++++ src/atomate2/forcefields/utils.py | 5 ++ tests/forcefields/test_jobs.py | 72 +++++++++++++++++ tests/forcefields/test_md.py | 10 ++- tests/test_data/forcefields/deepmd/README.md | 7 ++ tests/test_data/forcefields/deepmd/graph.pb | Bin 0 -> 5254227 bytes 9 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 tests/test_data/forcefields/deepmd/README.md create mode 100644 tests/test_data/forcefields/deepmd/graph.pb diff --git a/pyproject.toml b/pyproject.toml index fb4a48ff5d..e775cbc42b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,7 @@ forcefields = [ "quippy-ase>=0.9.14; python_version < '3.12'", "sevenn>=0.9.3", "torchdata<=0.7.1", # TODO: remove when issue fixed + "deepmd>=2.1.4", ] ase = ["ase>=3.23.0"] # tblite py3.12 support tracked in https://github.com/tblite/tblite/issues/198 diff --git a/src/atomate2/forcefields/__init__.py b/src/atomate2/forcefields/__init__.py index e16c088654..9e578569d9 100644 --- a/src/atomate2/forcefields/__init__.py +++ b/src/atomate2/forcefields/__init__.py @@ -16,6 +16,7 @@ class MLFF(Enum): # TODO inherit from StrEnum when 3.11+ NEP = "NEP" Nequip = "Nequip" SevenNet = "SevenNet" + DeepMD = "DeepMD" def _get_formatted_ff_name(force_field_name: str | MLFF) -> str: diff --git a/src/atomate2/forcefields/jobs.py b/src/atomate2/forcefields/jobs.py index f2307e3a50..50594d2b69 100644 --- a/src/atomate2/forcefields/jobs.py +++ b/src/atomate2/forcefields/jobs.py @@ -34,6 +34,7 @@ MLFF.M3GNet: {"stress_weight": _GPa_to_eV_per_A3}, MLFF.NEP: {"model_filename": "nep.txt"}, MLFF.GAP: {"args_str": "IP GAP", "param_filename": "gap.xml"}, + MLFF.DeepMD: {"model": "graph.pb"}, } @@ -782,3 +783,82 @@ class GAPStaticMaker(ForceFieldStaticMaker): calculator_kwargs: dict = field( default_factory=lambda: _DEFAULT_CALCULATOR_KWARGS[MLFF.GAP] ) + + +@deprecated( + replacement=ForceFieldRelaxMaker, + deadline=(2025, 1, 1), + message="To use DeepMD, set `force_field_name = 'DeepMD'` in ForceFieldRelaxMaker.", +) +@dataclass +class DeepMDRelaxMaker(ForceFieldRelaxMaker): + """ + Base Maker to calculate forces and stresses using a Deep Potential (DP) model. + + Parameters + ---------- + name : str + The job name. + force_field_name : str or .MLFF + The name of the force field. + relax_cell : bool = True + Whether to allow the cell shape/volume to change during relaxation. + fix_symmetry : bool = False + Whether to fix the symmetry during relaxation. + Refines the symmetry of the initial structure. + symprec : float = 1e-2 + Tolerance for symmetry finding in case of fix_symmetry. + steps : int + Maximum number of ionic steps allowed during relaxation. + relax_kwargs : dict + Keyword arguments that will get passed to :obj:`AseRelaxer.relax`. + optimizer_kwargs : dict + Keyword arguments that will get passed to :obj:`AseRelaxer()`. + calculator_kwargs : dict + Keyword arguments that will get passed to the ASE calculator. + task_document_kwargs : dict (deprecated) + Additional keyword args passed to :obj:`.ForceFieldTaskDocument()`. + """ + + name: str = f"{MLFF.DeepMD} relax" + force_field_name: str | MLFF = MLFF.DeepMD + relax_cell: bool = True + fix_symmetry: bool = False + symprec: float = 1e-2 + steps: int = 500 + relax_kwargs: dict = field(default_factory=dict) + optimizer_kwargs: dict = field(default_factory=dict) + calculator_kwargs: dict = field( + default_factory=lambda: _DEFAULT_CALCULATOR_KWARGS[MLFF.DeepMD] + ) + task_document_kwargs: dict = field(default_factory=dict) + + +@deprecated( + replacement=ForceFieldRelaxMaker, + deadline=(2025, 1, 1), + message="To use DeepMD, set `force_field_name = 'DeepMD'` in ForceFieldRelaxMaker.", +) +@dataclass +class DeepMDStaticMaker(ForceFieldStaticMaker): + """ + Base Maker to calculate forces and stresses using a DeepPotential (DP) model. + + Parameters + ---------- + name : str + The job name. + force_field_name : str or .MLFF + The name of the force field. + calculator_kwargs : dict + Keyword arguments that will get passed to the ASE calculator. + task_document_kwargs : dict (deprecated) + Additional keyword args passed to :obj:`.ForceFieldTaskDocument()`. + """ + + name: str = f"{MLFF.DeepMD} static" + force_field_name: str | MLFF = MLFF.DeepMD + task_document_kwargs: dict = field(default_factory=dict) + calculator_kwargs: dict = field( + default_factory=lambda: _DEFAULT_CALCULATOR_KWARGS[MLFF.DeepMD] + ) diff --git a/src/atomate2/forcefields/md.py b/src/atomate2/forcefields/md.py index 44b14c14e7..3ff2304fb9 100644 --- a/src/atomate2/forcefields/md.py +++ b/src/atomate2/forcefields/md.py @@ -265,3 +265,19 @@ class NequipMDMaker(ForceFieldMDMaker): name: str = f"{MLFF.Nequip} MD" force_field_name: str | MLFF = MLFF.Nequip + + +@deprecated( + replacement=ForceFieldMDMaker, + deadline=(2025, 1, 1), + message="To use DP, set `force_field_name = 'DeepMD'` in ForceFieldMDMaker.", +) +@dataclass +class DeepMDMDMaker(ForceFieldMDMaker): + """Perform an MD run with DP.""" + + name: str = f"{MLFF.DeepMD} MD" + force_field_name: str | MLFF = MLFF.DeepMD + calculator_kwargs: dict = field( + default_factory=lambda: _DEFAULT_CALCULATOR_KWARGS[MLFF.DeepMD] + ) diff --git a/src/atomate2/forcefields/utils.py b/src/atomate2/forcefields/utils.py index 3d299ec39b..c1964274e7 100644 --- a/src/atomate2/forcefields/utils.py +++ b/src/atomate2/forcefields/utils.py @@ -83,6 +83,11 @@ def ase_calculator(calculator_meta: str | dict, **kwargs: Any) -> Calculator | N calculator = SevenNetCalculator(**{"model": "7net-0"} | kwargs) + elif calculator_name == MLFF.DeepMD: + from deepmd.calculator import DP + + calculator = DP(**kwargs) + elif isinstance(calculator_meta, dict): calc_cls = MontyDecoder().decode(json.dumps(calculator_meta)) calculator = calc_cls(**kwargs) diff --git a/tests/forcefields/test_jobs.py b/tests/forcefields/test_jobs.py index 145152edd5..0f2e4bdd24 100644 --- a/tests/forcefields/test_jobs.py +++ b/tests/forcefields/test_jobs.py @@ -10,6 +10,8 @@ from atomate2.forcefields.jobs import ( CHGNetRelaxMaker, CHGNetStaticMaker, + DeepMDRelaxMaker, + DeepMDStaticMaker, ForceFieldRelaxMaker, ForceFieldStaticMaker, GAPRelaxMaker, @@ -535,3 +537,73 @@ def test_nequip_relax_maker( with pytest.warns(FutureWarning): NequipRelaxMaker() + + +def test_deepmd_static_maker(sr_ti_o3_structure: Structure, test_dir: Path): + importorskip("deepmd") + + # generate job + # NOTE the test model is not trained on Si, so the energy is not accurate + job = ForceFieldStaticMaker( + force_field_name="DeepMD", + ionic_step_data=("structure", "energy"), + calculator_kwargs={"model": test_dir / "forcefields" / "deepmd" / "graph.pb"}, + ).make(sr_ti_o3_structure) + + # run the flow or job and ensure that it finished running successfully + responses = run_locally(job, ensure_success=True) + + # validation the outputs of the job + output1 = responses[job.uuid][1].output + assert isinstance(output1, ForceFieldTaskDocument) + assert output1.output.energy == approx(-44.40017, rel=1e-4) # change here + assert output1.output.n_steps == 1 + assert output1.forcefield_version == get_imported_version("deepmd") + + with pytest.warns(FutureWarning): + DeepMDStaticMaker() + + +@pytest.mark.parametrize( + ("relax_cell", "fix_symmetry"), + [(True, False), (False, True)], +) +def test_deepmd_relax_maker( + sr_ti_o3_structure: Structure, + test_dir: Path, + relax_cell: bool, + fix_symmetry: bool, +): + importorskip("deepmd") + # translate one atom to ensure a small number of relaxation steps are taken + sr_ti_o3_structure.translate_sites(0, [0, 0, 0.2]) + # generate job + job = ForceFieldRelaxMaker( + force_field_name="DeepMD", + steps=25, + optimizer_kwargs={"optimizer": "BFGSLineSearch"}, + relax_cell=relax_cell, + fix_symmetry=fix_symmetry, + calculator_kwargs={"model": test_dir / "forcefields" / "deepmd" / "graph.pb"}, + ).make(sr_ti_o3_structure) + + # run the flow or job and ensure that it finished running successfully + responses = run_locally(job, ensure_success=True) + + # validation the outputs of the job + output1 = responses[job.uuid][1].output + assert isinstance(output1, ForceFieldTaskDocument) + if relax_cell: + assert output1.output.energy == approx(-44.407, rel=1e-3) + assert output1.output.n_steps == 5 + else: + assert output1.output.energy == approx(-44.40015, rel=1e-4) + assert output1.output.n_steps == 5 + + # fix_symmetry makes no difference for this structure relaxer combo + # just testing that passing fix_symmetry doesn't break + final_spg_num = output1.output.structure.get_space_group_info()[1] + assert final_spg_num == 99 + + with pytest.warns(FutureWarning): + DeepMDRelaxMaker() diff --git a/tests/forcefields/test_md.py b/tests/forcefields/test_md.py index 2b11d0686e..e75c845bbf 100644 --- a/tests/forcefields/test_md.py +++ b/tests/forcefields/test_md.py @@ -15,6 +15,7 @@ from atomate2.forcefields.md import ( CHGNetMDMaker, + DeepMDMDMaker, ForceFieldMDMaker, GAPMDMaker, M3GNetMDMaker, @@ -30,6 +31,7 @@ "GAP": GAPMDMaker, "NEP": NEPMDMaker, "Nequip": NequipMDMaker, + "DeepMD": DeepMDMDMaker, } @@ -49,7 +51,7 @@ def test_maker_initialization(): @pytest.mark.parametrize( "ff_name", - ["CHGNet", "M3GNet", "MACE", "GAP", "NEP", "Nequip"], + ["CHGNet", "M3GNet", "MACE", "GAP", "NEP", "Nequip", "DeepMD"], ) def test_ml_ff_md_maker( ff_name, si_structure, sr_ti_o3_structure, al2_au_structure, test_dir, clean_dir @@ -68,6 +70,7 @@ def test_ml_ff_md_maker( "GAP": -5.391255755606209, "NEP": -3.966232215741286, "Nequip": -8.84670181274414, + "DeepMD": -5.391255755606209, # CHANGE THIS } # ASE can slightly change tolerances on structure positions @@ -96,6 +99,11 @@ def test_ml_ff_md_maker( "model_path": test_dir / "forcefields" / "nequip" / "nequip_ff_sr_ti_o3.pth" } unit_cell_structure = sr_ti_o3_structure.copy() + elif ff_name == "DeepMD": + calculator_kwargs = { + "model_path": test_dir / "forcefields" / "deepmd" / "graph.pb" + } + unit_cell_structure = sr_ti_o3_structure.copy() structure = unit_cell_structure.to_conventional() * (2, 2, 2) diff --git a/tests/test_data/forcefields/deepmd/README.md b/tests/test_data/forcefields/deepmd/README.md new file mode 100644 index 0000000000..f6363adc1d --- /dev/null +++ b/tests/test_data/forcefields/deepmd/README.md @@ -0,0 +1,7 @@ +# About this model + +The Deep Potential model used for this test is `UniPero`, a universal interatomic potential for perovskite oxides. + +It can be downloaded from: https://github.com/sliutheorygroup/UniPero, + +For more details, refer to the original article: https://doi.org/10.1103/PhysRevB.108.L180104 diff --git a/tests/test_data/forcefields/deepmd/graph.pb b/tests/test_data/forcefields/deepmd/graph.pb new file mode 100644 index 0000000000000000000000000000000000000000..c0494d5ebb087ce126791d66b844771f95ab74b9 GIT binary patch literal 5254227 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