From f8ad655173b10d9bdc7aaa77f07214b2bde2f8d8 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 28 Feb 2024 01:33:38 -0500 Subject: [PATCH 1/7] add @version to serialization data (#3349) At this time, all current versions and the minimal and maximum versions are set to 1. If any changes, whether breaking or not, are made to the serialization data, the version should be bumped. --------- Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../dpmodel/atomic_model/dp_atomic_model.py | 5 ++++ .../atomic_model/linear_atomic_model.py | 7 +++++ .../atomic_model/pairtab_atomic_model.py | 5 ++++ deepmd/dpmodel/descriptor/se_e2_a.py | 5 ++++ deepmd/dpmodel/descriptor/se_r.py | 5 ++++ deepmd/dpmodel/fitting/general_fitting.py | 5 ++++ deepmd/dpmodel/utils/network.py | 30 ++++++++++++++++++- .../pt/model/atomic_model/dp_atomic_model.py | 5 ++++ .../model/atomic_model/linear_atomic_model.py | 10 +++++++ .../atomic_model/pairtab_atomic_model.py | 7 +++++ deepmd/pt/model/descriptor/se_a.py | 5 ++++ deepmd/pt/model/descriptor/se_r.py | 5 ++++ deepmd/pt/model/task/fitting.py | 5 ++++ deepmd/tf/descriptor/se_a.py | 5 ++++ deepmd/tf/descriptor/se_r.py | 5 ++++ deepmd/tf/fit/dipole.py | 6 ++++ deepmd/tf/fit/ener.py | 6 ++++ deepmd/tf/fit/polar.py | 6 ++++ deepmd/tf/model/model.py | 6 +++- deepmd/utils/pair_tab.py | 9 ++++++ deepmd/utils/version.py | 27 +++++++++++++++++ 21 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 deepmd/utils/version.py diff --git a/deepmd/dpmodel/atomic_model/dp_atomic_model.py b/deepmd/dpmodel/atomic_model/dp_atomic_model.py index 178b286e79..cd349749fa 100644 --- a/deepmd/dpmodel/atomic_model/dp_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/dp_atomic_model.py @@ -17,6 +17,9 @@ from deepmd.dpmodel.output_def import ( FittingOutputDef, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .base_atomic_model import ( BaseAtomicModel, @@ -132,6 +135,7 @@ def serialize(self) -> dict: return { "@class": "Model", "type": "standard", + "@version": 1, "type_map": self.type_map, "descriptor": self.descriptor.serialize(), "fitting": self.fitting.serialize(), @@ -140,6 +144,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "DPAtomicModel": data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") descriptor_obj = BaseDescriptor.deserialize(data["descriptor"]) diff --git a/deepmd/dpmodel/atomic_model/linear_atomic_model.py b/deepmd/dpmodel/atomic_model/linear_atomic_model.py index e1130eaf45..6d8aea499e 100644 --- a/deepmd/dpmodel/atomic_model/linear_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/linear_atomic_model.py @@ -19,6 +19,9 @@ get_multiple_nlist_key, nlist_distinguish_types, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from ..output_def import ( FittingOutputDef, @@ -185,6 +188,7 @@ def serialize(models) -> dict: return { "@class": "Model", "type": "linear", + "@version": 1, "models": [model.serialize() for model in models], "model_name": [model.__class__.__name__ for model in models], } @@ -192,6 +196,7 @@ def serialize(models) -> dict: @staticmethod def deserialize(data) -> List[BaseAtomicModel]: data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") model_names = data["model_name"] @@ -271,6 +276,7 @@ def serialize(self) -> dict: return { "@class": "Model", "type": "zbl", + "@version": 1, "models": LinearAtomicModel.serialize([self.dp_model, self.zbl_model]), "sw_rmin": self.sw_rmin, "sw_rmax": self.sw_rmax, @@ -280,6 +286,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "DPZBLLinearAtomicModel": data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") sw_rmin = data["sw_rmin"] diff --git a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py index dc3dfaf2ed..ddece80f2d 100644 --- a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py @@ -16,6 +16,9 @@ from deepmd.utils.pair_tab import ( PairTab, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .base_atomic_model import ( BaseAtomicModel, @@ -109,6 +112,7 @@ def serialize(self) -> dict: return { "@class": "Model", "type": "pairtab", + "@version": 1, "tab": self.tab.serialize(), "rcut": self.rcut, "sel": self.sel, @@ -117,6 +121,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "PairTabAtomicModel": data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") rcut = data["rcut"] diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index a28215c35a..b102933ac9 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -9,6 +9,9 @@ from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.version import ( + check_version_compatibility, +) try: from deepmd._version import version as __version__ @@ -345,6 +348,7 @@ def serialize(self) -> dict: return { "@class": "Descriptor", "type": "se_e2_a", + "@version": 1, "rcut": self.rcut, "rcut_smth": self.rcut_smth, "sel": self.sel, @@ -371,6 +375,7 @@ def serialize(self) -> dict: def deserialize(cls, data: dict) -> "DescrptSeA": """Deserialize from dict.""" data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class", None) data.pop("type", None) variables = data.pop("@variables") diff --git a/deepmd/dpmodel/descriptor/se_r.py b/deepmd/dpmodel/descriptor/se_r.py index 77e43f7d85..5973c55353 100644 --- a/deepmd/dpmodel/descriptor/se_r.py +++ b/deepmd/dpmodel/descriptor/se_r.py @@ -4,6 +4,9 @@ from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.version import ( + check_version_compatibility, +) try: from deepmd._version import version as __version__ @@ -282,6 +285,7 @@ def serialize(self) -> dict: return { "@class": "Descriptor", "type": "se_r", + "@version": 1, "rcut": self.rcut, "rcut_smth": self.rcut_smth, "sel": self.sel, @@ -307,6 +311,7 @@ def serialize(self) -> dict: def deserialize(cls, data: dict) -> "DescrptSeR": """Deserialize from dict.""" data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class", None) data.pop("type", None) variables = data.pop("@variables") diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py index 152836e928..752a550849 100644 --- a/deepmd/dpmodel/fitting/general_fitting.py +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -21,6 +21,9 @@ FittingNet, NetworkCollection, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .base_fitting import ( BaseFitting, @@ -210,6 +213,7 @@ def serialize(self) -> dict: """Serialize the fitting to dict.""" return { "@class": "Fitting", + "@version": 1, "var_name": self.var_name, "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, @@ -241,6 +245,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data: dict) -> "GeneralFitting": data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") data.pop("type") variables = data.pop("@variables") diff --git a/deepmd/dpmodel/utils/network.py b/deepmd/dpmodel/utils/network.py index 2133bc4889..feb3355e77 100644 --- a/deepmd/dpmodel/utils/network.py +++ b/deepmd/dpmodel/utils/network.py @@ -20,6 +20,10 @@ import h5py import numpy as np +from deepmd.utils.version import ( + check_version_compatibility, +) + try: from deepmd._version import version as __version__ except ImportError: @@ -189,6 +193,8 @@ def serialize(self) -> dict: "idt": self.idt, } return { + "@class": "Layer", + "@version": 1, "bias": self.b is not None, "use_timestep": self.idt is not None, "activation_function": self.activation_function, @@ -208,6 +214,8 @@ def deserialize(cls, data: dict) -> "NativeLayer": The dict to deserialize from. """ data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) + data.pop("@class", None) variables = data.pop("@variables") assert variables["w"] is not None and len(variables["w"].shape) == 2 num_in, num_out = variables["w"].shape @@ -349,7 +357,11 @@ def serialize(self) -> dict: dict The serialized network. """ - return {"layers": [layer.serialize() for layer in self.layers]} + return { + "@class": "NN", + "@version": 1, + "layers": [layer.serialize() for layer in self.layers], + } @classmethod def deserialize(cls, data: dict) -> "NN": @@ -360,6 +372,9 @@ def deserialize(cls, data: dict) -> "NN": data : dict The dict to deserialize from. """ + data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) + data.pop("@class", None) return cls(data["layers"]) def __getitem__(self, key): @@ -471,6 +486,8 @@ def serialize(self) -> dict: The serialized network. """ return { + "@class": "EmbeddingNetwork", + "@version": 1, "in_dim": self.in_dim, "neuron": self.neuron.copy(), "activation_function": self.activation_function, @@ -490,6 +507,8 @@ def deserialize(cls, data: dict) -> "EmbeddingNet": The dict to deserialize from. """ data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) + data.pop("@class", None) layers = data.pop("layers") obj = cls(**data) super(EN, obj).__init__(layers) @@ -566,6 +585,8 @@ def serialize(self) -> dict: The serialized network. """ return { + "@class": "FittingNetwork", + "@version": 1, "in_dim": self.in_dim, "out_dim": self.out_dim, "neuron": self.neuron.copy(), @@ -586,6 +607,8 @@ def deserialize(cls, data: dict) -> "FittingNet": The dict to deserialize from. """ data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) + data.pop("@class", None) layers = data.pop("layers") obj = cls(**data) T_Network.__init__(obj, layers) @@ -688,6 +711,8 @@ def serialize(self) -> dict: network_type_map_inv = {v: k for k, v in self.NETWORK_TYPE_MAP.items()} network_type_name = network_type_map_inv[self.network_type] return { + "@class": "NetworkCollection", + "@version": 1, "ndim": self.ndim, "ntypes": self.ntypes, "network_type": network_type_name, @@ -703,4 +728,7 @@ def deserialize(cls, data: dict) -> "NetworkCollection": data : dict The dict to deserialize from. """ + data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) + data.pop("@class", None) return cls(**data) diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index 881ea4c97d..d2c1743d30 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -24,6 +24,9 @@ from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .base_atomic_model import ( BaseAtomicModel, @@ -95,6 +98,7 @@ def serialize(self) -> dict: return { "@class": "Model", "type": "standard", + "@version": 1, "type_map": self.type_map, "descriptor": self.descriptor.serialize(), "fitting": self.fitting_net.serialize(), @@ -103,6 +107,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "DPAtomicModel": data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) descriptor_obj = BaseDescriptor.deserialize(data["descriptor"]) fitting_obj = BaseFitting.deserialize(data["fitting"]) obj = cls(descriptor_obj, fitting_obj, type_map=data["type_map"]) diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 68ff303d64..52f5f1d13c 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy import sys from abc import ( abstractmethod, @@ -24,6 +25,9 @@ get_multiple_nlist_key, nlist_distinguish_types, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .base_atomic_model import ( BaseAtomicModel, @@ -206,6 +210,7 @@ def fitting_output_def(self) -> FittingOutputDef: def serialize(models) -> dict: return { "@class": "Model", + "@version": 1, "type": "linear", "models": [model.serialize() for model in models], "model_name": [model.__class__.__name__ for model in models], @@ -213,6 +218,8 @@ def serialize(models) -> dict: @staticmethod def deserialize(data) -> List[BaseAtomicModel]: + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) model_names = data["model_name"] models = [ getattr(sys.modules[__name__], name).deserialize(model) @@ -303,6 +310,7 @@ def serialize(self) -> dict: return { "@class": "Model", "type": "zbl", + "@version": 1, "models": LinearAtomicModel.serialize([self.dp_model, self.zbl_model]), "sw_rmin": self.sw_rmin, "sw_rmax": self.sw_rmax, @@ -311,6 +319,8 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "DPZBLLinearAtomicModel": + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) sw_rmin = data["sw_rmin"] sw_rmax = data["sw_rmax"] smin_alpha = data["smin_alpha"] diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index 86bfe98c36..c0b7c65d7a 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: LGPL-3.0-or-later +import copy from typing import ( Dict, List, @@ -15,6 +16,9 @@ from deepmd.utils.pair_tab import ( PairTab, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .base_atomic_model import ( BaseAtomicModel, @@ -124,6 +128,7 @@ def serialize(self) -> dict: return { "@class": "Model", "type": "pairtab", + "@version": 1, "tab": self.tab.serialize(), "rcut": self.rcut, "sel": self.sel, @@ -131,6 +136,8 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "PairTabAtomicModel": + data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) rcut = data["rcut"] sel = data["sel"] tab = PairTab.deserialize(data["tab"]) diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 033d640ad8..6c29636d6d 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -31,6 +31,9 @@ from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.version import ( + check_version_compatibility, +) try: from typing import ( @@ -182,6 +185,7 @@ def serialize(self) -> dict: return { "@class": "Descriptor", "type": "se_e2_a", + "@version": 1, "rcut": obj.rcut, "rcut_smth": obj.rcut_smth, "sel": obj.sel, @@ -208,6 +212,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data: dict) -> "DescrptSeA": data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class", None) data.pop("type", None) variables = data.pop("@variables") diff --git a/deepmd/pt/model/descriptor/se_r.py b/deepmd/pt/model/descriptor/se_r.py index c685640426..bdb7dafe73 100644 --- a/deepmd/pt/model/descriptor/se_r.py +++ b/deepmd/pt/model/descriptor/se_r.py @@ -36,6 +36,9 @@ from deepmd.utils.path import ( DPPath, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .base_descriptor import ( BaseDescriptor, @@ -277,6 +280,7 @@ def serialize(self) -> dict: return { "@class": "Descriptor", "type": "se_r", + "@version": 1, "rcut": self.rcut, "rcut_smth": self.rcut_smth, "sel": self.sel, @@ -302,6 +306,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data: dict) -> "DescrptSeR": data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) variables = data.pop("@variables") embeddings = data.pop("embeddings") env_mat = data.pop("env_mat") diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 6c395d3800..0c64983f60 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -43,6 +43,9 @@ to_numpy_array, to_torch_tensor, ) +from deepmd.utils.version import ( + check_version_compatibility, +) dtype = env.GLOBAL_PT_FLOAT_PRECISION device = env.DEVICE @@ -367,6 +370,7 @@ def serialize(self) -> dict: """Serialize the fitting to dict.""" return { "@class": "Fitting", + "@version": 1, "var_name": self.var_name, "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, @@ -404,6 +408,7 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data: dict) -> "GeneralFitting": data = copy.deepcopy(data) + check_version_compatibility(data.pop("@version", 1), 1, 1) variables = data.pop("@variables") nets = data.pop("nets") obj = cls(**data) diff --git a/deepmd/tf/descriptor/se_a.py b/deepmd/tf/descriptor/se_a.py index e1b7258c63..0e15ba13a8 100644 --- a/deepmd/tf/descriptor/se_a.py +++ b/deepmd/tf/descriptor/se_a.py @@ -65,6 +65,9 @@ from deepmd.tf.utils.type_embed import ( embed_atom_type, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .descriptor import ( Descriptor, @@ -1368,6 +1371,7 @@ def deserialize(cls, data: dict, suffix: str = ""): if cls is not DescrptSeA: raise NotImplementedError("Not implemented in class %s" % cls.__name__) data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class", None) data.pop("type", None) embedding_net_variables = cls.deserialize_network( @@ -1422,6 +1426,7 @@ def serialize(self, suffix: str = "") -> dict: return { "@class": "Descriptor", "type": "se_e2_a", + "@version": 1, "rcut": self.rcut_r, "rcut_smth": self.rcut_r_smth, "sel": self.sel_a, diff --git a/deepmd/tf/descriptor/se_r.py b/deepmd/tf/descriptor/se_r.py index 1a12befdf0..ba1a261390 100644 --- a/deepmd/tf/descriptor/se_r.py +++ b/deepmd/tf/descriptor/se_r.py @@ -38,6 +38,9 @@ from deepmd.tf.utils.tabulate import ( DPTabulate, ) +from deepmd.utils.version import ( + check_version_compatibility, +) from .descriptor import ( Descriptor, @@ -720,6 +723,7 @@ def deserialize(cls, data: dict, suffix: str = ""): if cls is not DescrptSeR: raise NotImplementedError("Not implemented in class %s" % cls.__name__) data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) embedding_net_variables = cls.deserialize_network( data.pop("embeddings"), suffix=suffix ) @@ -763,6 +767,7 @@ def serialize(self, suffix: str = "") -> dict: return { "@class": "Descriptor", "type": "se_r", + "@version": 1, "rcut": self.rcut, "rcut_smth": self.rcut_smth, "sel": self.sel_r, diff --git a/deepmd/tf/fit/dipole.py b/deepmd/tf/fit/dipole.py index 3557d00aa0..f503789308 100644 --- a/deepmd/tf/fit/dipole.py +++ b/deepmd/tf/fit/dipole.py @@ -30,6 +30,9 @@ one_layer, one_layer_rand_seed_shift, ) +from deepmd.utils.version import ( + check_version_compatibility, +) @Fitting.register("dipole") @@ -346,6 +349,7 @@ def serialize(self, suffix: str) -> dict: data = { "@class": "Fitting", "type": "dipole", + "@version": 1, "var_name": "dipole", "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, @@ -388,6 +392,8 @@ def deserialize(cls, data: dict, suffix: str): Model The deserialized model """ + data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) fitting = cls(**data) fitting.fitting_net_variables = cls.deserialize_network( data["nets"], diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index 0cdd1a1676..106e10839d 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -53,6 +53,9 @@ from deepmd.tf.utils.spin import ( Spin, ) +from deepmd.utils.version import ( + check_version_compatibility, +) if TYPE_CHECKING: pass @@ -959,6 +962,8 @@ def deserialize(cls, data: dict, suffix: str = ""): Model The deserialized model """ + data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) fitting = cls(**data) fitting.fitting_net_variables = cls.deserialize_network( data["nets"], @@ -984,6 +989,7 @@ def serialize(self, suffix: str = "") -> dict: data = { "@class": "Fitting", "type": "ener", + "@version": 1, "var_name": "energy", "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, diff --git a/deepmd/tf/fit/polar.py b/deepmd/tf/fit/polar.py index f5cebf9a39..002082ad2e 100644 --- a/deepmd/tf/fit/polar.py +++ b/deepmd/tf/fit/polar.py @@ -34,6 +34,9 @@ one_layer, one_layer_rand_seed_shift, ) +from deepmd.utils.version import ( + check_version_compatibility, +) @Fitting.register("polar") @@ -536,6 +539,7 @@ def serialize(self, suffix: str) -> dict: data = { "@class": "Fitting", "type": "polar", + "@version": 1, "var_name": "polar", "ntypes": self.ntypes, "dim_descrpt": self.dim_descrpt, @@ -581,6 +585,8 @@ def deserialize(cls, data: dict, suffix: str): Model The deserialized model """ + data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) fitting = cls(**data) fitting.fitting_net_variables = cls.deserialize_network( data["nets"], diff --git a/deepmd/tf/model/model.py b/deepmd/tf/model/model.py index 2ae2879226..889f7ccc4d 100644 --- a/deepmd/tf/model/model.py +++ b/deepmd/tf/model/model.py @@ -63,6 +63,9 @@ from deepmd.utils.plugin import ( make_plugin_registry, ) +from deepmd.utils.version import ( + check_version_compatibility, +) class Model(ABC, make_plugin_registry("model")): @@ -778,7 +781,7 @@ def deserialize(cls, data: dict, suffix: str = "") -> "Descriptor": The deserialized descriptor """ data = copy.deepcopy(data) - + check_version_compatibility(data.pop("@version", 1), 1, 1) descriptor = Descriptor.deserialize(data.pop("descriptor"), suffix=suffix) fitting = Fitting.deserialize(data.pop("fitting"), suffix=suffix) return cls( @@ -807,6 +810,7 @@ def serialize(self, suffix: str = "") -> dict: return { "@class": "Model", "type": "standard", + "@version": 1, "type_map": self.type_map, "descriptor": self.descrpt.serialize(suffix=suffix), "fitting": self.fitting.serialize(suffix=suffix), diff --git a/deepmd/utils/pair_tab.py b/deepmd/utils/pair_tab.py index b807354171..1b397a3cfa 100644 --- a/deepmd/utils/pair_tab.py +++ b/deepmd/utils/pair_tab.py @@ -12,6 +12,10 @@ CubicSpline, ) +from deepmd.utils.version import ( + check_version_compatibility, +) + log = logging.getLogger(__name__) @@ -72,6 +76,8 @@ def reinit(self, filename: str, rcut: Optional[float] = None) -> None: def serialize(self) -> dict: return { + "@class": "PairTab", + "@version": 1, "rmin": self.rmin, "rmax": self.rmax, "hh": self.hh, @@ -87,6 +93,9 @@ def serialize(self) -> dict: @classmethod def deserialize(cls, data) -> "PairTab": + data = data.copy() + check_version_compatibility(data.pop("@version", 1), 1, 1) + data.pop("@class") variables = data.pop("@variables") tab = PairTab(None, None) tab.vdata = variables["vdata"] diff --git a/deepmd/utils/version.py b/deepmd/utils/version.py new file mode 100644 index 0000000000..a0b479778d --- /dev/null +++ b/deepmd/utils/version.py @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +def check_version_compatibility( + current_version: int, + maximum_supported_version: int, + minimal_supported_version: int = 1, +): + """Check if the current version is compatible with the supported versions. + + Parameters + ---------- + current_version : int + The current version. + maximum_supported_version : int + The maximum supported version. + minimal_supported_version : int, optional + The minimal supported version. Default is 1. + + Raises + ------ + ValueError + If the current version is not compatible with the supported versions. + """ + if not minimal_supported_version <= current_version <= maximum_supported_version: + raise ValueError( + f"Current version {current_version} is not compatible with supported versions " + f"[{minimal_supported_version}, {maximum_supported_version}]." + ) From fd17e2efcf347790100f170fffbaaafb494ee7c2 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Wed, 28 Feb 2024 15:08:43 +0800 Subject: [PATCH 2/7] Fix: se_r prod_env_mat (#3351) This should fix the bug. image --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/pt/utils/env_mat_stat.py | 9 +++ source/tests/pt/model/test_descriptor_se_r.py | 55 ++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/deepmd/pt/utils/env_mat_stat.py b/deepmd/pt/utils/env_mat_stat.py index 3af03bda97..70b7228440 100644 --- a/deepmd/pt/utils/env_mat_stat.py +++ b/deepmd/pt/utils/env_mat_stat.py @@ -101,6 +101,14 @@ def iter( dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE, ) + if self.last_dim == 4: + radial_only = False + elif self.last_dim == 1: + radial_only = True + else: + raise ValueError( + "last_dim should be 1 for raial-only or 4 for full descriptor." + ) for system in data: coord, atype, box, natoms = ( system["coord"], @@ -130,6 +138,7 @@ def iter( self.descriptor.get_rcut(), # TODO: export rcut_smth from DescriptorBlock self.descriptor.rcut_smth, + radial_only, ) # reshape to nframes * nloc at the atom level, # so nframes/mixed_type do not matter diff --git a/source/tests/pt/model/test_descriptor_se_r.py b/source/tests/pt/model/test_descriptor_se_r.py index c999f06863..5b8b6c9251 100644 --- a/source/tests/pt/model/test_descriptor_se_r.py +++ b/source/tests/pt/model/test_descriptor_se_r.py @@ -15,6 +15,9 @@ from deepmd.pt.utils.env import ( PRECISION_DICT, ) +from deepmd.pt.utils.env_mat_stat import ( + EnvMatStatSe, +) from .test_env_mat import ( TestCaseSingleFrameWithNlist, @@ -103,13 +106,61 @@ def test_consistency( err_msg=err_msg, ) + def test_load_stat(self): + rng = np.random.default_rng() + _, _, nnei = self.nlist.shape + davg = rng.normal(size=(self.nt, nnei, 1)) + dstd = rng.normal(size=(self.nt, nnei, 1)) + dstd = 0.1 + np.abs(dstd) + + for idt, prec in itertools.product( + [False, True], + ["float64", "float32"], + ): + dtype = PRECISION_DICT[prec] + + # sea new impl + dd0 = DescrptSeR( + self.rcut, + self.rcut_smth, + self.sel, + precision=prec, + resnet_dt=idt, + old_impl=False, + ) + dd0.mean = torch.tensor(davg, dtype=dtype, device=env.DEVICE) + dd0.dstd = torch.tensor(dstd, dtype=dtype, device=env.DEVICE) + dd1 = DescrptSeR.deserialize(dd0.serialize()) + dd1.compute_input_stats( + [ + { + "r0": None, + "coord": torch.from_numpy(self.coord_ext) + .reshape(-1, self.nall, 3) + .to(env.DEVICE), + "atype": torch.from_numpy(self.atype_ext).to(env.DEVICE), + "box": None, + "natoms": self.nall, + } + ] + ) + + with self.assertRaises(ValueError) as cm: + ev = EnvMatStatSe(dd1) + ev.last_dim = 3 + ev.load_or_compute_stats([]) + self.assertEqual( + "last_dim should be 1 for raial-only or 4 for full descriptor.", + str(cm.exception), + ) + def test_jit( self, ): rng = np.random.default_rng() _, _, nnei = self.nlist.shape - davg = rng.normal(size=(self.nt, nnei, 4)) - dstd = rng.normal(size=(self.nt, nnei, 4)) + davg = rng.normal(size=(self.nt, nnei, 1)) + dstd = rng.normal(size=(self.nt, nnei, 1)) dstd = 0.1 + np.abs(dstd) for idt, prec in itertools.product( From d377ccb19bc3d2fd5fc3161ba76668772362a519 Mon Sep 17 00:00:00 2001 From: Lysithea <52808607+CaRoLZhangxy@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:13:52 +0800 Subject: [PATCH 3/7] cc: add torch backend support for Multiple backend (#3162) need to test in union environment (tf and pt) see https://github.com/deepmodeling/deepmd-kit/issues/3119 --------- Signed-off-by: Lysithea <52808607+CaRoLZhangxy@users.noreply.github.com> Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Jinzhe Zeng Co-authored-by: Han Wang <92130845+wanghan-iapcm@users.noreply.github.com> --- .github/workflows/test_cc.yml | 4 +- .github/workflows/test_cuda.yml | 12 +- deepmd/pt/model/model/make_model.py | 7 + source/api_cc/include/DeepPotPT.h | 332 ++++++++++++ source/api_cc/include/commonPT.h | 24 + source/api_cc/src/DeepPot.cc | 19 +- source/api_cc/src/DeepPotPT.cc | 356 ++++++++++++- source/api_cc/src/commonPT.cc | 23 + source/api_cc/tests/test_deeppot_pt.cc | 625 ++++++++++++++++++++++ source/install/test_cc_local.sh | 10 +- source/ipi/tests/test_driver.py | 105 ++++ source/lmp/tests/test_lammps_pt.py | 695 +++++++++++++++++++++++++ source/tests/infer/deeppot_sea.pth | Bin 0 -> 123025 bytes 13 files changed, 2201 insertions(+), 11 deletions(-) create mode 100644 source/api_cc/include/DeepPotPT.h create mode 100644 source/api_cc/include/commonPT.h create mode 100644 source/api_cc/src/commonPT.cc create mode 100644 source/api_cc/tests/test_deeppot_pt.cc create mode 100644 source/lmp/tests/test_lammps_pt.py create mode 100644 source/tests/infer/deeppot_sea.pth diff --git a/.github/workflows/test_cc.yml b/.github/workflows/test_cc.yml index 2082e7e4cc..d98f8ca58d 100644 --- a/.github/workflows/test_cc.yml +++ b/.github/workflows/test_cc.yml @@ -56,7 +56,7 @@ jobs: TF_INTRA_OP_PARALLELISM_THREADS: 1 TF_INTER_OP_PARALLELISM_THREADS: 1 LAMMPS_PLUGIN_PATH: ${{ github.workspace }}/dp_test/lib/deepmd_lmp - LD_LIBRARY_PATH: ${{ github.workspace }}/dp_test/lib + LD_LIBRARY_PATH: ${{ github.workspace }}/dp_test/lib:${{ github.workspace }}/libtorch/lib if: ${{ !matrix.check_memleak }} # test ipi - run: pytest --cov=deepmd source/ipi/tests @@ -65,7 +65,7 @@ jobs: TF_INTRA_OP_PARALLELISM_THREADS: 1 TF_INTER_OP_PARALLELISM_THREADS: 1 PATH: ${{ github.workspace }}/dp_test/bin:$PATH - LD_LIBRARY_PATH: ${{ github.workspace }}/dp_test/lib + LD_LIBRARY_PATH: ${{ github.workspace }}/dp_test/lib:${{ github.workspace }}/libtorch/lib if: ${{ !matrix.check_memleak }} - uses: codecov/codecov-action@v4 env: diff --git a/.github/workflows/test_cuda.yml b/.github/workflows/test_cuda.yml index 0d934e6d77..915d983663 100644 --- a/.github/workflows/test_cuda.yml +++ b/.github/workflows/test_cuda.yml @@ -38,6 +38,8 @@ jobs: with: useLocalCache: true useCloudCache: false + - name: Install wget and unzip + run: apt-get update && apt-get install -y wget unzip - run: | wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.0-1_all.deb \ && sudo dpkg -i cuda-keyring_1.0-1_all.deb \ @@ -53,7 +55,13 @@ jobs: DP_ENABLE_NATIVE_OPTIMIZATION: 1 - run: dp --version - run: python -m pytest source/tests --durations=0 - - run: source/install/test_cc_local.sh + - name: Download libtorch + run: | + wget https://download.pytorch.org/libtorch/cu121/libtorch-cxx11-abi-shared-with-deps-2.2.1%2Bcu121.zip -O libtorch.zip + unzip libtorch.zip + - run: | + export CMAKE_PREFIX_PATH=$GITHUB_WORKSPACE/libtorch + source/install/test_cc_local.sh env: OMP_NUM_THREADS: 1 TF_INTRA_OP_PARALLELISM_THREADS: 1 @@ -63,7 +71,7 @@ jobs: DP_VARIANT: cuda DP_USE_MPICH2: 1 - run: | - export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/dp_test/lib:$CUDA_PATH/lib64:$LD_LIBRARY_PATH + export LD_LIBRARY_PATH=$GITHUB_WORKSPACE/dp_test/lib:$GITHUB_WORKSPACE/libtorch/lib:$CUDA_PATH/lib64:$LD_LIBRARY_PATH export PATH=$GITHUB_WORKSPACE/dp_test/bin:$PATH python -m pytest source/lmp/tests python -m pytest source/ipi/tests diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 79634186e4..b6478d297f 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -17,6 +17,9 @@ communicate_extended_output, fit_output_to_model_output, ) +from deepmd.pt.utils import ( + env, +) from deepmd.pt.utils.nlist import ( extend_input_and_build_neighbor_list, nlist_distinguish_types, @@ -115,6 +118,9 @@ def forward_common( The keys are defined by the `ModelOutputDef`. """ + coord = coord.to(env.GLOBAL_PT_FLOAT_PRECISION) + if box is not None: + box = box.to(env.GLOBAL_PT_FLOAT_PRECISION) ( extended_coord, extended_atype, @@ -183,6 +189,7 @@ def forward_common_lower( the result dict, defined by the `FittingOutputDef`. """ + extended_coord = extended_coord.to(env.GLOBAL_PT_FLOAT_PRECISION) nframes, nall = extended_atype.shape[:2] extended_coord = extended_coord.view(nframes, -1, 3) nlist = self.format_nlist(extended_coord, extended_atype, nlist) diff --git a/source/api_cc/include/DeepPotPT.h b/source/api_cc/include/DeepPotPT.h new file mode 100644 index 0000000000..1b757069c3 --- /dev/null +++ b/source/api_cc/include/DeepPotPT.h @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#pragma once + +#include + +#include "DeepPot.h" +#include "commonPT.h" + +namespace deepmd { +/** + * @brief PyTorch implementation for Deep Potential. + **/ +class DeepPotPT : public DeepPotBase { + public: + /** + * @brief DP constructor without initialization. + **/ + DeepPotPT(); + ~DeepPotPT(); + /** + * @brief DP constructor with initialization. + * @param[in] model The name of the frozen model file. + * @param[in] gpu_rank The GPU rank. Default is 0. + * @param[in] file_content The content of the model file. If it is not empty, + *DP will read from the string instead of the file. + **/ + DeepPotPT(const std::string& model, + const int& gpu_rank = 0, + const std::string& file_content = ""); + /** + * @brief Initialize the DP. + * @param[in] model The name of the frozen model file. + * @param[in] gpu_rank The GPU rank. Default is 0. + * @param[in] file_content The content of the model file. If it is not empty, + *DP will read from the string instead of the file. + **/ + void init(const std::string& model, + const int& gpu_rank = 0, + const std::string& file_content = ""); + + private: + /** + * @brief Evaluate the energy, force, virial, atomic energy, and atomic virial + *by using this DP. + * @param[out] ener The system energy. + * @param[out] force The force on each atom. + * @param[out] virial The virial. + * @param[out] atom_energy The atomic energy. + * @param[out] atom_virial The atomic virial. + * @param[in] coord The coordinates of atoms. The array should be of size + *nframes x natoms x 3. + * @param[in] atype The atom types. The list should contain natoms ints. + * @param[in] box The cell of the region. The array should be of size nframes + *x 9. + * @param[in] fparam The frame parameter. The array can be of size : + * nframes x dim_fparam. + * dim_fparam. Then all frames are assumed to be provided with the same + *fparam. + * @param[in] aparam The atomic parameter The array can be of size : + * nframes x natoms x dim_aparam. + * natoms x dim_aparam. Then all frames are assumed to be provided with the + *same aparam. + **/ + template + void compute(ENERGYVTYPE& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box); + // const std::vector& fparam = std::vector(), + // const std::vector& aparam = std::vector()); + /** + * @brief Evaluate the energy, force, virial, atomic energy, and atomic virial + *by using this DP. + * @param[out] ener The system energy. + * @param[out] force The force on each atom. + * @param[out] virial The virial. + * @param[out] atom_energy The atomic energy. + * @param[out] atom_virial The atomic virial. + * @param[in] coord The coordinates of atoms. The array should be of size + *nframes x natoms x 3. + * @param[in] atype The atom types. The list should contain natoms ints. + * @param[in] box The cell of the region. The array should be of size nframes + *x 9. + * @param[in] nghost The number of ghost atoms. + * @param[in] lmp_list The input neighbour list. + * @param[in] ago Update the internal neighbour list if ago is 0. + * @param[in] fparam The frame parameter. The array can be of size : + * nframes x dim_fparam. + * dim_fparam. Then all frames are assumed to be provided with the same + *fparam. + * @param[in] aparam The atomic parameter The array can be of size : + * nframes x natoms x dim_aparam. + * natoms x dim_aparam. Then all frames are assumed to be provided with the + *same aparam. + **/ + template + void compute(ENERGYVTYPE& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + // const int nghost, + const InputNlist& lmp_list, + const int& ago); + // const std::vector& fparam = std::vector(), + // const std::vector& aparam = std::vector()); + /** + * @brief Evaluate the energy, force, and virial with the mixed type + *by using this DP. + * @param[out] ener The system energy. + * @param[out] force The force on each atom. + * @param[out] virial The virial. + * @param[in] nframes The number of frames. + * @param[in] coord The coordinates of atoms. The array should be of size + *nframes x natoms x 3. + * @param[in] atype The atom types. The array should be of size nframes x + *natoms. + * @param[in] box The cell of the region. The array should be of size nframes + *x 9. + * @param[in] fparam The frame parameter. The array can be of size : + * nframes x dim_fparam. + * dim_fparam. Then all frames are assumed to be provided with the same + *fparam. + * @param[in] aparam The atomic parameter The array can be of size : + * nframes x natoms x dim_aparam. + * natoms x dim_aparam. Then all frames are assumed to be provided with the + *same aparam. + **/ + template + void compute_mixed_type( + ENERGYVTYPE& ener, + std::vector& force, + std::vector& virial, + const int& nframes, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + /** + * @brief Evaluate the energy, force, and virial with the mixed type + *by using this DP. + * @param[out] ener The system energy. + * @param[out] force The force on each atom. + * @param[out] virial The virial. + * @param[out] atom_energy The atomic energy. + * @param[out] atom_virial The atomic virial. + * @param[in] nframes The number of frames. + * @param[in] coord The coordinates of atoms. The array should be of size + *nframes x natoms x 3. + * @param[in] atype The atom types. The array should be of size nframes x + *natoms. + * @param[in] box The cell of the region. The array should be of size nframes + *x 9. + * @param[in] fparam The frame parameter. The array can be of size : + * nframes x dim_fparam. + * dim_fparam. Then all frames are assumed to be provided with the same + *fparam. + * @param[in] aparam The atomic parameter The array can be of size : + * nframes x natoms x dim_aparam. + * natoms x dim_aparam. Then all frames are assumed to be provided with the + *same aparam. + **/ + template + void compute_mixed_type( + ENERGYVTYPE& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const int& nframes, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + + public: + /** + * @brief Get the cutoff radius. + * @return The cutoff radius. + **/ + double cutoff() const { + assert(inited); + return rcut; + }; + /** + * @brief Get the number of types. + * @return The number of types. + **/ + int numb_types() const { + assert(inited); + return ntypes; + }; + /** + * @brief Get the number of types with spin. + * @return The number of types with spin. + **/ + int numb_types_spin() const { + assert(inited); + return ntypes_spin; + }; + /** + * @brief Get the dimension of the frame parameter. + * @return The dimension of the frame parameter. + **/ + int dim_fparam() const { + assert(inited); + return dfparam; + }; + /** + * @brief Get the dimension of the atomic parameter. + * @return The dimension of the atomic parameter. + **/ + int dim_aparam() const { + assert(inited); + return daparam; + }; + /** + * @brief Get the type map (element name of the atom types) of this model. + * @param[out] type_map The type map of this model. + **/ + void get_type_map(std::string& type_map); + + /** + * @brief Get whether the atom dimension of aparam is nall instead of fparam. + * @param[out] aparam_nall whether the atom dimension of aparam is nall + *instead of fparam. + **/ + bool is_aparam_nall() const { + assert(inited); + return aparam_nall; + }; + + // forward to template class + void computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + void computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + void computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const int nghost, + const InputNlist& inlist, + const int& ago, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + void computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const int nghost, + const InputNlist& inlist, + const int& ago, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + void computew_mixed_type( + std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const int& nframes, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + void computew_mixed_type( + std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const int& nframes, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam = std::vector(), + const std::vector& aparam = std::vector()); + + private: + int num_intra_nthreads, num_inter_nthreads; + bool inited; + int ntypes; + int ntypes_spin; + int dfparam; + int daparam; + bool aparam_nall; + // copy neighbor list info from host + torch::jit::script::Module module; + double rcut; + NeighborListDataPT nlist_data; + int max_num_neighbors; + int gpu_id; + bool gpu_enabled; + at::Tensor firstneigh_tensor; +}; + +} // namespace deepmd diff --git a/source/api_cc/include/commonPT.h b/source/api_cc/include/commonPT.h new file mode 100644 index 0000000000..57ffd5b295 --- /dev/null +++ b/source/api_cc/include/commonPT.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#include + +#include +#include +#include +#include + +#include "neighbor_list.h" +namespace deepmd { +struct NeighborListDataPT { + /// Array stores the core region atom's index + std::vector ilist; + /// Array stores the core region atom's neighbor index + std::vector jlist; + /// Array stores the number of neighbors of core region atoms + std::vector numneigh; + /// Array stores the the location of the first neighbor of core region atoms + std::vector firstneigh; + + public: + void copy_from_nlist(const InputNlist& inlist, int& max_num_neighbors); +}; +} // namespace deepmd diff --git a/source/api_cc/src/DeepPot.cc b/source/api_cc/src/DeepPot.cc index c598549844..442e2d90cc 100644 --- a/source/api_cc/src/DeepPot.cc +++ b/source/api_cc/src/DeepPot.cc @@ -10,6 +10,9 @@ #ifdef BUILD_TENSORFLOW #include "DeepPotTF.h" #endif +#ifdef BUILD_PYTORCH +#include "DeepPotPT.h" +#endif #include "device.h" using namespace deepmd; @@ -34,8 +37,14 @@ void DeepPot::init(const std::string& model, << std::endl; return; } - // TODO: To implement detect_backend - DPBackend backend = deepmd::DPBackend::TensorFlow; + DPBackend backend; + if (model.length() >= 4 && model.substr(model.length() - 4) == ".pth") { + backend = deepmd::DPBackend::PyTorch; + } else if (model.length() >= 3 && model.substr(model.length() - 3) == ".pb") { + backend = deepmd::DPBackend::TensorFlow; + } else { + throw deepmd::deepmd_exception("Unsupported model file format"); + } if (deepmd::DPBackend::TensorFlow == backend) { #ifdef BUILD_TENSORFLOW dp = std::make_shared(model, gpu_rank, file_content); @@ -43,7 +52,11 @@ void DeepPot::init(const std::string& model, throw deepmd::deepmd_exception("TensorFlow backend is not built"); #endif } else if (deepmd::DPBackend::PyTorch == backend) { - throw deepmd::deepmd_exception("PyTorch backend is not supported yet"); +#ifdef BUILD_PYTORCH + dp = std::make_shared(model, gpu_rank, file_content); +#else + throw deepmd::deepmd_exception("PyTorch backend is not built"); +#endif } else if (deepmd::DPBackend::Paddle == backend) { throw deepmd::deepmd_exception("PaddlePaddle backend is not supported yet"); } else { diff --git a/source/api_cc/src/DeepPotPT.cc b/source/api_cc/src/DeepPotPT.cc index c94fb4247b..f05e27b9b2 100644 --- a/source/api_cc/src/DeepPotPT.cc +++ b/source/api_cc/src/DeepPotPT.cc @@ -1,8 +1,358 @@ // SPDX-License-Identifier: LGPL-3.0-or-later #ifdef BUILD_PYTORCH -#include +#include "DeepPotPT.h" -void test_function_please_remove_after_torch_is_actually_used() { - torch::Tensor tensor = torch::rand({2, 3}); +#include "common.h" +using namespace deepmd; +DeepPotPT::DeepPotPT() : inited(false) {} +DeepPotPT::DeepPotPT(const std::string& model, + const int& gpu_rank, + const std::string& file_content) + : inited(false) { + try { + init(model, gpu_rank, file_content); + } catch (...) { + // Clean up and rethrow, as the destructor will not be called + throw; + } +} +void DeepPotPT::init(const std::string& model, + const int& gpu_rank, + const std::string& file_content) { + if (inited) { + std::cerr << "WARNING: deepmd-kit should not be initialized twice, do " + "nothing at the second call of initializer" + << std::endl; + return; + } + gpu_id = gpu_rank; + torch::Device device(torch::kCUDA, gpu_rank); + gpu_enabled = torch::cuda::is_available(); + if (!gpu_enabled) { + device = torch::Device(torch::kCPU); + std::cout << "load model from: " << model << " to cpu " << gpu_rank + << std::endl; + } else { + std::cout << "load model from: " << model << " to gpu " << gpu_rank + << std::endl; + } + module = torch::jit::load(model, device); + + torch::jit::FusionStrategy strategy; + strategy = {{torch::jit::FusionBehavior::DYNAMIC, 10}}; + torch::jit::setFusionStrategy(strategy); + + get_env_nthreads(num_intra_nthreads, + num_inter_nthreads); // need to be fixed as + // DP_INTRA_OP_PARALLELISM_THREADS + if (num_inter_nthreads) { + try { + at::set_num_interop_threads(num_inter_nthreads); + } catch (...) { + } + } + if (num_intra_nthreads) { + try { + at::set_num_threads(num_intra_nthreads); + } catch (...) { + } + } + + auto rcut_ = module.run_method("get_rcut").toDouble(); + rcut = static_cast(rcut_); + ntypes = 0; + ntypes_spin = 0; + dfparam = 0; + daparam = 0; + aparam_nall = false; + inited = true; +} +DeepPotPT::~DeepPotPT() {} + +template +void DeepPotPT::compute(ENERGYVTYPE& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const InputNlist& lmp_list, + const int& ago) { + torch::Device device(torch::kCUDA, gpu_id); + if (!gpu_enabled) { + device = torch::Device(torch::kCPU); + } + std::vector coord_wrapped = coord; + int natoms = atype.size(); + auto options = torch::TensorOptions().dtype(torch::kFloat64); + torch::ScalarType floatType = torch::kFloat64; + if (std::is_same_v) { + options = torch::TensorOptions().dtype(torch::kFloat32); + floatType = torch::kFloat32; + } + auto int_options = torch::TensorOptions().dtype(torch::kInt64); + auto int32_options = torch::TensorOptions().dtype(torch::kInt32); + at::Tensor coord_wrapped_Tensor = + torch::from_blob(coord_wrapped.data(), {1, natoms, 3}, options) + .to(device); + std::vector atype_64(atype.begin(), atype.end()); + at::Tensor atype_Tensor = + torch::from_blob(atype_64.data(), {1, natoms}, int_options).to(device); + if (ago == 0) { + nlist_data.copy_from_nlist(lmp_list, max_num_neighbors); + } + at::Tensor firstneigh = + torch::from_blob(nlist_data.jlist.data(), + {1, lmp_list.inum, max_num_neighbors}, int32_options); + firstneigh_tensor = firstneigh.to(torch::kInt64).to(device); + bool do_atom_virial_tensor = true; + c10::optional optional_tensor; + c10::Dict outputs = + module + .run_method("forward_lower", coord_wrapped_Tensor, atype_Tensor, + firstneigh_tensor, optional_tensor, optional_tensor, + optional_tensor, do_atom_virial_tensor) + .toGenericDict(); + c10::IValue energy_ = outputs.at("energy"); + c10::IValue force_ = outputs.at("extended_force"); + c10::IValue virial_ = outputs.at("virial"); + c10::IValue atom_virial_ = outputs.at("extended_virial"); + c10::IValue atom_energy_ = outputs.at("atom_energy"); + torch::Tensor flat_energy_ = energy_.toTensor().view({-1}); + torch::Tensor cpu_energy_ = flat_energy_.to(torch::kCPU); + ener.assign(cpu_energy_.data_ptr(), + cpu_energy_.data_ptr() + cpu_energy_.numel()); + torch::Tensor flat_atom_energy_ = + atom_energy_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_atom_energy_ = flat_atom_energy_.to(torch::kCPU); + atom_energy.resize(natoms, 0.0); // resize to nall to be consistenet with TF. + atom_energy.assign( + cpu_atom_energy_.data_ptr(), + cpu_atom_energy_.data_ptr() + cpu_atom_energy_.numel()); + torch::Tensor flat_force_ = force_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_force_ = flat_force_.to(torch::kCPU); + force.assign(cpu_force_.data_ptr(), + cpu_force_.data_ptr() + cpu_force_.numel()); + torch::Tensor flat_virial_ = virial_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_virial_ = flat_virial_.to(torch::kCPU); + virial.assign(cpu_virial_.data_ptr(), + cpu_virial_.data_ptr() + cpu_virial_.numel()); + torch::Tensor flat_atom_virial_ = + atom_virial_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_atom_virial_ = flat_atom_virial_.to(torch::kCPU); + atom_virial.assign( + cpu_atom_virial_.data_ptr(), + cpu_atom_virial_.data_ptr() + cpu_atom_virial_.numel()); +} +template void DeepPotPT::compute>( + std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const InputNlist& lmp_list, + const int& ago); +template void DeepPotPT::compute>( + std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const InputNlist& lmp_list, + const int& ago); +template +void DeepPotPT::compute(ENERGYVTYPE& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box) { + torch::Device device(torch::kCUDA, gpu_id); + if (!gpu_enabled) { + device = torch::Device(torch::kCPU); + } + std::vector coord_wrapped = coord; + int natoms = atype.size(); + auto options = torch::TensorOptions().dtype(torch::kFloat64); + torch::ScalarType floatType = torch::kFloat64; + if (std::is_same_v) { + options = torch::TensorOptions().dtype(torch::kFloat32); + floatType = torch::kFloat32; + } + auto int_options = torch::TensorOptions().dtype(torch::kInt64); + std::vector inputs; + at::Tensor coord_wrapped_Tensor = + torch::from_blob(coord_wrapped.data(), {1, natoms, 3}, options) + .to(device); + inputs.push_back(coord_wrapped_Tensor); + std::vector atype_64(atype.begin(), atype.end()); + at::Tensor atype_Tensor = + torch::from_blob(atype_64.data(), {1, natoms}, int_options).to(device); + inputs.push_back(atype_Tensor); + c10::optional box_Tensor; + if (!box.empty()) { + box_Tensor = + torch::from_blob(const_cast(box.data()), {1, 9}, options) + .to(device); + } + inputs.push_back(box_Tensor); + c10::optional fparam_tensor; + inputs.push_back(fparam_tensor); + c10::optional aparam_tensor; + inputs.push_back(aparam_tensor); + bool do_atom_virial_tensor = true; + inputs.push_back(do_atom_virial_tensor); + c10::Dict outputs = + module.forward(inputs).toGenericDict(); + c10::IValue energy_ = outputs.at("energy"); + c10::IValue force_ = outputs.at("force"); + c10::IValue virial_ = outputs.at("virial"); + c10::IValue atom_virial_ = outputs.at("atom_virial"); + c10::IValue atom_energy_ = outputs.at("atom_energy"); + torch::Tensor flat_energy_ = energy_.toTensor().view({-1}); + torch::Tensor cpu_energy_ = flat_energy_.to(torch::kCPU); + ener.assign(cpu_energy_.data_ptr(), + cpu_energy_.data_ptr() + cpu_energy_.numel()); + torch::Tensor flat_atom_energy_ = + atom_energy_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_atom_energy_ = flat_atom_energy_.to(torch::kCPU); + atom_energy.assign( + cpu_atom_energy_.data_ptr(), + cpu_atom_energy_.data_ptr() + cpu_atom_energy_.numel()); + torch::Tensor flat_force_ = force_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_force_ = flat_force_.to(torch::kCPU); + force.assign(cpu_force_.data_ptr(), + cpu_force_.data_ptr() + cpu_force_.numel()); + torch::Tensor flat_virial_ = virial_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_virial_ = flat_virial_.to(torch::kCPU); + virial.assign(cpu_virial_.data_ptr(), + cpu_virial_.data_ptr() + cpu_virial_.numel()); + torch::Tensor flat_atom_virial_ = + atom_virial_.toTensor().view({-1}).to(floatType); + torch::Tensor cpu_atom_virial_ = flat_atom_virial_.to(torch::kCPU); + atom_virial.assign( + cpu_atom_virial_.data_ptr(), + cpu_atom_virial_.data_ptr() + cpu_atom_virial_.numel()); +} + +template void DeepPotPT::compute>( + std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box); +template void DeepPotPT::compute>( + std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box); +void DeepPotPT::get_type_map(std::string& type_map) { + auto ret = module.run_method("get_type_map").toList(); + for (const torch::IValue& element : ret) { + type_map += torch::str(element); // Convert each element to a string + type_map += " "; // Add a space between elements + } +} + +// forward to template method +void DeepPotPT::computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam, + const std::vector& aparam) { + compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box); +} +void DeepPotPT::computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam, + const std::vector& aparam) { + compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box); +} +void DeepPotPT::computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const int nghost, + const InputNlist& inlist, + const int& ago, + const std::vector& fparam, + const std::vector& aparam) { + // TODO: atomic compute unsupported + compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box, + inlist, ago); +} +void DeepPotPT::computew(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const int nghost, + const InputNlist& inlist, + const int& ago, + const std::vector& fparam, + const std::vector& aparam) { + compute(ener, force, virial, atom_energy, atom_virial, coord, atype, box, + inlist, ago); +} +void DeepPotPT::computew_mixed_type(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const int& nframes, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam, + const std::vector& aparam) { + throw deepmd::deepmd_exception("computew_mixed_type is not implemented"); +} +void DeepPotPT::computew_mixed_type(std::vector& ener, + std::vector& force, + std::vector& virial, + std::vector& atom_energy, + std::vector& atom_virial, + const int& nframes, + const std::vector& coord, + const std::vector& atype, + const std::vector& box, + const std::vector& fparam, + const std::vector& aparam) { + throw deepmd::deepmd_exception("computew_mixed_type is not implemented"); } #endif diff --git a/source/api_cc/src/commonPT.cc b/source/api_cc/src/commonPT.cc new file mode 100644 index 0000000000..4ed3b21fe8 --- /dev/null +++ b/source/api_cc/src/commonPT.cc @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#ifdef BUILD_PYTORCH +#include "commonPT.h" +using namespace deepmd; +void NeighborListDataPT::copy_from_nlist(const InputNlist& inlist, + int& max_num_neighbors) { + int inum = inlist.inum; + ilist.resize(inum); + numneigh.resize(inum); + memcpy(&ilist[0], inlist.ilist, inum * sizeof(int)); + int* max_element = std::max_element(inlist.numneigh, inlist.numneigh + inum); + max_num_neighbors = *max_element; + unsigned long nlist_size = (unsigned long)inum * max_num_neighbors; + jlist.resize(nlist_size); + jlist.assign(nlist_size, -1); + for (int ii = 0; ii < inum; ++ii) { + int jnum = inlist.numneigh[ii]; + numneigh[ii] = inlist.numneigh[ii]; + memcpy(&jlist[(unsigned long)ii * max_num_neighbors], inlist.firstneigh[ii], + jnum * sizeof(int)); + } +} +#endif diff --git a/source/api_cc/tests/test_deeppot_pt.cc b/source/api_cc/tests/test_deeppot_pt.cc new file mode 100644 index 0000000000..e0e90ac75c --- /dev/null +++ b/source/api_cc/tests/test_deeppot_pt.cc @@ -0,0 +1,625 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "DeepPot.h" +#include "neighbor_list.h" +#include "test_utils.h" + +template +class TestInferDeepPotAPt : public ::testing::Test { + protected: + std::vector coord = {12.83, 2.56, 2.18, 12.09, 2.87, 2.74, + 00.25, 3.32, 1.68, 3.36, 3.00, 1.81, + 3.51, 2.51, 2.60, 4.27, 3.22, 1.56}; + std::vector atype = {0, 1, 1, 0, 1, 1}; + std::vector box = {13., 0., 0., 0., 13., 0., 0., 0., 13.}; + std::vector expected_e = { + + -93.016873944029, -185.923296645958, -185.927096544970, + -93.019371018039, -185.926179995548, -185.924351901852}; + std::vector expected_f = { + + 0.006277522211, -0.001117962774, 0.000618580445, 0.009928999655, + 0.003026035654, -0.006941982227, 0.000667853212, -0.002449963843, + 0.006506463508, -0.007284129115, 0.000530662205, -0.000028806821, + 0.000068097781, 0.006121331983, -0.009019754602, -0.009658343745, + -0.006110103225, 0.008865499697}; + std::vector expected_v = { + -0.000155238009, 0.000116605516, -0.007869862476, 0.000465578340, + 0.008182547185, -0.002398713212, -0.008112887338, -0.002423738425, + 0.007210716605, -0.019203504012, 0.001724938709, 0.009909211091, + 0.001153857542, -0.001600015103, -0.000560024090, 0.010727836276, + -0.001034836404, -0.007973454377, -0.021517399106, -0.004064359664, + 0.004866398692, -0.003360038617, -0.007241406162, 0.005920941051, + 0.004899151657, 0.006290788591, -0.006478820311, 0.001921504710, + 0.001313470921, -0.000304091236, 0.001684345981, 0.004124109256, + -0.006396084465, -0.000701095618, -0.006356507032, 0.009818550859, + -0.015230664587, -0.000110244376, 0.000690319396, 0.000045953023, + -0.005726548770, 0.008769818495, -0.000572380210, 0.008860603423, + -0.013819348050, -0.021227082558, -0.004977781343, 0.006646239696, + -0.005987066507, -0.002767831232, 0.003746502525, 0.007697590397, + 0.003746130152, -0.005172634748}; + int natoms; + double expected_tot_e; + std::vector expected_tot_v; + + deepmd::DeepPot dp; + + void SetUp() override { + std::string file_name = "../../tests/infer/deeppot_sea.pth"; + + dp.init(file_name); + + natoms = expected_e.size(); + EXPECT_EQ(natoms * 3, expected_f.size()); + EXPECT_EQ(natoms * 9, expected_v.size()); + expected_tot_e = 0.; + expected_tot_v.resize(9); + std::fill(expected_tot_v.begin(), expected_tot_v.end(), 0.); + for (int ii = 0; ii < natoms; ++ii) { + expected_tot_e += expected_e[ii]; + } + for (int ii = 0; ii < natoms; ++ii) { + for (int dd = 0; dd < 9; ++dd) { + expected_tot_v[dd] += expected_v[ii * 9 + dd]; + } + } + }; + + void TearDown() override { remove("deeppot.pb"); }; +}; + +TYPED_TEST_SUITE(TestInferDeepPotAPt, ValueTypes); + +TYPED_TEST(TestInferDeepPotAPt, cpu_build_nlist) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + double ener; + std::vector force, virial; + dp.compute(ener, force, virial, coord, atype, box); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAPt, cpu_build_nlist_numfv) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + class MyModel : public EnergyModelTest { + deepmd::DeepPot& mydp; + const std::vector atype; + + public: + MyModel(deepmd::DeepPot& dp_, const std::vector& atype_) + : mydp(dp_), atype(atype_){}; + virtual void compute(double& ener, + std::vector& force, + std::vector& virial, + const std::vector& coord, + const std::vector& box) { + mydp.compute(ener, force, virial, coord, atype, box); + } + }; + MyModel model(dp, atype); + model.test_f(coord, box); + model.test_v(coord, box); + std::vector box_(box); + box_[1] -= 0.4; + model.test_f(coord, box_); + model.test_v(coord, box_); + box_[2] += 0.5; + model.test_f(coord, box_); + model.test_v(coord, box_); + box_[4] += 0.2; + model.test_f(coord, box_); + model.test_v(coord, box_); + box_[3] -= 0.3; + model.test_f(coord, box_); + model.test_v(coord, box_); + box_[6] -= 0.7; + model.test_f(coord, box_); + model.test_v(coord, box_); + box_[7] += 0.6; + model.test_f(coord, box_); + model.test_v(coord, box_); +} + +TYPED_TEST(TestInferDeepPotAPt, cpu_build_nlist_atomic) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + double ener; + std::vector force, virial, atom_ener, atom_vir; + dp.compute(ener, force, virial, atom_ener, atom_vir, coord, atype, box); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + EXPECT_EQ(atom_ener.size(), natoms); + EXPECT_EQ(atom_vir.size(), natoms * 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + for (int ii = 0; ii < natoms; ++ii) { + EXPECT_LT(fabs(atom_ener[ii] - expected_e[ii]), EPSILON); + } + for (int ii = 0; ii < natoms * 9; ++ii) { + EXPECT_LT(fabs(atom_vir[ii] - expected_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAPt, cpu_lmp_nlist) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + float rc = dp.cutoff(); + int nloc = coord.size() / 3; + std::vector coord_cpy; + std::vector atype_cpy, mapping; + std::vector > nlist_data; + _build_nlist(nlist_data, coord_cpy, atype_cpy, mapping, coord, + atype, box, rc); + int nall = coord_cpy.size() / 3; + std::vector ilist(nloc), numneigh(nloc); + std::vector firstneigh(nloc); + deepmd::InputNlist inlist(nloc, &ilist[0], &numneigh[0], &firstneigh[0]); + convert_nlist(inlist, nlist_data); + + double ener; + std::vector force_, virial; + dp.compute(ener, force_, virial, coord_cpy, atype_cpy, box, nall - nloc, + inlist, 0); + std::vector force; + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + + ener = 0.; + std::fill(force_.begin(), force_.end(), 0.0); + std::fill(virial.begin(), virial.end(), 0.0); + dp.compute(ener, force_, virial, coord_cpy, atype_cpy, box, nall - nloc, + inlist, 1); + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAPt, cpu_lmp_nlist_atomic) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + float rc = dp.cutoff(); + int nloc = coord.size() / 3; + std::vector coord_cpy; + std::vector atype_cpy, mapping; + std::vector > nlist_data; + _build_nlist(nlist_data, coord_cpy, atype_cpy, mapping, coord, + atype, box, rc); + int nall = coord_cpy.size() / 3; + std::vector ilist(nloc), numneigh(nloc); + std::vector firstneigh(nloc); + deepmd::InputNlist inlist(nloc, &ilist[0], &numneigh[0], &firstneigh[0]); + convert_nlist(inlist, nlist_data); + double ener; + std::vector force_, atom_ener_, atom_vir_, virial; + std::vector force, atom_ener, atom_vir; + dp.compute(ener, force_, virial, atom_ener_, atom_vir_, coord_cpy, atype_cpy, + box, nall - nloc, inlist, 0); + _fold_back(force, force_, mapping, nloc, nall, 3); + _fold_back(atom_ener, atom_ener_, mapping, nloc, nall, 1); + _fold_back(atom_vir, atom_vir_, mapping, nloc, nall, 9); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + EXPECT_EQ(atom_ener.size(), natoms); + EXPECT_EQ(atom_vir.size(), natoms * 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + for (int ii = 0; ii < natoms; ++ii) { + EXPECT_LT(fabs(atom_ener[ii] - expected_e[ii]), EPSILON); + } + for (int ii = 0; ii < natoms * 9; ++ii) { + EXPECT_LT(fabs(atom_vir[ii] - expected_v[ii]), EPSILON); + } + + ener = 0.; + std::fill(force_.begin(), force_.end(), 0.0); + std::fill(virial.begin(), virial.end(), 0.0); + std::fill(atom_ener_.begin(), atom_ener_.end(), 0.0); + std::fill(atom_vir_.begin(), atom_vir_.end(), 0.0); + dp.compute(ener, force_, virial, atom_ener_, atom_vir_, coord_cpy, atype_cpy, + box, nall - nloc, inlist, 1); + _fold_back(force, force_, mapping, nloc, nall, 3); + _fold_back(atom_ener, atom_ener_, mapping, nloc, nall, 1); + _fold_back(atom_vir, atom_vir_, mapping, nloc, nall, 9); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + EXPECT_EQ(atom_ener.size(), natoms); + EXPECT_EQ(atom_vir.size(), natoms * 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + for (int ii = 0; ii < natoms; ++ii) { + EXPECT_LT(fabs(atom_ener[ii] - expected_e[ii]), EPSILON); + } + for (int ii = 0; ii < natoms * 9; ++ii) { + EXPECT_LT(fabs(atom_vir[ii] - expected_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAPt, cpu_lmp_nlist_2rc) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + float rc = dp.cutoff(); + int nloc = coord.size() / 3; + std::vector coord_cpy; + std::vector atype_cpy, mapping; + std::vector > nlist_data; + _build_nlist(nlist_data, coord_cpy, atype_cpy, mapping, coord, + atype, box, rc * 2); + int nall = coord_cpy.size() / 3; + std::vector ilist(nloc), numneigh(nloc); + std::vector firstneigh(nloc); + deepmd::InputNlist inlist(nloc, &ilist[0], &numneigh[0], &firstneigh[0]); + convert_nlist(inlist, nlist_data); + + double ener; + std::vector force_(nall * 3, 0.0), virial(9, 0.0); + dp.compute(ener, force_, virial, coord_cpy, atype_cpy, box, nall - nloc, + inlist, 0); + std::vector force; + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + + ener = 0.; + std::fill(force_.begin(), force_.end(), 0.0); + std::fill(virial.begin(), virial.end(), 0.0); + dp.compute(ener, force_, virial, coord_cpy, atype_cpy, box, nall - nloc, + inlist, 1); + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAPt, cpu_lmp_nlist_type_sel) { + GTEST_SKIP() << "Skipping this test for unsupported"; + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + float rc = dp.cutoff(); + + // add vir atoms + int nvir = 2; + std::vector coord_vir(nvir * 3); + std::vector atype_vir(nvir, 2); + for (int ii = 0; ii < nvir; ++ii) { + coord_vir[ii] = coord[ii]; + } + coord.insert(coord.begin(), coord_vir.begin(), coord_vir.end()); + atype.insert(atype.begin(), atype_vir.begin(), atype_vir.end()); + natoms += nvir; + std::vector expected_f_vir(nvir * 3, 0.0); + expected_f.insert(expected_f.begin(), expected_f_vir.begin(), + expected_f_vir.end()); + + // build nlist + int nloc = coord.size() / 3; + std::vector coord_cpy; + std::vector atype_cpy, mapping; + std::vector > nlist_data; + _build_nlist(nlist_data, coord_cpy, atype_cpy, mapping, coord, + atype, box, rc); + int nall = coord_cpy.size() / 3; + std::vector ilist(nloc), numneigh(nloc); + std::vector firstneigh(nloc); + deepmd::InputNlist inlist(nloc, &ilist[0], &numneigh[0], &firstneigh[0]); + convert_nlist(inlist, nlist_data); + + // dp compute + double ener; + std::vector force_(nall * 3, 0.0), virial(9, 0.0); + dp.compute(ener, force_, virial, coord_cpy, atype_cpy, box, nall - nloc, + inlist, 0); + // fold back + std::vector force; + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAPt, cpu_lmp_nlist_type_sel_atomic) { + GTEST_SKIP() << "Skipping this test for unsupported"; + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + float rc = dp.cutoff(); + + // add vir atoms + int nvir = 2; + std::vector coord_vir(nvir * 3); + std::vector atype_vir(nvir, 2); + for (int ii = 0; ii < nvir; ++ii) { + coord_vir[ii] = coord[ii]; + } + coord.insert(coord.begin(), coord_vir.begin(), coord_vir.end()); + atype.insert(atype.begin(), atype_vir.begin(), atype_vir.end()); + natoms += nvir; + std::vector expected_f_vir(nvir * 3, 0.0); + expected_f.insert(expected_f.begin(), expected_f_vir.begin(), + expected_f_vir.end()); + + // build nlist + int nloc = coord.size() / 3; + std::vector coord_cpy; + std::vector atype_cpy, mapping; + std::vector > nlist_data; + _build_nlist(nlist_data, coord_cpy, atype_cpy, mapping, coord, + atype, box, rc); + int nall = coord_cpy.size() / 3; + std::vector ilist(nloc), numneigh(nloc); + std::vector firstneigh(nloc); + deepmd::InputNlist inlist(nloc, &ilist[0], &numneigh[0], &firstneigh[0]); + convert_nlist(inlist, nlist_data); + + // dp compute + double ener; + std::vector force_(nall * 3, 0.0), virial(9, 0.0), atomic_energy, + atomic_virial; + dp.compute(ener, force_, virial, atomic_energy, atomic_virial, coord_cpy, + atype_cpy, box, nall - nloc, inlist, 0); + // fold back + std::vector force; + _fold_back(force, force_, mapping, nloc, nall, 3); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotAPt, print_summary) { + deepmd::DeepPot& dp = this->dp; + dp.print_summary(""); +} + +template +class TestInferDeepPotAPtNoPbc : public ::testing::Test { + protected: + std::vector coord = {12.83, 2.56, 2.18, 12.09, 2.87, 2.74, + 00.25, 3.32, 1.68, 3.36, 3.00, 1.81, + 3.51, 2.51, 2.60, 4.27, 3.22, 1.56}; + std::vector atype = {0, 1, 1, 0, 1, 1}; + std::vector box = {}; + std::vector expected_e = {-93.003304908874, -185.915806542480, + -185.928116717624, -93.017934934346, + -185.924393412278, -185.923906740801}; + std::vector expected_f = { + 0.000868182637, -0.000363698132, -0.000657003077, -0.000868182637, + 0.000363698132, 0.000657003077, 0.007932614680, -0.001003609844, + 0.000737731722, -0.003883788858, 0.000686896282, -0.000578400682, + 0.004064895086, 0.006115547962, -0.008747097814, -0.008113720908, + -0.005798834400, 0.008587766774}; + std::vector expected_v = { + 0.007762485364, -0.003251851977, -0.005874313248, -0.003251851977, + 0.001362262315, 0.002460860955, -0.005874313248, 0.002460860955, + 0.004445426242, -0.007120030212, 0.002982715359, 0.005388130971, + 0.002982715359, -0.001249515894, -0.002257190002, 0.005388130971, + -0.002257190002, -0.004077504519, -0.015805863589, 0.001952684835, + -0.001522876482, 0.001796574704, -0.000358803950, 0.000369710813, + -0.001108943040, 0.000332585300, -0.000395481309, 0.008873525623, + 0.001919112114, -0.001486235522, 0.002002929532, 0.004222469272, + -0.006517211126, -0.001656192522, -0.006501210045, 0.010118622295, + -0.006548889778, -0.000465126991, 0.001002876603, 0.000240398734, + -0.005794489784, 0.008940685179, -0.000121727685, 0.008931999051, + -0.013852797563, -0.017962955675, -0.004645050453, 0.006214692837, + -0.005278283465, -0.002662692758, 0.003618275905, 0.007095320684, + 0.003648086464, -0.005023397513}; + int natoms; + double expected_tot_e; + std::vector expected_tot_v; + + deepmd::DeepPot dp; + + void SetUp() override { + std::string file_name = "../../tests/infer/deeppot_sea.pth"; + dp.init(file_name); + + natoms = expected_e.size(); + EXPECT_EQ(natoms * 3, expected_f.size()); + EXPECT_EQ(natoms * 9, expected_v.size()); + expected_tot_e = 0.; + expected_tot_v.resize(9); + std::fill(expected_tot_v.begin(), expected_tot_v.end(), 0.); + for (int ii = 0; ii < natoms; ++ii) { + expected_tot_e += expected_e[ii]; + } + for (int ii = 0; ii < natoms; ++ii) { + for (int dd = 0; dd < 9; ++dd) { + expected_tot_v[dd] += expected_v[ii * 9 + dd]; + } + } + }; + + void TearDown() override { remove("deeppot.pb"); }; +}; + +TYPED_TEST_SUITE(TestInferDeepPotAPtNoPbc, ValueTypes); + +TYPED_TEST(TestInferDeepPotAPtNoPbc, cpu_build_nlist) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + double ener; + std::vector force, virial; + dp.compute(ener, force, virial, coord, atype, box); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} diff --git a/source/install/test_cc_local.sh b/source/install/test_cc_local.sh index 22d22a27f6..73aa74ed90 100755 --- a/source/install/test_cc_local.sh +++ b/source/install/test_cc_local.sh @@ -18,7 +18,15 @@ INSTALL_PREFIX=${SCRIPT_PATH}/../../dp_test BUILD_TMP_DIR=${SCRIPT_PATH}/../build_tests mkdir -p ${BUILD_TMP_DIR} cd ${BUILD_TMP_DIR} -cmake -DINSTALL_TENSORFLOW=FALSE -DUSE_TF_PYTHON_LIBS=TRUE -DCMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} -DBUILD_TESTING:BOOL=TRUE -DLAMMPS_VERSION=stable_2Aug2023_update2 ${CUDA_ARGS} .. +cmake \ + -D ENABLE_TENSORFLOW=TRUE \ + -D ENABLE_PYTORCH=TRUE \ + -D INSTALL_TENSORFLOW=FALSE \ + -D USE_TF_PYTHON_LIBS=TRUE \ + -D CMAKE_INSTALL_PREFIX=${INSTALL_PREFIX} \ + -D BUILD_TESTING:BOOL=TRUE \ + -D LAMMPS_VERSION=stable_2Aug2023_update2 \ + ${CUDA_ARGS} .. cmake --build . -j${NPROC} cmake --install . ctest --output-on-failure diff --git a/source/ipi/tests/test_driver.py b/source/ipi/tests/test_driver.py index 1b2e1dd951..b0fbf53b01 100644 --- a/source/ipi/tests/test_driver.py +++ b/source/ipi/tests/test_driver.py @@ -251,3 +251,108 @@ def test_normalize_coords(self): ) expected_se = np.sum(self.expected_e.reshape([nframes, -1]), axis=1) np.testing.assert_almost_equal(ee.ravel(), expected_se.ravel(), default_places) + + +class TestDPIPIPt(TestDPIPI): + @classmethod + def setUpClass(cls): + cls.model_file = str(tests_path / "infer" / "deeppot_sea.pth") + + def setUp(self): + super().setUp() + + self.box = np.array([13.0, 0.0, 0.0, 0.0, 13.0, 0.0, 0.0, 0.0, 13.0]) + self.expected_e = np.array( + [ + -93.016873944029, + -185.923296645958, + -185.927096544970, + -93.019371018039, + -185.926179995548, + -185.924351901852, + ] + ) + self.expected_f = np.array( + [ + 0.006277522211, + -0.001117962774, + 0.000618580445, + 0.009928999655, + 0.003026035654, + -0.006941982227, + 0.000667853212, + -0.002449963843, + 0.006506463508, + -0.007284129115, + 0.000530662205, + -0.000028806821, + 0.000068097781, + 0.006121331983, + -0.009019754602, + -0.009658343745, + -0.006110103225, + 0.008865499697, + ] + ) + self.expected_v = np.array( + [ + -0.000155238009, + 0.000116605516, + -0.007869862476, + 0.000465578340, + 0.008182547185, + -0.002398713212, + -0.008112887338, + -0.002423738425, + 0.007210716605, + -0.019203504012, + 0.001724938709, + 0.009909211091, + 0.001153857542, + -0.001600015103, + -0.000560024090, + 0.010727836276, + -0.001034836404, + -0.007973454377, + -0.021517399106, + -0.004064359664, + 0.004866398692, + -0.003360038617, + -0.007241406162, + 0.005920941051, + 0.004899151657, + 0.006290788591, + -0.006478820311, + 0.001921504710, + 0.001313470921, + -0.000304091236, + 0.001684345981, + 0.004124109256, + -0.006396084465, + -0.000701095618, + -0.006356507032, + 0.009818550859, + -0.015230664587, + -0.000110244376, + 0.000690319396, + 0.000045953023, + -0.005726548770, + 0.008769818495, + -0.000572380210, + 0.008860603423, + -0.013819348050, + -0.021227082558, + -0.004977781343, + 0.006646239696, + -0.005987066507, + -0.002767831232, + 0.003746502525, + 0.007697590397, + 0.003746130152, + -0.005172634748, + ] + ) + + @classmethod + def tearDownClass(cls): + cls.dp = None diff --git a/source/lmp/tests/test_lammps_pt.py b/source/lmp/tests/test_lammps_pt.py new file mode 100644 index 0000000000..bf1ef97e2b --- /dev/null +++ b/source/lmp/tests/test_lammps_pt.py @@ -0,0 +1,695 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import os +import subprocess as sp +import sys +from pathlib import ( + Path, +) + +import constants +import numpy as np +import pytest +from lammps import ( + PyLammps, +) +from write_lmp_data import ( + write_lmp_data, +) + +pbtxt_file2 = ( + Path(__file__).parent.parent.parent / "tests" / "infer" / "deeppot-1.pbtxt" +) +pb_file = Path(__file__).parent.parent.parent / "tests" / "infer" / "deeppot_sea.pth" +pb_file2 = Path(__file__).parent / "graph2.pb" +system_file = Path(__file__).parent.parent.parent / "tests" +data_file = Path(__file__).parent / "data.lmp" +data_file_si = Path(__file__).parent / "data.si" +data_type_map_file = Path(__file__).parent / "data_type_map.lmp" +md_file = Path(__file__).parent / "md.out" + +# this is as the same as python and c++ tests, test_deeppot_a.py +expected_ae = np.array( + [ + -93.016873944029, + -185.923296645958, + -185.927096544970, + -93.019371018039, + -185.926179995548, + -185.924351901852, + ] +) +expected_e = np.sum(expected_ae) +expected_f = np.array( + [ + 0.006277522211, + -0.001117962774, + 0.000618580445, + 0.009928999655, + 0.003026035654, + -0.006941982227, + 0.000667853212, + -0.002449963843, + 0.006506463508, + -0.007284129115, + 0.000530662205, + -0.000028806821, + 0.000068097781, + 0.006121331983, + -0.009019754602, + -0.009658343745, + -0.006110103225, + 0.008865499697, + ] +).reshape(6, 3) + +expected_f2 = np.array( + [ + [-0.6454949, 1.72457783, 0.18897958], + [1.68936514, -0.36995299, -1.36044464], + [-1.09902692, -1.35487928, 1.17416702], + [1.68426111, -0.50835585, 0.98340415], + [0.05771758, 1.12515818, -1.77561531], + [-1.686822, -0.61654789, 0.78950921], + ] +) + +expected_v = -np.array( + [ + -0.000155238009, + 0.000116605516, + -0.007869862476, + 0.000465578340, + 0.008182547185, + -0.002398713212, + -0.008112887338, + -0.002423738425, + 0.007210716605, + -0.019203504012, + 0.001724938709, + 0.009909211091, + 0.001153857542, + -0.001600015103, + -0.000560024090, + 0.010727836276, + -0.001034836404, + -0.007973454377, + -0.021517399106, + -0.004064359664, + 0.004866398692, + -0.003360038617, + -0.007241406162, + 0.005920941051, + 0.004899151657, + 0.006290788591, + -0.006478820311, + 0.001921504710, + 0.001313470921, + -0.000304091236, + 0.001684345981, + 0.004124109256, + -0.006396084465, + -0.000701095618, + -0.006356507032, + 0.009818550859, + -0.015230664587, + -0.000110244376, + 0.000690319396, + 0.000045953023, + -0.005726548770, + 0.008769818495, + -0.000572380210, + 0.008860603423, + -0.013819348050, + -0.021227082558, + -0.004977781343, + 0.006646239696, + -0.005987066507, + -0.002767831232, + 0.003746502525, + 0.007697590397, + 0.003746130152, + -0.005172634748, + ] +).reshape(6, 9) +expected_v2 = -np.array( + [ + [ + -0.70008436, + -0.06399891, + 0.63678391, + -0.07642171, + -0.70580035, + 0.20506145, + 0.64098364, + 0.20305781, + -0.57906794, + ], + [ + -0.6372635, + 0.14315552, + 0.51952246, + 0.04604049, + -0.06003681, + -0.02688702, + 0.54489318, + -0.10951559, + -0.43730539, + ], + [ + -0.25090748, + -0.37466262, + 0.34085833, + -0.26690852, + -0.37676917, + 0.29080825, + 0.31600481, + 0.37558276, + -0.33251064, + ], + [ + -0.80195614, + -0.10273138, + 0.06935364, + -0.10429256, + -0.29693811, + 0.45643496, + 0.07247872, + 0.45604679, + -0.71048816, + ], + [ + -0.03840668, + -0.07680205, + 0.10940472, + -0.02374189, + -0.27610266, + 0.4336071, + 0.02465248, + 0.4290638, + -0.67496763, + ], + [ + -0.61475065, + -0.21163135, + 0.26652929, + -0.26134659, + -0.11560267, + 0.15415902, + 0.34343952, + 0.1589482, + -0.21370642, + ], + ] +).reshape(6, 9) + +box = np.array([0, 13, 0, 13, 0, 13, 0, 0, 0]) +coord = np.array( + [ + [12.83, 2.56, 2.18], + [12.09, 2.87, 2.74], + [0.25, 3.32, 1.68], + [3.36, 3.00, 1.81], + [3.51, 2.51, 2.60], + [4.27, 3.22, 1.56], + ] +) +type_OH = np.array([1, 2, 2, 1, 2, 2]) +type_HO = np.array([2, 1, 1, 2, 1, 1]) + + +sp.check_output( + "{} -m deepmd convert-from pbtxt -i {} -o {}".format( + sys.executable, + pbtxt_file2.resolve(), + pb_file2.resolve(), + ).split() +) + + +def setup_module(): + write_lmp_data(box, coord, type_OH, data_file) + write_lmp_data(box, coord, type_HO, data_type_map_file) + write_lmp_data( + box * constants.dist_metal2si, + coord * constants.dist_metal2si, + type_OH, + data_file_si, + ) + + +def teardown_module(): + os.remove(data_file) + os.remove(data_type_map_file) + + +def _lammps(data_file, units="metal") -> PyLammps: + lammps = PyLammps() + lammps.units(units) + lammps.boundary("p p p") + lammps.atom_style("atomic") + if units == "metal" or units == "real": + lammps.neighbor("2.0 bin") + elif units == "si": + lammps.neighbor("2.0e-10 bin") + else: + raise ValueError("units should be metal, real, or si") + lammps.neigh_modify("every 10 delay 0 check no") + lammps.read_data(data_file.resolve()) + if units == "metal" or units == "real": + lammps.mass("1 16") + lammps.mass("2 2") + elif units == "si": + lammps.mass("1 %.10e" % (16 * constants.mass_metal2si)) + lammps.mass("2 %.10e" % (2 * constants.mass_metal2si)) + else: + raise ValueError("units should be metal, real, or si") + if units == "metal": + lammps.timestep(0.0005) + elif units == "real": + lammps.timestep(0.5) + elif units == "si": + lammps.timestep(5e-16) + else: + raise ValueError("units should be metal, real, or si") + lammps.fix("1 all nve") + return lammps + + +@pytest.fixture +def lammps(): + lmp = _lammps(data_file=data_file) + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_type_map(): + lmp = _lammps(data_file=data_type_map_file) + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_real(): + lmp = _lammps(data_file=data_file, units="real") + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_si(): + lmp = _lammps(data_file=data_file_si, units="si") + yield lmp + lmp.close() + + +def test_pair_deepmd(lammps): + lammps.pair_style(f"deepmd {pb_file.resolve()}") + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + lammps.run(1) + + +def test_pair_deepmd_virial(lammps): + lammps.pair_style(f"deepmd {pb_file.resolve()}") + lammps.pair_coeff("* *") + lammps.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps.variable(f"virial{jj} atom c_virial[{ii+1}]") + lammps.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + idx_map = lammps.lmp.numpy.extract_atom("id") - 1 + for ii in range(9): + assert np.array( + lammps.variables[f"virial{ii}"].value + ) / constants.nktv2p == pytest.approx(expected_v[idx_map, ii]) + + +def test_pair_deepmd_model_devi(lammps): + lammps.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic".format( + pb_file.resolve(), pb_file2.resolve(), md_file.resolve() + ) + ) + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f) + assert md[4] == pytest.approx(np.max(expected_md_f)) + assert md[5] == pytest.approx(np.min(expected_md_f)) + assert md[6] == pytest.approx(np.mean(expected_md_f)) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v)) + assert md[2] == pytest.approx(np.min(expected_md_v)) + assert md[3] == pytest.approx(np.sqrt(np.mean(np.square(expected_md_v)))) + + +def test_pair_deepmd_model_devi_virial(lammps): + lammps.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic".format( + pb_file.resolve(), pb_file2.resolve(), md_file.resolve() + ) + ) + lammps.pair_coeff("* *") + lammps.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps.variable(f"virial{jj} atom c_virial[{ii+1}]") + lammps.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + idx_map = lammps.lmp.numpy.extract_atom("id") - 1 + for ii in range(9): + assert np.array( + lammps.variables[f"virial{ii}"].value + ) / constants.nktv2p == pytest.approx(expected_v[idx_map, ii]) + # load model devi + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f) + assert md[4] == pytest.approx(np.max(expected_md_f)) + assert md[5] == pytest.approx(np.min(expected_md_f)) + assert md[6] == pytest.approx(np.mean(expected_md_f)) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v)) + assert md[2] == pytest.approx(np.min(expected_md_v)) + assert md[3] == pytest.approx(np.sqrt(np.mean(np.square(expected_md_v)))) + + +def test_pair_deepmd_model_devi_atomic_relative(lammps): + relative = 1.0 + lammps.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic relative {}".format( + pb_file.resolve(), pb_file2.resolve(), md_file.resolve(), relative + ) + ) + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + norm = np.linalg.norm(np.mean([expected_f, expected_f2], axis=0), axis=1) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + expected_md_f /= norm + relative + assert md[7:] == pytest.approx(expected_md_f) + assert md[4] == pytest.approx(np.max(expected_md_f)) + assert md[5] == pytest.approx(np.min(expected_md_f)) + assert md[6] == pytest.approx(np.mean(expected_md_f)) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v)) + assert md[2] == pytest.approx(np.min(expected_md_v)) + assert md[3] == pytest.approx(np.sqrt(np.mean(np.square(expected_md_v)))) + + +def test_pair_deepmd_model_devi_atomic_relative_v(lammps): + relative = 1.0 + lammps.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic relative_v {}".format( + pb_file.resolve(), pb_file2.resolve(), md_file.resolve(), relative + ) + ) + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f) + assert md[4] == pytest.approx(np.max(expected_md_f)) + assert md[5] == pytest.approx(np.min(expected_md_f)) + assert md[6] == pytest.approx(np.mean(expected_md_f)) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + norm = ( + np.abs( + np.mean([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) + ) + / 6 + ) + expected_md_v /= norm + relative + assert md[1] == pytest.approx(np.max(expected_md_v)) + assert md[2] == pytest.approx(np.min(expected_md_v)) + assert md[3] == pytest.approx(np.sqrt(np.mean(np.square(expected_md_v)))) + + +def test_pair_deepmd_type_map(lammps_type_map): + lammps_type_map.pair_style(f"deepmd {pb_file.resolve()}") + lammps_type_map.pair_coeff("* * H O") + lammps_type_map.run(0) + assert lammps_type_map.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps_type_map.atoms[ii].force == pytest.approx( + expected_f[lammps_type_map.atoms[ii].id - 1] + ) + lammps_type_map.run(1) + + +def test_pair_deepmd_real(lammps_real): + lammps_real.pair_style(f"deepmd {pb_file.resolve()}") + lammps_real.pair_coeff("* *") + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + lammps_real.run(1) + + +def test_pair_deepmd_virial_real(lammps_real): + lammps_real.pair_style(f"deepmd {pb_file.resolve()}") + lammps_real.pair_coeff("* *") + lammps_real.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps_real.variable(f"virial{jj} atom c_virial[{ii+1}]") + lammps_real.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + idx_map = lammps_real.lmp.numpy.extract_atom("id") - 1 + for ii in range(9): + assert np.array( + lammps_real.variables[f"virial{ii}"].value + ) / constants.nktv2p_real == pytest.approx( + expected_v[idx_map, ii] * constants.ener_metal2real + ) + + +def test_pair_deepmd_model_devi_real(lammps_real): + lammps_real.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic".format( + pb_file.resolve(), pb_file2.resolve(), md_file.resolve() + ) + ) + lammps_real.pair_coeff("* *") + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f * constants.force_metal2real) + assert md[4] == pytest.approx(np.max(expected_md_f) * constants.force_metal2real) + assert md[5] == pytest.approx(np.min(expected_md_f) * constants.force_metal2real) + assert md[6] == pytest.approx(np.mean(expected_md_f) * constants.force_metal2real) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v) * constants.ener_metal2real) + assert md[2] == pytest.approx(np.min(expected_md_v) * constants.ener_metal2real) + assert md[3] == pytest.approx( + np.sqrt(np.mean(np.square(expected_md_v))) * constants.ener_metal2real + ) + + +def test_pair_deepmd_model_devi_virial_real(lammps_real): + lammps_real.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic".format( + pb_file.resolve(), pb_file2.resolve(), md_file.resolve() + ) + ) + lammps_real.pair_coeff("* *") + lammps_real.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps_real.variable(f"virial{jj} atom c_virial[{ii+1}]") + lammps_real.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + idx_map = lammps_real.lmp.numpy.extract_atom("id") - 1 + for ii in range(9): + assert np.array( + lammps_real.variables[f"virial{ii}"].value + ) / constants.nktv2p_real == pytest.approx( + expected_v[idx_map, ii] * constants.ener_metal2real + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f * constants.force_metal2real) + assert md[4] == pytest.approx(np.max(expected_md_f) * constants.force_metal2real) + assert md[5] == pytest.approx(np.min(expected_md_f) * constants.force_metal2real) + assert md[6] == pytest.approx(np.mean(expected_md_f) * constants.force_metal2real) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v) * constants.ener_metal2real) + assert md[2] == pytest.approx(np.min(expected_md_v) * constants.ener_metal2real) + assert md[3] == pytest.approx( + np.sqrt(np.mean(np.square(expected_md_v))) * constants.ener_metal2real + ) + + +def test_pair_deepmd_model_devi_atomic_relative_real(lammps_real): + relative = 1.0 + lammps_real.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic relative {}".format( + pb_file.resolve(), + pb_file2.resolve(), + md_file.resolve(), + relative * constants.force_metal2real, + ) + ) + lammps_real.pair_coeff("* *") + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + norm = np.linalg.norm(np.mean([expected_f, expected_f2], axis=0), axis=1) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + expected_md_f /= norm + relative + assert md[7:] == pytest.approx(expected_md_f * constants.force_metal2real) + assert md[4] == pytest.approx(np.max(expected_md_f) * constants.force_metal2real) + assert md[5] == pytest.approx(np.min(expected_md_f) * constants.force_metal2real) + assert md[6] == pytest.approx(np.mean(expected_md_f) * constants.force_metal2real) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v) * constants.ener_metal2real) + assert md[2] == pytest.approx(np.min(expected_md_v) * constants.ener_metal2real) + assert md[3] == pytest.approx( + np.sqrt(np.mean(np.square(expected_md_v))) * constants.ener_metal2real + ) + + +def test_pair_deepmd_model_devi_atomic_relative_v_real(lammps_real): + relative = 1.0 + lammps_real.pair_style( + "deepmd {} {} out_file {} out_freq 1 atomic relative_v {}".format( + pb_file.resolve(), + pb_file2.resolve(), + md_file.resolve(), + relative * constants.ener_metal2real, + ) + ) + lammps_real.pair_coeff("* *") + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f * constants.force_metal2real) + assert md[4] == pytest.approx(np.max(expected_md_f) * constants.force_metal2real) + assert md[5] == pytest.approx(np.min(expected_md_f) * constants.force_metal2real) + assert md[6] == pytest.approx(np.mean(expected_md_f) * constants.force_metal2real) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + norm = ( + np.abs( + np.mean([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) + ) + / 6 + ) + expected_md_v /= norm + relative + assert md[1] == pytest.approx(np.max(expected_md_v) * constants.ener_metal2real) + assert md[2] == pytest.approx(np.min(expected_md_v) * constants.ener_metal2real) + assert md[3] == pytest.approx( + np.sqrt(np.mean(np.square(expected_md_v))) * constants.ener_metal2real + ) + + +def test_pair_deepmd_si(lammps_si): + lammps_si.pair_style(f"deepmd {pb_file.resolve()}") + lammps_si.pair_coeff("* *") + lammps_si.run(0) + assert lammps_si.eval("pe") == pytest.approx(expected_e * constants.ener_metal2si) + for ii in range(6): + assert lammps_si.atoms[ii].force == pytest.approx( + expected_f[lammps_si.atoms[ii].id - 1] * constants.force_metal2si + ) + lammps_si.run(1) diff --git a/source/tests/infer/deeppot_sea.pth b/source/tests/infer/deeppot_sea.pth new file mode 100644 index 0000000000000000000000000000000000000000..98aaa8a2ad0ccb1f462ff3475f28fea7d28e24fb GIT binary patch literal 123025 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�Q>X0Uo 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�=x9(71$;AXlw+(Q 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 F{vWF2=WGA~ literal 0 HcmV?d00001 From 897fcc5b9b5f6758f25ac912f5acb3ba16121a78 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 28 Feb 2024 03:14:16 -0500 Subject: [PATCH 4/7] pt: support `--init-frz-model` (#3350) Signed-off-by: Jinzhe Zeng --- deepmd/main.py | 2 +- deepmd/pt/entrypoints/main.py | 3 + deepmd/pt/train/training.py | 22 +++++- source/tests/pt/test_init_frz_model.py | 101 +++++++++++++++++++++++++ 4 files changed, 123 insertions(+), 5 deletions(-) create mode 100644 source/tests/pt/test_init_frz_model.py diff --git a/deepmd/main.py b/deepmd/main.py index 4d2d62ed14..df5c99bb2d 100644 --- a/deepmd/main.py +++ b/deepmd/main.py @@ -226,7 +226,7 @@ def main_parser() -> argparse.ArgumentParser: "--init-frz-model", type=str, default=None, - help="(Supported backend: TensorFlow) Initialize the training from the frozen model.", + help="Initialize the training from the frozen model.", ) parser_train_subgroup.add_argument( "-t", diff --git a/deepmd/pt/entrypoints/main.py b/deepmd/pt/entrypoints/main.py index 212a6824e7..a317cea6a9 100644 --- a/deepmd/pt/entrypoints/main.py +++ b/deepmd/pt/entrypoints/main.py @@ -65,6 +65,7 @@ def get_trainer( finetune_model=None, model_branch="", force_load=False, + init_frz_model=None, ): # Initialize DDP local_rank = os.environ.get("LOCAL_RANK") @@ -200,6 +201,7 @@ def prepare_trainer_input_single( finetune_model=finetune_model, force_load=force_load, shared_links=shared_links, + init_frz_model=init_frz_model, ) return trainer @@ -243,6 +245,7 @@ def train(FLAGS): FLAGS.finetune, FLAGS.model_branch, FLAGS.force_load, + FLAGS.init_frz_model, ) trainer.run() diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 5a783e412b..152c69a444 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -75,6 +75,7 @@ def __init__( finetune_model=None, force_load=False, shared_links=None, + init_frz_model=None, ): """Construct a DeePMD trainer. @@ -271,7 +272,7 @@ def get_loss(loss_params, start_lr, _ntypes): self.warmup_steps = training_params.get("warmup_steps", 0) self.gradient_max_norm = training_params.get("gradient_max_norm", 0.0) assert ( - self.num_steps - self.warmup_steps > 0 + self.num_steps - self.warmup_steps > 0 or self.warmup_steps == 0 ), "Warm up steps must be less than total training steps!" if self.multi_task and config.get("learning_rate_dict", None) is not None: self.lr_exp = {} @@ -394,6 +395,9 @@ def get_loss(loss_params, start_lr, _ntypes): ntest=ntest, bias_shift=model_params.get("bias_shift", "delta"), ) + if init_frz_model is not None: + frz_model = torch.jit.load(init_frz_model, map_location=DEVICE) + self.model.load_state_dict(frz_model.state_dict()) # Set trainable params self.wrapper.set_trainable_params() @@ -724,6 +728,15 @@ def log_loss_valid(_task_key="Default"): if ( self.rank == 0 or dist.get_rank() == 0 ): # Handle the case if rank 0 aborted and re-assigned + if self.num_steps == 0: + # when num_steps is 0, the checkpoint is never not saved + self.latest_model = Path(self.save_ckpt + "-0.pt") + self.save_model(self.latest_model, lr=0, step=0) + log.info(f"Saved model to {self.latest_model}") + symlink_prefix_files(self.latest_model.stem, self.save_ckpt) + with open("checkpoint", "w") as f: + f.write(str(self.latest_model)) + if JIT: pth_model_path = ( "frozen_model.pth" # We use .pth to denote the frozen model @@ -759,9 +772,10 @@ def get_data(self, is_train=True, task_key="Default"): batch_data = next(iter(self.training_data)) except StopIteration: # Refresh the status of the dataloader to start from a new epoch - self.training_data = BufferedIterator( - iter(self.training_dataloader) - ) + with torch.device("cpu"): + self.training_data = BufferedIterator( + iter(self.training_dataloader) + ) batch_data = next(iter(self.training_data)) else: try: diff --git a/source/tests/pt/test_init_frz_model.py b/source/tests/pt/test_init_frz_model.py new file mode 100644 index 0000000000..d156eddc41 --- /dev/null +++ b/source/tests/pt/test_init_frz_model.py @@ -0,0 +1,101 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import json +import unittest +from argparse import ( + Namespace, +) +from copy import ( + deepcopy, +) +from pathlib import ( + Path, +) + +import numpy as np + +from deepmd.pt.entrypoints.main import ( + freeze, + get_trainer, +) +from deepmd.pt.infer.deep_eval import ( + DeepPot, +) + + +class TestInitFrzModel(unittest.TestCase): + def setUp(self): + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + config = json.load(f) + config["training"]["numb_steps"] = 1 + config["training"]["save_freq"] = 1 + config["learning_rate"]["start_lr"] = 1.0 + config["training"]["training_data"]["systems"] = [ + str(Path(__file__).parent / "water/data/single") + ] + config["training"]["validation_data"]["systems"] = [ + str(Path(__file__).parent / "water/data/single") + ] + + self.models = [] + for imodel in range(2): + if imodel == 1: + config["training"]["numb_steps"] = 0 + trainer = get_trainer(deepcopy(config), init_frz_model=self.models[-1]) + else: + trainer = get_trainer(deepcopy(config)) + trainer.run() + + frozen_model = f"frozen_model{imodel}.pth" + ns = Namespace( + model="model.pt", + output=frozen_model, + head=None, + ) + freeze(ns) + self.models.append(frozen_model) + + def test_dp_test(self): + dp1 = DeepPot(str(self.models[0])) + dp2 = DeepPot(str(self.models[1])) + cell = np.array( + [ + 5.122106549439247480e00, + 4.016537340154059388e-01, + 6.951654033828678081e-01, + 4.016537340154059388e-01, + 6.112136112297989143e00, + 8.178091365465004481e-01, + 6.951654033828678081e-01, + 8.178091365465004481e-01, + 6.159552512682983760e00, + ] + ).reshape(1, 3, 3) + coord = np.array( + [ + 2.978060152121375648e00, + 3.588469695887098077e00, + 2.792459820604495491e00, + 3.895592322591093115e00, + 2.712091020667753760e00, + 1.366836847133650501e00, + 9.955616170888935690e-01, + 4.121324820711413039e00, + 1.817239061889086571e00, + 3.553661462345699906e00, + 5.313046969500791583e00, + 6.635182659098815883e00, + 6.088601018589653080e00, + 6.575011420004332585e00, + 6.825240650611076099e00, + ] + ).reshape(1, -1, 3) + atype = np.array([0, 0, 0, 1, 1]).reshape(1, -1) + + e1, f1, v1, ae1, av1 = dp1.eval(coord, cell, atype, atomic=True) + e2, f2, v2, ae2, av2 = dp2.eval(coord, cell, atype, atomic=True) + np.testing.assert_allclose(e1, e2, rtol=1e-10, atol=1e-10) + np.testing.assert_allclose(f1, f2, rtol=1e-10, atol=1e-10) + np.testing.assert_allclose(v1, v2, rtol=1e-10, atol=1e-10) + np.testing.assert_allclose(ae1, ae2, rtol=1e-10, atol=1e-10) + np.testing.assert_allclose(av1, av2, rtol=1e-10, atol=1e-10) From 16cd26ca8b4687fc54e32bf226ee2075bad94d64 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Wed, 28 Feb 2024 18:29:12 +0800 Subject: [PATCH 5/7] Fix single-task training&data stat (#3355) --- deepmd/pt/model/descriptor/dpa2.py | 2 +- deepmd/pt/model/model/__init__.py | 9 ++++----- deepmd/pt/model/model/dp_model.py | 5 ++++- deepmd/pt/model/model/model.py | 4 ++-- deepmd/utils/path.py | 1 + examples/water/dpa2/input_torch.json | 8 ++------ examples/water/se_atten/input_torch.json | 2 ++ examples/water/se_e2_a/input_torch.json | 1 + 8 files changed, 17 insertions(+), 15 deletions(-) diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index e693116cf4..b1df56a004 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -304,7 +304,7 @@ def compute_input_stats(self, merged: List[dict], path: Optional[DPPath] = None) } for item in merged ] - descrpt.compute_input_stats(merged_tmp) + descrpt.compute_input_stats(merged_tmp, path) def serialize(self) -> dict: """Serialize the obj to dict.""" diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index 0dc9ae20af..b823a051f5 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -20,7 +20,7 @@ BaseDescriptor, ) from deepmd.pt.model.task import ( - Fitting, + BaseFitting, ) from .dp_model import ( @@ -61,7 +61,7 @@ def get_zbl_model(model_params): fitting_net["out_dim"] = descriptor.get_dim_emb() if "ener" in fitting_net["type"]: fitting_net["return_energy"] = True - fitting = Fitting(**fitting_net) + fitting = BaseFitting(**fitting_net) dp_model = DPAtomicModel(descriptor, fitting, type_map=model_params["type_map"]) # pairtab filepath = model_params["use_srtab"] @@ -97,9 +97,8 @@ def get_model(model_params): fitting_net["out_dim"] = descriptor.get_dim_emb() if "ener" in fitting_net["type"]: fitting_net["return_energy"] = True - fitting = Fitting(**fitting_net) - - model = EnergyModel(descriptor, fitting, type_map=model_params["type_map"]) + fitting = BaseFitting(**fitting_net) + model = DPModel(descriptor, fitting, type_map=model_params["type_map"]) model.model_def_script = json.dumps(model_params) return model diff --git a/deepmd/pt/model/model/dp_model.py b/deepmd/pt/model/model/dp_model.py index 5410f518d1..79c129334a 100644 --- a/deepmd/pt/model/model/dp_model.py +++ b/deepmd/pt/model/model/dp_model.py @@ -10,6 +10,7 @@ ) from deepmd.pt.model.task.ener import ( EnergyFittingNet, + EnergyFittingNetDirect, ) from deepmd.pt.model.task.polarizability import ( PolarFittingNet, @@ -36,7 +37,9 @@ def __new__(cls, descriptor, fitting, *args, **kwargs): # according to the fitting network to decide the type of the model if cls is DPModel: # map fitting to model - if isinstance(fitting, EnergyFittingNet): + if isinstance(fitting, EnergyFittingNet) or isinstance( + fitting, EnergyFittingNetDirect + ): cls = EnergyModel elif isinstance(fitting, DipoleFittingNet): cls = DipoleModel diff --git a/deepmd/pt/model/model/model.py b/deepmd/pt/model/model/model.py index 0f5e27aea9..e32d2f307d 100644 --- a/deepmd/pt/model/model/model.py +++ b/deepmd/pt/model/model/model.py @@ -59,9 +59,9 @@ # in DPAtomicModel (and other classes), but this requires the developer aware # of it when developing it... class BaseModel(make_base_model()): - def __init__(self): + def __init__(self, *args, **kwargs): """Construct a basic model for different tasks.""" - super().__init__() + super().__init__(*args, **kwargs) def compute_or_load_stat( self, diff --git a/deepmd/utils/path.py b/deepmd/utils/path.py index c9a7cd8554..79361b6c23 100644 --- a/deepmd/utils/path.py +++ b/deepmd/utils/path.py @@ -355,6 +355,7 @@ def save_numpy(self, arr: np.ndarray) -> None: if self._name in self._keys: del self.root[self._name] self.root.create_dataset(self._name, data=arr) + self.root.flush() def glob(self, pattern: str) -> List["DPPath"]: """Search path using the glob pattern. diff --git a/examples/water/dpa2/input_torch.json b/examples/water/dpa2/input_torch.json index 9d783b35d5..108e75df62 100644 --- a/examples/water/dpa2/input_torch.json +++ b/examples/water/dpa2/input_torch.json @@ -1,18 +1,13 @@ { "_comment": "that's all", "model": { - "type_embedding": { - "neuron": [ - 8 - ], - "tebd_input_mode": "concat" - }, "type_map": [ "O", "H" ], "descriptor": { "type": "dpa2", + "tebd_dim": 8, "repinit_rcut": 9.0, "repinit_rcut_smth": 8.0, "repinit_nsel": 120, @@ -74,6 +69,7 @@ "_comment": " that's all" }, "training": { + "stat_file": "./dpa2", "training_data": { "systems": [ "../data/data_0", diff --git a/examples/water/se_atten/input_torch.json b/examples/water/se_atten/input_torch.json index 7da3d64164..bc948cc2a0 100644 --- a/examples/water/se_atten/input_torch.json +++ b/examples/water/se_atten/input_torch.json @@ -15,6 +15,7 @@ 50, 100 ], + "tebd_dim": 8, "axis_neuron": 16, "attn": 128, "attn_layer": 2, @@ -59,6 +60,7 @@ "_comment": " that's all" }, "training": { + "stat_file": "./dpa1", "training_data": { "systems": [ "../data/data_0", diff --git a/examples/water/se_e2_a/input_torch.json b/examples/water/se_e2_a/input_torch.json index 053a721a44..c686b49d45 100644 --- a/examples/water/se_e2_a/input_torch.json +++ b/examples/water/se_e2_a/input_torch.json @@ -51,6 +51,7 @@ "_comment": " that's all" }, "training": { + "stat_file": "./se_e2_a", "training_data": { "systems": [ "../data/data_0", From 3ad57daf39eb5d91d62ce8866e6e42643a49d335 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 28 Feb 2024 05:34:34 -0500 Subject: [PATCH 6/7] merge compute_output_stat (#3310) Signed-off-by: Jinzhe Zeng --- deepmd/pt/model/task/dipole.py | 8 -- deepmd/pt/model/task/ener.py | 34 ++++--- deepmd/pt/model/task/fitting.py | 8 -- deepmd/pt/model/task/polarizability.py | 8 -- deepmd/pt/utils/env_mat_stat.py | 2 +- deepmd/pt/utils/stat.py | 21 ----- deepmd/tf/fit/dos.py | 9 +- deepmd/tf/fit/ener.py | 25 +++-- deepmd/tf/fit/polar.py | 6 +- deepmd/utils/data_system.py | 11 ++- deepmd/utils/out_stat.py | 117 +++++++++++++++++++++++ source/tests/common/test_out_stat.py | 124 +++++++++++++++++++++++++ source/tests/pt/test_stat.py | 17 ++-- source/tests/tf/common.py | 11 ++- 14 files changed, 312 insertions(+), 89 deletions(-) create mode 100644 deepmd/utils/out_stat.py create mode 100644 source/tests/common/test_out_stat.py diff --git a/deepmd/pt/model/task/dipole.py b/deepmd/pt/model/task/dipole.py index bff3dd93bc..9df3a5fb32 100644 --- a/deepmd/pt/model/task/dipole.py +++ b/deepmd/pt/model/task/dipole.py @@ -132,14 +132,6 @@ def output_def(self) -> FittingOutputDef: ] ) - @property - def data_stat_key(self): - """ - Get the keys for the data statistic of the fitting. - Return a list of statistic names needed, such as "bias_atom_e". - """ - return [] - def forward( self, descriptor: torch.Tensor, diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 8479111819..ff7ae6f8ec 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -28,8 +28,11 @@ from deepmd.pt.utils.env import ( DEFAULT_PRECISION, ) -from deepmd.pt.utils.stat import ( - compute_output_bias, +from deepmd.pt.utils.utils import ( + to_numpy_array, +) +from deepmd.utils.out_stat import ( + compute_stats_from_redu, ) from deepmd.utils.path import ( DPPath, @@ -135,16 +138,8 @@ def serialize(self) -> dict: data["atom_ener"] = self.atom_ener return data - @property - def data_stat_key(self): - """ - Get the keys for the data statistic of the fitting. - Return a list of statistic names needed, such as "bias_atom_e". - """ - return ["bias_atom_e"] - def compute_output_stats(self, merged, stat_file_path: Optional[DPPath] = None): - energy = [item["energy"] for item in merged] + energy = [item[self.var_name] for item in merged] data_mixed_type = "real_natoms_vec" in merged[0] if data_mixed_type: input_natoms = [item["real_natoms_vec"] for item in merged] @@ -155,7 +150,22 @@ def compute_output_stats(self, merged, stat_file_path: Optional[DPPath] = None): if stat_file_path is not None and stat_file_path.is_file(): bias_atom_e = stat_file_path.load_numpy() else: - bias_atom_e = compute_output_bias(energy, input_natoms, rcond=self.rcond) + # shape: (nframes, ndim) + merged_energy = to_numpy_array(torch.cat(energy)) + # shape: (nframes, ntypes) + merged_natoms = to_numpy_array(torch.cat(input_natoms)[:, 2:]) + if self.atom_ener is not None and len(self.atom_ener) > 0: + assigned_atom_ener = np.array( + [ee if ee is not None else np.nan for ee in self.atom_ener] + ) + else: + assigned_atom_ener = None + bias_atom_e, _ = compute_stats_from_redu( + merged_energy, + merged_natoms, + assigned_bias=assigned_atom_ener, + rcond=self.rcond, + ) if stat_file_path is not None: stat_file_path.save_numpy(bias_atom_e) assert all(x is not None for x in [bias_atom_e]) diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 0c64983f60..20876d9be7 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -92,14 +92,6 @@ def share_params(self, base_class, shared_level, resume=False): else: raise NotImplementedError - @property - def data_stat_key(self): - """ - Get the keys for the data statistic of the fitting. - Return a list of statistic names needed, such as "bias_atom_e". - """ - raise NotImplementedError("data_stat_key is not implemented!") - def change_energy_bias( self, config, model, old_type_map, new_type_map, bias_shift="delta", ntest=10 ): diff --git a/deepmd/pt/model/task/polarizability.py b/deepmd/pt/model/task/polarizability.py index 13b0d56e31..1bc4798c48 100644 --- a/deepmd/pt/model/task/polarizability.py +++ b/deepmd/pt/model/task/polarizability.py @@ -160,14 +160,6 @@ def output_def(self) -> FittingOutputDef: ] ) - @property - def data_stat_key(self): - """ - Get the keys for the data statistic of the fitting. - Return a list of statistic names needed, such as "bias_atom_e". - """ - return [] - def forward( self, descriptor: torch.Tensor, diff --git a/deepmd/pt/utils/env_mat_stat.py b/deepmd/pt/utils/env_mat_stat.py index 70b7228440..cd2943e6a8 100644 --- a/deepmd/pt/utils/env_mat_stat.py +++ b/deepmd/pt/utils/env_mat_stat.py @@ -80,7 +80,7 @@ def iter( Parameters ---------- data : List[Dict[str, torch.Tensor]] - The environment matrix. + The data. Yields ------ diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index 38f71d6994..4c769f019e 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -1,7 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import logging -import numpy as np import torch log = logging.getLogger(__name__) @@ -57,23 +56,3 @@ def make_stat_input(datasets, dataloaders, nbatches): sys_stat[key] = sys_stat_list lst.append(sys_stat) return lst - - -def compute_output_bias(energy, natoms, rcond=None): - """Update output bias for fitting net. - - Args: - - energy: Batched energy with shape [nframes, 1]. - - natoms: Batched atom statisics with shape [self.ntypes+2]. - - Returns - ------- - - energy_coef: Average enery per atom for each element. - """ - for i in range(len(energy)): - energy[i] = energy[i].mean(dim=0, keepdim=True) - natoms[i] = natoms[i].double().mean(dim=0, keepdim=True) - sys_ener = torch.cat(energy).cpu() - sys_tynatom = torch.cat(natoms)[:, 2:].cpu() - energy_coef, _, _, _ = np.linalg.lstsq(sys_tynatom, sys_ener, rcond) - return energy_coef diff --git a/deepmd/tf/fit/dos.py b/deepmd/tf/fit/dos.py index e8681f47ea..0cc5a7df62 100644 --- a/deepmd/tf/fit/dos.py +++ b/deepmd/tf/fit/dos.py @@ -43,6 +43,9 @@ from deepmd.tf.utils.network import ( one_layer_rand_seed_shift, ) +from deepmd.utils.out_stat import ( + compute_stats_from_redu, +) log = logging.getLogger(__name__) @@ -225,8 +228,10 @@ def _compute_output_stats(self, all_stat, rcond=1e-3, mixed_type=False): sys_tynatom = np.reshape(sys_tynatom, [nsys, -1]) sys_tynatom = sys_tynatom[:, 2:] - dos_shift, resd, rank, s_value = np.linalg.lstsq( - sys_tynatom, sys_dos, rcond=rcond + dos_shift, _ = compute_stats_from_redu( + sys_dos, + sys_tynatom, + rcond=rcond, ) return dos_shift diff --git a/deepmd/tf/fit/ener.py b/deepmd/tf/fit/ener.py index 106e10839d..a842df50bd 100644 --- a/deepmd/tf/fit/ener.py +++ b/deepmd/tf/fit/ener.py @@ -53,6 +53,9 @@ from deepmd.tf.utils.spin import ( Spin, ) +from deepmd.utils.out_stat import ( + compute_stats_from_redu, +) from deepmd.utils.version import ( check_version_compatibility, ) @@ -295,21 +298,17 @@ def _compute_output_stats(self, all_stat, rcond=1e-3, mixed_type=False): # In this situation, we directly use these assigned energies instead of computing stats. # This will make the loss decrease quickly assigned_atom_ener = np.array( - [ee for ee in self.atom_ener_v if ee is not None] + [ee if ee is not None else np.nan for ee in self.atom_ener_v] ) - assigned_ener_idx = [ - ii for ii, ee in enumerate(self.atom_ener_v) if ee is not None - ] - # np.dot out size: nframe - sys_ener -= np.dot(sys_tynatom[:, assigned_ener_idx], assigned_atom_ener) - sys_tynatom[:, assigned_ener_idx] = 0.0 - energy_shift, resd, rank, s_value = np.linalg.lstsq( - sys_tynatom, sys_ener, rcond=rcond + else: + assigned_atom_ener = None + energy_shift, _ = compute_stats_from_redu( + sys_ener.reshape(-1, 1), + sys_tynatom, + assigned_bias=assigned_atom_ener, + rcond=rcond, ) - if len(self.atom_ener) > 0: - for ii in assigned_ener_idx: - energy_shift[ii] = self.atom_ener_v[ii] - return energy_shift + return energy_shift.ravel() def compute_input_stats(self, all_stat: dict, protection: float = 1e-2) -> None: """Compute the input statistics. diff --git a/deepmd/tf/fit/polar.py b/deepmd/tf/fit/polar.py index 002082ad2e..7ac31809f3 100644 --- a/deepmd/tf/fit/polar.py +++ b/deepmd/tf/fit/polar.py @@ -151,16 +151,14 @@ def get_out_size(self) -> int: """Get the output size. Should be 9.""" return 9 - def compute_input_stats(self, all_stat, protection=1e-2): - """Compute the input statistics. + def compute_output_stats(self, all_stat): + """Compute the output statistics. Parameters ---------- all_stat Dictionary of inputs. can be prepared by model.make_stat_input - protection - Divided-by-zero protection """ if "polarizability" not in all_stat.keys(): self.avgeig = np.zeros([9]) diff --git a/deepmd/utils/data_system.py b/deepmd/utils/data_system.py index 20111558cf..592b1f9748 100644 --- a/deepmd/utils/data_system.py +++ b/deepmd/utils/data_system.py @@ -22,6 +22,9 @@ from deepmd.utils.data import ( DeepmdData, ) +from deepmd.utils.out_stat import ( + compute_stats_from_redu, +) log = logging.getLogger(__name__) @@ -248,10 +251,12 @@ def compute_energy_shift(self, rcond=None, key="energy"): sys_tynatom = np.array(self.natoms_vec, dtype=GLOBAL_NP_FLOAT_PRECISION) sys_tynatom = np.reshape(sys_tynatom, [self.nsystems, -1]) sys_tynatom = sys_tynatom[:, 2:] - energy_shift, resd, rank, s_value = np.linalg.lstsq( - sys_tynatom, sys_ener, rcond=rcond + energy_shift, _ = compute_stats_from_redu( + sys_ener.reshape(-1, 1), + sys_tynatom, + rcond=rcond, ) - return energy_shift + return energy_shift.ravel() def add_dict(self, adict: dict) -> None: """Add items to the data system by a `dict`. diff --git a/deepmd/utils/out_stat.py b/deepmd/utils/out_stat.py new file mode 100644 index 0000000000..8f68e32417 --- /dev/null +++ b/deepmd/utils/out_stat.py @@ -0,0 +1,117 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Output statistics.""" +from typing import ( + Optional, + Tuple, +) + +import numpy as np + + +def compute_stats_from_redu( + output_redu: np.ndarray, + natoms: np.ndarray, + assigned_bias: Optional[np.ndarray] = None, + rcond: Optional[float] = None, +) -> Tuple[np.ndarray, np.ndarray]: + """Compute the output statistics. + + Given the reduced output value and the number of atoms for each atom, + compute the least-squares solution as the atomic output bais and std. + + Parameters + ---------- + output_redu + The reduced output value, shape is [nframes, ndim]. + natoms + The number of atoms for each atom, shape is [nframes, ntypes]. + assigned_bias + The assigned output bias, shape is [ntypes, ndim]. Set to nan + if not assigned. + rcond + Cut-off ratio for small singular values of a. + + Returns + ------- + np.ndarray + The computed output bias, shape is [ntypes, ndim]. + np.ndarray + The computed output std, shape is [ntypes, ndim]. + """ + output_redu = np.array(output_redu) + natoms = np.array(natoms) + # check shape + assert output_redu.ndim == 2 + assert natoms.ndim == 2 + assert output_redu.shape[0] == natoms.shape[0] # nframes + if assigned_bias is not None: + assigned_bias = np.array(assigned_bias).reshape( + natoms.shape[1], output_redu.shape[1] + ) + # compute output bias + if assigned_bias is not None: + # Atomic energies stats are incorrect if atomic energies are assigned. + # In this situation, we directly use these assigned energies instead of computing stats. + # This will make the loss decrease quickly + assigned_bias_atom_mask = ~np.isnan(assigned_bias).any(axis=1) + # assigned_bias_masked: nmask, ndim + assigned_bias_masked = assigned_bias[assigned_bias_atom_mask] + # assigned_bias_natoms: nframes, nmask + assigned_bias_natoms = natoms[:, assigned_bias_atom_mask] + # output_redu: nframes, ndim + output_redu -= np.einsum( + "ij,jk->ik", assigned_bias_natoms, assigned_bias_masked + ) + # remove assigned atom + natoms[:, assigned_bias_atom_mask] = 0 + + # computed_output_bias: ntypes, ndim + computed_output_bias, _, _, _ = np.linalg.lstsq(natoms, output_redu, rcond=rcond) + if assigned_bias is not None: + # add back assigned atom; this might not be required + computed_output_bias[assigned_bias_atom_mask] = assigned_bias_masked + # rest_redu: nframes, ndim + rest_redu = output_redu - np.einsum("ij,jk->ik", natoms, computed_output_bias) + output_std = rest_redu.std(axis=0) + return computed_output_bias, output_std + + +def compute_stats_from_atomic( + output: np.ndarray, + atype: np.ndarray, +) -> Tuple[np.ndarray, np.ndarray]: + """Compute the output statistics. + + Given the output value and the type of atoms, + compute the atomic output bais and std. + + Parameters + ---------- + output + The output value, shape is [nframes, nloc, ndim]. + atype + The type of atoms, shape is [nframes, nloc]. + + Returns + ------- + np.ndarray + The computed output bias, shape is [ntypes, ndim]. + np.ndarray + The computed output std, shape is [ntypes, ndim]. + """ + output = np.array(output) + atype = np.array(atype) + # check shape + assert output.ndim == 3 + assert atype.ndim == 2 + assert output.shape[:2] == atype.shape + # compute output bias + nframes, nloc, ndim = output.shape + ntypes = atype.max() + 1 + output_bias = np.zeros((ntypes, ndim)) + output_std = np.zeros((ntypes, ndim)) + for type_i in range(ntypes): + mask = atype == type_i + output_bias[type_i] = output[mask].mean(axis=0) + output_std[type_i] = output[mask].std(axis=0) + return output_bias, output_std diff --git a/source/tests/common/test_out_stat.py b/source/tests/common/test_out_stat.py new file mode 100644 index 0000000000..c0cfc25071 --- /dev/null +++ b/source/tests/common/test_out_stat.py @@ -0,0 +1,124 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import unittest + +import numpy as np + +from deepmd.utils.out_stat import ( + compute_stats_from_atomic, + compute_stats_from_redu, +) + + +class TestOutStat(unittest.TestCase): + def setUp(self) -> None: + rng = np.random.default_rng(20240227) + ndim = 5 + nframes = 1000 + ntypes = 3 + nloc = 1000 + self.atype = rng.integers(0, ntypes, size=(nframes, nloc)) + # compute the number of atoms for each type in each frame + self.natoms = np.zeros((nframes, ntypes), dtype=np.int64) + for i in range(ntypes): + self.natoms[:, i] = (self.atype == i).sum(axis=1) + self.mean = rng.random((ntypes, ndim)) * 1e4 + self.std = rng.random((ntypes, ndim)) * 1e-3 + + # generate random output + self.output = rng.normal( + loc=self.mean[self.atype, :], + scale=self.std[self.atype, :], + size=(nframes, nloc, ndim), + ) + self.output_redu = self.output.sum(axis=1) + + return super().setUp() + + def test_compute_stats_from_redu(self): + bias, std = compute_stats_from_redu(self.output_redu, self.natoms) + np.testing.assert_allclose(bias, self.mean, rtol=1e-7) + reference_std = np.array( + [ + 0.01700638138272794, + 0.01954897296228177, + 0.020281857747683162, + 0.010741237959989648, + 0.020258211828681347, + ] + ) + np.testing.assert_allclose( + std, + reference_std, + rtol=1e-7, + ) + # ensure the sum is close + np.testing.assert_allclose( + self.output_redu, + self.natoms @ bias, + rtol=1e-7, + ) + + def test_compute_stats_from_redu_with_assigned_bias(self): + assigned_bias = np.full_like(self.mean, np.nan) + assigned_bias[0] = self.mean[0] + bias, std = compute_stats_from_redu( + self.output_redu, + self.natoms, + assigned_bias=assigned_bias, + ) + np.testing.assert_allclose(bias, self.mean, rtol=1e-7) + np.testing.assert_allclose(bias[0], self.mean[0], rtol=1e-14) + reference_std = np.array( + [ + 0.017015794087883902, + 0.019549011723239484, + 0.020285565914828625, + 0.01074124012073672, + 0.020283557003416414, + ] + ) + np.testing.assert_allclose( + std, + reference_std, + rtol=1e-7, + ) + # ensure the sum is close + np.testing.assert_allclose( + self.output_redu, + self.natoms @ bias, + rtol=1e-7, + ) + + def test_compute_stats_from_atomic(self): + bias, std = compute_stats_from_atomic(self.output, self.atype) + np.testing.assert_allclose(bias, self.mean) + reference_std = np.array( + [ + [ + 0.0005452949516910239, + 0.000686732800598535, + 0.00089423457667224, + 7.818017989121455e-05, + 0.0004758637035637342, + ], + [ + 2.0610161678825724e-05, + 0.0007728218734771541, + 0.0004754659308165858, + 0.0001809007655290948, + 0.0008187364708029638, + ], + [ + 0.0007935836092665254, + 0.00031176505013516624, + 0.0005469653430009186, + 0.0005652240916389281, + 0.0006087722080071852, + ], + ] + ) + np.testing.assert_allclose( + std, + reference_std, + rtol=1e-7, + ) diff --git a/source/tests/pt/test_stat.py b/source/tests/pt/test_stat.py index 1e3c707d6f..98d4e59d95 100644 --- a/source/tests/pt/test_stat.py +++ b/source/tests/pt/test_stat.py @@ -20,15 +20,15 @@ from deepmd.pt.model.descriptor.dpa1 import ( DescrptDPA1, ) +from deepmd.pt.model.task.ener import ( + EnergyFittingNet, +) from deepmd.pt.utils import ( env, ) from deepmd.pt.utils.dataloader import ( DpLoaderSet, ) -from deepmd.pt.utils.stat import ( - compute_output_bias, -) from deepmd.pt.utils.stat import make_stat_input as my_make from deepmd.tf.common import ( expand_sys_str, @@ -145,9 +145,14 @@ def my_merge(energy, natoms): dp_fn = EnerFitting( self.dp_d.get_ntypes(), self.dp_d.get_dim_out(), self.n_neuron ) - dp_fn.compute_output_stats(self.dp_sampled) - bias_atom_e = compute_output_bias(energy, natoms) - self.assertTrue(np.allclose(dp_fn.bias_atom_e, bias_atom_e[:, 0])) + dp_fn.compute_output_stats(self.dp_sampled, mixed_type=self.mixed_type) + pt_fn = EnergyFittingNet( + self.dp_d.get_ntypes(), self.dp_d.get_dim_out(), self.n_neuron + ) + pt_fn.compute_output_stats(self.my_sampled) + np.testing.assert_allclose( + dp_fn.bias_atom_e, pt_fn.bias_atom_e.detach().cpu().numpy().ravel() + ) # temporarily delete this function for performance of seeds in tf and pytorch may be different """ diff --git a/source/tests/tf/common.py b/source/tests/tf/common.py index a83397c11c..0bcb29b4b5 100644 --- a/source/tests/tf/common.py +++ b/source/tests/tf/common.py @@ -17,6 +17,9 @@ tf, ) from deepmd.tf.utils import random as dp_random +from deepmd.utils.out_stat import ( + compute_stats_from_redu, +) if GLOBAL_NP_FLOAT_PRECISION == np.float32: global_default_fv_hh = 1e-2 @@ -1041,10 +1044,12 @@ def compute_energy_shift(self): sys_tynatom = np.array(self.natoms_vec, dtype=GLOBAL_NP_FLOAT_PRECISION) sys_tynatom = np.reshape(sys_tynatom, [self.nsystems, -1]) sys_tynatom = sys_tynatom[:, 2:] - energy_shift, resd, rank, s_value = np.linalg.lstsq( - sys_tynatom, sys_ener, rcond=None + energy_shift, _ = compute_stats_from_redu( + sys_ener.reshape(-1, 1), + sys_tynatom, + rcond=None, ) - return energy_shift + return energy_shift.ravel() def process_sys_weights(self, sys_weights): sys_weights = np.array(sys_weights) From 2a1508df452b6f538892ebda4a618d45a9c14848 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 28 Feb 2024 09:19:43 -0500 Subject: [PATCH 7/7] feat(pt): support fparam/aparam in DeepEval (#3356) Signed-off-by: Jinzhe Zeng Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/infer/deep_eval.py | 27 +++++++++ deepmd/pt/infer/deep_eval.py | 32 ++++++++-- deepmd/pt/train/wrapper.py | 9 ++- source/tests/infer/fparam_aparam.pbtxt | 2 +- source/tests/infer/fparam_aparam.pth | Bin 0 -> 105871 bytes source/tests/pt/model/test_deeppot.py | 22 +++++++ source/tests/tf/test_deeppot_a.py | 77 ++++++++++++------------- 7 files changed, 121 insertions(+), 48 deletions(-) create mode 100644 source/tests/infer/fparam_aparam.pth diff --git a/deepmd/infer/deep_eval.py b/deepmd/infer/deep_eval.py index 35d170cdab..de964b88b9 100644 --- a/deepmd/infer/deep_eval.py +++ b/deepmd/infer/deep_eval.py @@ -472,6 +472,33 @@ def _standard_input(self, coords, cells, atom_types, fparam, aparam, mixed_type) aparam = np.array(aparam) natoms, nframes = self._get_natoms_and_nframes(coords, atom_types, mixed_type) atom_types = self._expande_atype(atom_types, nframes, mixed_type) + coords = coords.reshape(nframes, natoms, 3) + if cells is not None: + cells = cells.reshape(nframes, 3, 3) + if fparam is not None: + fdim = self.get_dim_fparam() + if fparam.size == nframes * fdim: + fparam = np.reshape(fparam, [nframes, fdim]) + elif fparam.size == fdim: + fparam = np.tile(fparam.reshape([-1]), [nframes, 1]) + else: + raise RuntimeError( + "got wrong size of frame param, should be either %d x %d or %d" + % (nframes, fdim, fdim) + ) + if aparam is not None: + fdim = self.get_dim_aparam() + if aparam.size == nframes * natoms * fdim: + aparam = np.reshape(aparam, [nframes, natoms * fdim]) + elif aparam.size == natoms * fdim: + aparam = np.tile(aparam.reshape([-1]), [nframes, 1]) + elif aparam.size == fdim: + aparam = np.tile(aparam.reshape([-1]), [nframes, natoms]) + else: + raise RuntimeError( + "got wrong size of frame param, should be either %d x %d x %d or %d x %d or %d" + % (nframes, natoms, fdim, natoms, fdim, fdim) + ) return coords, cells, atom_types, fparam, aparam, nframes, natoms def get_sel_type(self) -> List[int]: diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index b13a968a61..f75052166b 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -54,6 +54,9 @@ DEVICE, GLOBAL_PT_FLOAT_PRECISION, ) +from deepmd.pt.utils.utils import ( + to_torch_tensor, +) if TYPE_CHECKING: import ase.neighborlist @@ -228,8 +231,6 @@ def eval( The output of the evaluation. The keys are the names of the output variables, and the values are the corresponding output arrays. """ - if fparam is not None or aparam is not None: - raise NotImplementedError # convert all of the input to numpy array atom_types = np.array(atom_types, dtype=np.int32) coords = np.array(coords) @@ -240,7 +241,12 @@ def eval( ) request_defs = self._get_request_defs(atomic) out = self._eval_func(self._eval_model, numb_test, natoms)( - coords, cells, atom_types, request_defs + coords, + cells, + atom_types, + fparam, + aparam, + request_defs, ) return dict( zip( @@ -330,6 +336,8 @@ def _eval_model( coords: np.ndarray, cells: Optional[np.ndarray], atom_types: np.ndarray, + fparam: Optional[np.ndarray], + aparam: Optional[np.ndarray], request_defs: List[OutputVariableDef], ): model = self.dp.to(DEVICE) @@ -355,12 +363,26 @@ def _eval_model( ) else: box_input = None - + if fparam is not None: + fparam_input = to_torch_tensor(fparam.reshape(-1, self.get_dim_fparam())) + else: + fparam_input = None + if aparam is not None: + aparam_input = to_torch_tensor( + aparam.reshape(-1, natoms, self.get_dim_aparam()) + ) + else: + aparam_input = None do_atomic_virial = any( x.category == OutputVariableCategory.DERV_C_REDU for x in request_defs ) batch_output = model( - coord_input, type_input, box=box_input, do_atomic_virial=do_atomic_virial + coord_input, + type_input, + box=box_input, + do_atomic_virial=do_atomic_virial, + fparam=fparam_input, + aparam=aparam_input, ) if isinstance(batch_output, tuple): batch_output = batch_output[0] diff --git a/deepmd/pt/train/wrapper.py b/deepmd/pt/train/wrapper.py index 2207f111a0..74b4a83ce7 100644 --- a/deepmd/pt/train/wrapper.py +++ b/deepmd/pt/train/wrapper.py @@ -164,6 +164,8 @@ def forward( task_key: Optional[torch.Tensor] = None, inference_only=False, do_atomic_virial=False, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, ): if not self.multi_task: task_key = "Default" @@ -172,7 +174,12 @@ def forward( task_key is not None ), f"Multitask model must specify the inference task! Supported tasks are {list(self.model.keys())}." model_pred = self.model[task_key]( - coord, atype, box=box, do_atomic_virial=do_atomic_virial + coord, + atype, + box=box, + do_atomic_virial=do_atomic_virial, + fparam=fparam, + aparam=aparam, ) natoms = atype.shape[-1] if not self.inference_only and not inference_only: diff --git a/source/tests/infer/fparam_aparam.pbtxt b/source/tests/infer/fparam_aparam.pbtxt index a89596961e..8c2e884090 100644 --- a/source/tests/infer/fparam_aparam.pbtxt +++ b/source/tests/infer/fparam_aparam.pbtxt @@ -35,7 +35,7 @@ node { dtype: DT_STRING tensor_shape { } - string_val: "{\"model\":{\"data_stat_nbatch\":1,\"descriptor\":{\"type\":\"se_e2_a\",\"sel\":[60],\"rcut_smth\":1.8,\"rcut\":6.0,\"neuron\":[5,10,20],\"resnet_dt\":false,\"axis_neuron\":8,\"seed\":1,\"activation_function\":\"tanh\",\"type_one_side\":false,\"precision\":\"default\",\"trainable\":true,\"exclude_types\":[],\"set_davg_zero\":false},\"fitting_net\":{\"neuron\":[5,5,5],\"resnet_dt\":true,\"numb_fparam\":1,\"numb_aparam\":1,\"seed\":1,\"type\":\"ener\",\"activation_function\":\"tanh\",\"precision\":\"default\",\"trainable\":true,\"rcond\":0.001,\"atom_ener\":[],\"use_aparam_as_mask\":false},\"data_stat_protect\":0.01,\"data_bias_nsample\":10},\"loss\":{\"start_pref_e\":0.02,\"limit_pref_e\":1,\"start_pref_f\":1000,\"limit_pref_f\":1,\"start_pref_v\":0,\"limit_pref_v\":0,\"type\":\"ener\",\"start_pref_ae\":0.0,\"limit_pref_ae\":0.0,\"start_pref_pf\":0.0,\"limit_pref_pf\":0.0,\"enable_atom_ener_coeff\":false},\"learning_rate\":{\"start_lr\":0.001,\"stop_lr\":3e-08,\"decay_steps\":5000,\"scale_by_worker\":\"linear\",\"type\":\"exp\"},\"training\":{\"training_data\":{\"systems\":[\"../data/e3000_i2000/\",\"../data/e8000_i2000/\"],\"set_prefix\":\"set\",\"batch_size\":1,\"auto_prob\":\"prob_sys_size\",\"sys_probs\":null},\"seed\":1,\"disp_file\":\"lcurve.out\",\"disp_freq\":100,\"save_freq\":1000,\"save_ckpt\":\"model.ckpt\",\"disp_training\":true,\"time_training\":true,\"profiling\":false,\"profiling_file\":\"timeline.json\",\"numb_steps\":1000,\"validation_data\":null,\"enable_profiler\":false,\"tensorboard\":false,\"tensorboard_log_dir\":\"log\",\"tensorboard_freq\":1}}" + string_val: "{\"model\":{\"data_stat_nbatch\":1,\"type_map\":[\"O\"],\"descriptor\":{\"type\":\"se_e2_a\",\"sel\":[60],\"rcut_smth\":1.8,\"rcut\":6.0,\"neuron\":[5,10,20],\"resnet_dt\":false,\"axis_neuron\":8,\"seed\":1,\"activation_function\":\"tanh\",\"type_one_side\":false,\"precision\":\"default\",\"trainable\":true,\"exclude_types\":[],\"set_davg_zero\":false},\"fitting_net\":{\"neuron\":[5,5,5],\"resnet_dt\":true,\"numb_fparam\":1,\"numb_aparam\":1,\"seed\":1,\"type\":\"ener\",\"activation_function\":\"tanh\",\"precision\":\"default\",\"trainable\":true,\"rcond\":0.001,\"atom_ener\":[],\"use_aparam_as_mask\":false},\"data_stat_protect\":0.01,\"data_bias_nsample\":10},\"loss\":{\"start_pref_e\":0.02,\"limit_pref_e\":1,\"start_pref_f\":1000,\"limit_pref_f\":1,\"start_pref_v\":0,\"limit_pref_v\":0,\"type\":\"ener\",\"start_pref_ae\":0.0,\"limit_pref_ae\":0.0,\"start_pref_pf\":0.0,\"limit_pref_pf\":0.0,\"enable_atom_ener_coeff\":false},\"learning_rate\":{\"start_lr\":0.001,\"stop_lr\":3e-08,\"decay_steps\":5000,\"scale_by_worker\":\"linear\",\"type\":\"exp\"},\"training\":{\"training_data\":{\"systems\":[\"../data/e3000_i2000/\",\"../data/e8000_i2000/\"],\"set_prefix\":\"set\",\"batch_size\":1,\"auto_prob\":\"prob_sys_size\",\"sys_probs\":null},\"seed\":1,\"disp_file\":\"lcurve.out\",\"disp_freq\":100,\"save_freq\":1000,\"save_ckpt\":\"model.ckpt\",\"disp_training\":true,\"time_training\":true,\"profiling\":false,\"profiling_file\":\"timeline.json\",\"numb_steps\":1000,\"validation_data\":null,\"enable_profiler\":false,\"tensorboard\":false,\"tensorboard_log_dir\":\"log\",\"tensorboard_freq\":1}}" } } } diff --git a/source/tests/infer/fparam_aparam.pth b/source/tests/infer/fparam_aparam.pth new file mode 100644 index 0000000000000000000000000000000000000000..7b0204cdd3abbb1ac39e729ca0ed902d814ca6a1 GIT binary patch literal 105871 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 z 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 n{~Tz%mAe>YX%u`86Cq6-{