From 5794a871645f2dd999cf991e6e819bc27ad39ec1 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 5 Mar 2024 21:54:28 -0500 Subject: [PATCH 1/6] revert test Python to 3.11 (#3419) Python 3.12 or TF 2.16 seems to be slowing down... Signed-off-by: Jinzhe Zeng --- .github/workflows/test_python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_python.yml b/.github/workflows/test_python.yml index 514b552aec..60b5ecf0e0 100644 --- a/.github/workflows/test_python.yml +++ b/.github/workflows/test_python.yml @@ -18,7 +18,7 @@ jobs: - python: 3.8 tf: torch: - - python: "3.12" + - python: "3.11" tf: torch: From 278e6b8becbce93b275b86b2dbe26b23b621ef5f Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Tue, 5 Mar 2024 21:54:44 -0500 Subject: [PATCH 2/6] convert exclude_types to sel_type (#3418) This can give the correct result for `dp test`, ```sh cd examples/water_tensor/dipole dp --pt train input_torch.json dp --pt freeze dp test -m frozen_model.pth -s validation_data/global_system/ ``` Signed-off-by: Jinzhe Zeng --- deepmd/dpmodel/fitting/general_fitting.py | 2 +- deepmd/pt/model/task/fitting.py | 10 +++++++++- .../tests/common/dpmodel/test_fitting_invar_fitting.py | 4 ++++ source/tests/pt/model/test_ener_fitting.py | 1 + 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/deepmd/dpmodel/fitting/general_fitting.py b/deepmd/dpmodel/fitting/general_fitting.py index c004814b60..01bf107c63 100644 --- a/deepmd/dpmodel/fitting/general_fitting.py +++ b/deepmd/dpmodel/fitting/general_fitting.py @@ -179,7 +179,7 @@ def get_sel_type(self) -> List[int]: to the result of the model. If returning an empty list, all atom types are selected. """ - return [] + return [ii for ii in range(self.ntypes) if ii not in self.exclude_types] def __setitem__(self, key, value): if key in ["bias_atom_e"]: diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index bd38fca14a..22fb409cad 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -388,6 +388,9 @@ def get_dim_aparam(self) -> int: """Get the number (dimension) of atomic parameters of this atomic model.""" return self.numb_aparam + # make jit happy + exclude_types: List[int] + def get_sel_type(self) -> List[int]: """Get the selected atom types of this model. @@ -395,7 +398,12 @@ def get_sel_type(self) -> List[int]: to the result of the model. If returning an empty list, all atom types are selected. """ - return [] + # make jit happy + sel_type: List[int] = [] + for ii in range(self.ntypes): + if ii not in self.exclude_types: + sel_type.append(ii) + return sel_type def __setitem__(self, key, value): if key in ["bias_atom_e"]: diff --git a/source/tests/common/dpmodel/test_fitting_invar_fitting.py b/source/tests/common/dpmodel/test_fitting_invar_fitting.py index a31439d406..87eeb9e06b 100644 --- a/source/tests/common/dpmodel/test_fitting_invar_fitting.py +++ b/source/tests/common/dpmodel/test_fitting_invar_fitting.py @@ -64,6 +64,10 @@ def test_self_consistency( ret0 = ifn0(dd[0], atype, fparam=ifp, aparam=iap) ret1 = ifn1(dd[0], atype, fparam=ifp, aparam=iap) np.testing.assert_allclose(ret0["energy"], ret1["energy"]) + sel_set = set(ifn0.get_sel_type()) + exclude_set = set(et) + self.assertEqual(sel_set | exclude_set, set(range(self.nt))) + self.assertEqual(sel_set & exclude_set, set()) def test_mask(self): nf, nloc, nnei = self.nlist.shape diff --git a/source/tests/pt/model/test_ener_fitting.py b/source/tests/pt/model/test_ener_fitting.py index a41b4d6b9f..69bd4b42a3 100644 --- a/source/tests/pt/model/test_ener_fitting.py +++ b/source/tests/pt/model/test_ener_fitting.py @@ -95,6 +95,7 @@ def test_consistency( to_numpy_array(ret0["foo"]), to_numpy_array(ret2["foo"]), ) + self.assertEqual(ft0.get_sel_type(), ft1.get_sel_type()) def test_new_old( self, From d3ca9d72bdea229f106e1aca50a115f2a0cdbcde Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Wed, 6 Mar 2024 01:07:37 -0500 Subject: [PATCH 3/6] pt: avoid D2H in se_e2_a (#3424) sec is used as a slice index, so it should not stored on the GPU, otherwise, D2H will happen to create the tensor with the shape. Before: ![image](https://github.com/deepmodeling/deepmd-kit/assets/9496702/f5a520d8-ed83-4520-aed0-d8fed547c293) After: ![image](https://github.com/deepmodeling/deepmd-kit/assets/9496702/5548632b-3099-4fe2-ab53-5c570abd714a) Signed-off-by: Jinzhe Zeng --- deepmd/pt/model/descriptor/se_a.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index 3a18f150a4..c4b2c772f8 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -342,9 +342,8 @@ def __init__( self.reinit_exclude(exclude_types) self.sel = sel - self.sec = torch.tensor( - np.append([0], np.cumsum(self.sel)), dtype=int, device=env.DEVICE - ) + # should be on CPU to avoid D2H, as it is used as slice index + self.sec = [0, *np.cumsum(self.sel).tolist()] self.split_sel = self.sel self.nnei = sum(sel) self.ndescrpt = self.nnei * 4 From fa8e6453b5cc88ae575284c4c0ded06758c6de82 Mon Sep 17 00:00:00 2001 From: Anyang Peng <137014849+anyangml@users.noreply.github.com> Date: Wed, 6 Mar 2024 17:23:29 +0800 Subject: [PATCH 4/6] Feat: add zbl training (#3398) Signed-off-by: Anyang Peng <137014849+anyangml@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/dpmodel/atomic_model/__init__.py | 8 +- .../atomic_model/linear_atomic_model.py | 62 ++++++++--- .../atomic_model/make_base_atomic_model.py | 11 +- .../atomic_model/pairtab_atomic_model.py | 11 +- deepmd/pt/model/atomic_model/__init__.py | 8 +- .../model/atomic_model/linear_atomic_model.py | 100 +++++++++++++++--- .../atomic_model/pairtab_atomic_model.py | 85 ++++++++++++++- deepmd/pt/model/model/dp_zbl_model.py | 4 +- deepmd/pt/model/task/ener.py | 49 ++------- deepmd/pt/train/training.py | 29 +++-- deepmd/pt/utils/stat.py | 81 ++++++++++++++ .../dpmodel/test_linear_atomic_model.py | 8 +- .../pt/model/test_linear_atomic_model.py | 32 ++++-- .../pt/model/test_pairtab_atomic_model.py | 2 +- source/tests/pt/model/water/zbl.json | 92 ++++++++++++++++ source/tests/pt/test_finetune.py | 51 +++++++-- source/tests/pt/test_training.py | 18 ++++ 17 files changed, 519 insertions(+), 132 deletions(-) create mode 100644 source/tests/pt/model/water/zbl.json diff --git a/deepmd/dpmodel/atomic_model/__init__.py b/deepmd/dpmodel/atomic_model/__init__.py index 2cd20f54c1..e51ca0a65e 100644 --- a/deepmd/dpmodel/atomic_model/__init__.py +++ b/deepmd/dpmodel/atomic_model/__init__.py @@ -22,8 +22,8 @@ DPAtomicModel, ) from .linear_atomic_model import ( - DPZBLLinearAtomicModel, - LinearAtomicModel, + DPZBLLinearEnergyAtomicModel, + LinearEnergyAtomicModel, ) from .make_base_atomic_model import ( make_base_atomic_model, @@ -37,6 +37,6 @@ "BaseAtomicModel", "DPAtomicModel", "PairTabAtomicModel", - "LinearAtomicModel", - "DPZBLLinearAtomicModel", + "LinearEnergyAtomicModel", + "DPZBLLinearEnergyAtomicModel", ] diff --git a/deepmd/dpmodel/atomic_model/linear_atomic_model.py b/deepmd/dpmodel/atomic_model/linear_atomic_model.py index b1a32cdaa5..ac2a73a381 100644 --- a/deepmd/dpmodel/atomic_model/linear_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/linear_atomic_model.py @@ -38,7 +38,7 @@ ) -class LinearAtomicModel(BaseAtomicModel): +class LinearEnergyAtomicModel(BaseAtomicModel): """Linear model make linear combinations of several existing models. Parameters @@ -59,14 +59,16 @@ def __init__( self.models = models sub_model_type_maps = [md.get_type_map() for md in models] err_msg = [] + self.mapping_list = [] common_type_map = set(type_map) + self.type_map = type_map for tpmp in sub_model_type_maps: if not common_type_map.issubset(set(tpmp)): err_msg.append( f"type_map {tpmp} is not a subset of type_map {type_map}" ) + self.mapping_list.append(self.remap_atype(tpmp, self.type_map)) assert len(err_msg) == 0, "\n".join(err_msg) - self.type_map = type_map self.mixed_types_list = [model.mixed_types() for model in self.models] super().__init__(**kwargs) @@ -163,17 +165,20 @@ def forward_atomic( self.mixed_types_list, raw_nlists, self.get_model_sels() ) ] - ener_list = [ - model.forward_atomic( - extended_coord, - extended_atype, - nl, - mapping, - fparam, - aparam, - )["energy"] - for model, nl in zip(self.models, nlists_) - ] + ener_list = [] + + for i, model in enumerate(self.models): + mapping = self.mapping_list[i] + ener_list.append( + model.forward_atomic( + extended_coord, + mapping[extended_atype], + nlists_[i], + mapping, + fparam, + aparam, + )["energy"] + ) self.weights = self._compute_weight(extended_coord, extended_atype, nlists_) self.atomic_bias = None if self.atomic_bias is not None: @@ -184,6 +189,29 @@ def forward_atomic( } # (nframes, nloc, 1) return fit_ret + @staticmethod + def remap_atype(ori_map: List[str], new_map: List[str]) -> np.ndarray: + """ + This method is used to map the atype from the common type_map to the original type_map of + indivial AtomicModels. + + Parameters + ---------- + ori_map : List[str] + The original type map of an AtomicModel. + new_map : List[str] + The common type map of the DPZBLLinearEnergyAtomicModel, created by the `get_type_map` method, + must be a subset of the ori_map. + + Returns + ------- + np.ndarray + """ + type_2_idx = {atp: idx for idx, atp in enumerate(ori_map)} + # this maps the atype in the new map to the original map + mapping = np.array([type_2_idx[new_map[idx]] for idx in range(len(new_map))]) + return mapping + def fitting_output_def(self) -> FittingOutputDef: return FittingOutputDef( [ @@ -261,7 +289,7 @@ def is_aparam_nall(self) -> bool: return False -class DPZBLLinearAtomicModel(LinearAtomicModel): +class DPZBLLinearEnergyAtomicModel(LinearEnergyAtomicModel): """Model linearly combine a list of AtomicModels. Parameters @@ -308,7 +336,7 @@ def serialize(self) -> dict: "@class": "Model", "type": "zbl", "@version": 1, - "models": LinearAtomicModel.serialize( + "models": LinearEnergyAtomicModel.serialize( [self.dp_model, self.zbl_model], self.type_map ), "sw_rmin": self.sw_rmin, @@ -319,7 +347,7 @@ def serialize(self) -> dict: return dd @classmethod - def deserialize(cls, data) -> "DPZBLLinearAtomicModel": + def deserialize(cls, data) -> "DPZBLLinearEnergyAtomicModel": data = copy.deepcopy(data) check_version_compatibility(data.pop("@version", 1), 1, 1) data.pop("@class") @@ -328,7 +356,7 @@ def deserialize(cls, data) -> "DPZBLLinearAtomicModel": sw_rmax = data.pop("sw_rmax") smin_alpha = data.pop("smin_alpha") - ([dp_model, zbl_model], type_map) = LinearAtomicModel.deserialize( + ([dp_model, zbl_model], type_map) = LinearEnergyAtomicModel.deserialize( data.pop("models") ) diff --git a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py index 5548147d54..ce1a6708e6 100644 --- a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py @@ -54,18 +54,13 @@ def get_rcut(self) -> float: pass @abstractmethod - def get_type_map(self) -> Optional[List[str]]: + def get_type_map(self) -> List[str]: """Get the type map.""" + pass def get_ntypes(self) -> int: """Get the number of atom types.""" - tmap = self.get_type_map() - if tmap is not None: - return len(tmap) - else: - raise ValueError( - "cannot infer the number of types from a None type map" - ) + return len(self.get_type_map()) @abstractmethod def get_sel(self) -> List[int]: diff --git a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py index c858179939..46ec808ad4 100644 --- a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py @@ -66,9 +66,17 @@ def __init__( self.type_map = type_map self.tab = PairTab(self.tab_file, rcut=rcut) + self.type_map = type_map + self.ntypes = len(type_map) if self.tab_file is not None: self.tab_info, self.tab_data = self.tab.get() + nspline, ntypes_tab = self.tab_info[-2:].astype(int) + self.tab_data = self.tab_data.reshape(ntypes_tab, ntypes_tab, nspline, 4) + if self.ntypes != ntypes_tab: + raise ValueError( + "The `type_map` provided does not match the number of columns in the table." + ) else: self.tab_info, self.tab_data = None, None @@ -145,7 +153,8 @@ def deserialize(cls, data) -> "PairTabAtomicModel": tab_model = cls(None, rcut, sel, type_map, **data) tab_model.tab = tab tab_model.tab_info = tab_model.tab.tab_info - tab_model.tab_data = tab_model.tab.tab_data + nspline, ntypes = tab_model.tab_info[-2:].astype(int) + tab_model.tab_data = tab_model.tab.tab_data.reshape(ntypes, ntypes, nspline, 4) return tab_model def forward_atomic( diff --git a/deepmd/pt/model/atomic_model/__init__.py b/deepmd/pt/model/atomic_model/__init__.py index 75c1ce3c2e..a747f28556 100644 --- a/deepmd/pt/model/atomic_model/__init__.py +++ b/deepmd/pt/model/atomic_model/__init__.py @@ -21,8 +21,8 @@ DPAtomicModel, ) from .linear_atomic_model import ( - DPZBLLinearAtomicModel, - LinearAtomicModel, + DPZBLLinearEnergyAtomicModel, + LinearEnergyAtomicModel, ) from .pairtab_atomic_model import ( PairTabAtomicModel, @@ -32,6 +32,6 @@ "BaseAtomicModel", "DPAtomicModel", "PairTabAtomicModel", - "LinearAtomicModel", - "DPZBLLinearAtomicModel", + "LinearEnergyAtomicModel", + "DPZBLLinearEnergyAtomicModel", ] diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 0dd1b13723..5e1a80087e 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -25,6 +25,9 @@ get_multiple_nlist_key, nlist_distinguish_types, ) +from deepmd.utils.path import ( + DPPath, +) from deepmd.utils.version import ( check_version_compatibility, ) @@ -40,7 +43,7 @@ ) -class LinearAtomicModel(torch.nn.Module, BaseAtomicModel): +class LinearEnergyAtomicModel(torch.nn.Module, BaseAtomicModel): """Linear model make linear combinations of several existing models. Parameters @@ -62,14 +65,17 @@ def __init__( self.models = torch.nn.ModuleList(models) sub_model_type_maps = [md.get_type_map() for md in models] err_msg = [] + self.mapping_list = [] common_type_map = set(type_map) + self.type_map = type_map for tpmp in sub_model_type_maps: if not common_type_map.issubset(set(tpmp)): err_msg.append( f"type_map {tpmp} is not a subset of type_map {type_map}" ) + self.mapping_list.append(self.remap_atype(tpmp, self.type_map)) assert len(err_msg) == 0, "\n".join(err_msg) - self.type_map = type_map + self.atomic_bias = None self.mixed_types_list = [model.mixed_types() for model in self.models] BaseAtomicModel.__init__(self, **kwargs) @@ -117,8 +123,8 @@ def _sort_rcuts_sels(self, device: torch.device) -> Tuple[List[float], List[int] nsels = torch.tensor(self.get_model_nsels(), device=device) zipped = torch.stack( [ - torch.tensor(rcuts, device=device), - torch.tensor(nsels, device=device), + rcuts, + nsels, ], dim=0, ).T @@ -185,10 +191,11 @@ def forward_atomic( ener_list = [] for i, model in enumerate(self.models): + mapping = self.mapping_list[i] ener_list.append( model.forward_atomic( extended_coord, - extended_atype, + mapping[extended_atype], nlists_[i], mapping, fparam, @@ -198,16 +205,48 @@ def forward_atomic( weights = self._compute_weight(extended_coord, extended_atype, nlists_) - if self.atomic_bias is not None: - raise NotImplementedError("Need to add bias in a future PR.") - else: - fit_ret = { - "energy": torch.sum( - torch.stack(ener_list) * torch.stack(weights), dim=0 - ), - } # (nframes, nloc, 1) + atype = extended_atype[:, :nloc] + for idx, model in enumerate(self.models): + # TODO: provide interfaces for atomic models to access bias_atom_e + if isinstance(model, DPAtomicModel): + bias_atom_e = model.fitting_net.bias_atom_e + elif isinstance(model, PairTabAtomicModel): + bias_atom_e = model.bias_atom_e + else: + bias_atom_e = None + if bias_atom_e is not None: + ener_list[idx] += bias_atom_e[atype] + + fit_ret = { + "energy": torch.sum(torch.stack(ener_list) * torch.stack(weights), dim=0), + } # (nframes, nloc, 1) return fit_ret + @staticmethod + def remap_atype(ori_map: List[str], new_map: List[str]) -> torch.Tensor: + """ + This method is used to map the atype from the common type_map to the original type_map of + indivial AtomicModels. It creates a index mapping for the conversion. + + Parameters + ---------- + ori_map : List[str] + The original type map of an AtomicModel. + new_map : List[str] + The common type map of the DPZBLLinearEnergyAtomicModel, created by the `get_type_map` method, + must be a subset of the ori_map. + + Returns + ------- + torch.Tensor + """ + type_2_idx = {atp: idx for idx, atp in enumerate(ori_map)} + # this maps the atype in the new map to the original map + mapping = torch.tensor( + [type_2_idx[new_map[idx]] for idx in range(len(new_map))], device=env.DEVICE + ) + return mapping + def fitting_output_def(self) -> FittingOutputDef: return FittingOutputDef( [ @@ -292,7 +331,7 @@ def is_aparam_nall(self) -> bool: return False -class DPZBLLinearAtomicModel(LinearAtomicModel): +class DPZBLLinearEnergyAtomicModel(LinearEnergyAtomicModel): """Model linearly combine a list of AtomicModels. Parameters @@ -336,6 +375,33 @@ def __init__( # this is a placeholder being updated in _compute_weight, to handle Jit attribute init error. self.zbl_weight = torch.empty(0, dtype=torch.float64, device=env.DEVICE) + def compute_or_load_stat( + self, + sampled_func, + stat_file_path: Optional[DPPath] = None, + ): + """ + Compute or load the statistics parameters of the model, + such as mean and standard deviation of descriptors or the energy bias of the fitting net. + When `sampled` is provided, all the statistics parameters will be calculated (or re-calculated for update), + and saved in the `stat_file_path`(s). + When `sampled` is not provided, it will check the existence of `stat_file_path`(s) + and load the calculated statistics parameters. + + Parameters + ---------- + sampled_func + The lazy sampled function to get data frames from different data systems. + stat_file_path + The dictionary of paths to the statistics files. + """ + self.dp_model.compute_or_load_stat(sampled_func, stat_file_path) + self.zbl_model.compute_or_load_stat(sampled_func, stat_file_path) + + def change_energy_bias(self): + # need to implement + pass + def serialize(self) -> dict: dd = BaseAtomicModel.serialize(self) dd.update( @@ -343,7 +409,7 @@ def serialize(self) -> dict: "@class": "Model", "@version": 1, "type": "zbl", - "models": LinearAtomicModel.serialize( + "models": LinearEnergyAtomicModel.serialize( [self.dp_model, self.zbl_model], self.type_map ), "sw_rmin": self.sw_rmin, @@ -354,14 +420,14 @@ def serialize(self) -> dict: return dd @classmethod - def deserialize(cls, data) -> "DPZBLLinearAtomicModel": + def deserialize(cls, data) -> "DPZBLLinearEnergyAtomicModel": data = copy.deepcopy(data) check_version_compatibility(data.pop("@version", 1), 1, 1) sw_rmin = data.pop("sw_rmin") sw_rmax = data.pop("sw_rmax") smin_alpha = data.pop("smin_alpha") - [dp_model, zbl_model], type_map = LinearAtomicModel.deserialize( + [dp_model, zbl_model], type_map = LinearEnergyAtomicModel.deserialize( data.pop("models") ) diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index bae4ea55e2..215bb25de5 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import copy from typing import ( + Callable, Dict, List, Optional, @@ -13,9 +14,18 @@ FittingOutputDef, OutputVariableDef, ) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.stat import ( + compute_output_stats, +) from deepmd.utils.pair_tab import ( PairTab, ) +from deepmd.utils.path import ( + DPPath, +) from deepmd.utils.version import ( check_version_compatibility, ) @@ -47,9 +57,14 @@ class PairTabAtomicModel(torch.nn.Module, BaseAtomicModel): The cutoff radius. sel : int or list[int] The maxmum number of atoms in the cut-off radius. - type_map : list[str] + type_map : List[str] Mapping atom type to the name (str) of the type. For example `type_map[1]` gives the name of the type 1. + rcond : float, optional + The condition number for the regression of atomic energy. + atom_ener + Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. + """ def __init__( @@ -58,6 +73,8 @@ def __init__( rcut: float, sel: Union[int, List[int]], type_map: List[str], + rcond: Optional[float] = None, + atom_ener: Optional[List[float]] = None, **kwargs, ): torch.nn.Module.__init__(self) @@ -65,8 +82,12 @@ def __init__( self.tab_file = tab_file self.rcut = rcut self.tab = self._set_pairtab(tab_file, rcut) - self.type_map = type_map + BaseAtomicModel.__init__(self, **kwargs) + self.rcond = rcond + self.atom_ener = atom_ener + self.type_map = type_map + self.ntypes = len(type_map) # handle deserialization with no input file if self.tab_file is not None: @@ -74,11 +95,22 @@ def __init__( tab_info, tab_data, ) = self.tab.get() # this returns -> Tuple[np.array, np.array] + nspline, ntypes_tab = tab_info[-2:].astype(int) self.register_buffer("tab_info", torch.from_numpy(tab_info)) - self.register_buffer("tab_data", torch.from_numpy(tab_data)) + self.register_buffer( + "tab_data", + torch.from_numpy(tab_data).reshape(ntypes_tab, ntypes_tab, nspline, 4), + ) + if self.ntypes != ntypes_tab: + raise ValueError( + "The `type_map` provided does not match the number of columns in the table." + ) else: self.register_buffer("tab_info", None) self.register_buffer("tab_data", None) + self.bias_atom_e = torch.zeros( + self.ntypes, 1, dtype=env.GLOBAL_PT_ENER_FLOAT_PRECISION, device=env.DEVICE + ) # self.model_type = "ener" # self.model_version = MODEL_VERSION ## this shoud be in the parent class @@ -145,6 +177,8 @@ def serialize(self) -> dict: "rcut": self.rcut, "sel": self.sel, "type_map": self.type_map, + "rcond": self.rcond, + "atom_ener": self.atom_ener, } ) return dd @@ -156,15 +190,56 @@ def deserialize(cls, data) -> "PairTabAtomicModel": rcut = data.pop("rcut") sel = data.pop("sel") type_map = data.pop("type_map") + rcond = data.pop("rcond") + atom_ener = data.pop("atom_ener") tab = PairTab.deserialize(data.pop("tab")) data.pop("@class", None) data.pop("type", None) - tab_model = cls(None, rcut, sel, type_map, **data) + tab_model = cls(None, rcut, sel, type_map, rcond, atom_ener, **data) + tab_model.tab = tab tab_model.register_buffer("tab_info", torch.from_numpy(tab_model.tab.tab_info)) - tab_model.register_buffer("tab_data", torch.from_numpy(tab_model.tab.tab_data)) + nspline, ntypes = tab_model.tab.tab_info[-2:].astype(int) + tab_model.register_buffer( + "tab_data", + torch.from_numpy(tab_model.tab.tab_data).reshape( + ntypes, ntypes, nspline, 4 + ), + ) return tab_model + def compute_or_load_stat( + self, + merged: Union[Callable[[], List[dict]], List[dict]], + stat_file_path: Optional[DPPath] = None, + ): + """ + Compute the output statistics (e.g. energy bias) for the fitting net from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + stat_file_path : Optional[DPPath] + The path to the stat file. + + """ + bias_atom_e = compute_output_stats( + merged, stat_file_path, self.rcond, self.atom_ener + ) + self.bias_atom_e.copy_( + torch.tensor(bias_atom_e, device=env.DEVICE).view([self.ntypes, 1]) + ) + + def change_energy_bias(self) -> None: + # need to implement + pass + def forward_atomic( self, extended_coord: torch.Tensor, diff --git a/deepmd/pt/model/model/dp_zbl_model.py b/deepmd/pt/model/model/dp_zbl_model.py index dcf1c36e83..cacf59c16c 100644 --- a/deepmd/pt/model/model/dp_zbl_model.py +++ b/deepmd/pt/model/model/dp_zbl_model.py @@ -10,7 +10,7 @@ DPModel, ) from deepmd.pt.model.atomic_model import ( - DPZBLLinearAtomicModel, + DPZBLLinearEnergyAtomicModel, ) from deepmd.pt.model.model.model import ( BaseModel, @@ -20,7 +20,7 @@ make_model, ) -DPZBLModel_ = make_model(DPZBLLinearAtomicModel) +DPZBLModel_ = make_model(DPZBLLinearEnergyAtomicModel) @BaseModel.register("zbl") diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index 8bf9cc1c90..a11f6410a4 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -30,11 +30,8 @@ from deepmd.pt.utils.env import ( DEFAULT_PRECISION, ) -from deepmd.pt.utils.utils import ( - to_numpy_array, -) -from deepmd.utils.out_stat import ( - compute_stats_from_redu, +from deepmd.pt.utils.stat import ( + compute_output_stats, ) from deepmd.utils.path import ( DPPath, @@ -84,8 +81,8 @@ class InvarFitting(GeneralFitting): Random seed. exclude_types: List[int] Atomic contributions of the excluded atom types are set zero. - atom_ener - Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. + atom_ener: List[float], optional + Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. """ @@ -164,41 +161,9 @@ def compute_output_stats( The path to the stat file. """ - if stat_file_path is not None: - stat_file_path = stat_file_path / "bias_atom_e" - if stat_file_path is not None and stat_file_path.is_file(): - bias_atom_e = stat_file_path.load_numpy() - else: - if callable(merged): - # only get data for once - sampled = merged() - else: - sampled = merged - energy = [item["energy"] for item in sampled] - data_mixed_type = "real_natoms_vec" in sampled[0] - if data_mixed_type: - input_natoms = [item["real_natoms_vec"] for item in sampled] - else: - input_natoms = [item["natoms"] for item in sampled] - # 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]) + bias_atom_e = compute_output_stats( + merged, stat_file_path, self.rcond, self.atom_ener + ) self.bias_atom_e.copy_( torch.tensor(bias_atom_e, device=env.DEVICE).view( [self.ntypes, self.dim_out] diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index e5a7632ac4..2a80956b9d 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -29,7 +29,9 @@ TensorLoss, ) from deepmd.pt.model.model import ( + DPZBLModel, get_model, + get_zbl_model, ) from deepmd.pt.optimizer import ( KFOptimizerWrapper, @@ -247,7 +249,10 @@ def get_sample(): def get_single_model( _model_params, ): - model = get_model(deepcopy(_model_params)).to(DEVICE) + if "use_srtab" in _model_params: + model = get_zbl_model(deepcopy(_model_params)).to(DEVICE) + else: + model = get_model(deepcopy(_model_params)).to(DEVICE) return model def get_lr(lr_params): @@ -506,14 +511,20 @@ def get_loss(loss_params, start_lr, _ntypes, _model): model_params["type_map"], model_params["new_type_map"], ) - self.model.fitting_net.change_energy_bias( - config, - self.model, - old_type_map, - new_type_map, - ntest=ntest, - bias_shift=model_params.get("bias_shift", "delta"), - ) + if hasattr(self.model, "fitting_net"): + self.model.fitting_net.change_energy_bias( + config, + self.model, + old_type_map, + new_type_map, + ntest=ntest, + bias_shift=model_params.get("bias_shift", "delta"), + ) + elif isinstance(self.model, DPZBLModel): + # need to updated + self.model.change_energy_bias() + else: + raise NotImplementedError 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()) diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index 3b246a0ec2..63abccc75d 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -1,10 +1,27 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import logging +from typing import ( + Callable, + List, + Optional, + Union, +) +import numpy as np import torch +from deepmd.pt.utils import ( + env, +) from deepmd.pt.utils.utils import ( dict_to_device, + to_numpy_array, +) +from deepmd.utils.out_stat import ( + compute_stats_from_redu, +) +from deepmd.utils.path import ( + DPPath, ) log = logging.getLogger(__name__) @@ -50,3 +67,67 @@ def make_stat_input(datasets, dataloaders, nbatches): dict_to_device(sys_stat) lst.append(sys_stat) return lst + + +def compute_output_stats( + merged: Union[Callable[[], List[dict]], List[dict]], + stat_file_path: Optional[DPPath] = None, + rcond: Optional[float] = None, + atom_ener: Optional[List[float]] = None, +): + """ + Compute the output statistics (e.g. energy bias) for the fitting net from packed data. + + Parameters + ---------- + merged : Union[Callable[[], List[dict]], List[dict]] + - List[dict]: A list of data samples from various data systems. + Each element, `merged[i]`, is a data dictionary containing `keys`: `torch.Tensor` + originating from the `i`-th data system. + - Callable[[], List[dict]]: A lazy function that returns data samples in the above format + only when needed. Since the sampling process can be slow and memory-intensive, + the lazy function helps by only sampling once. + stat_file_path : DPPath, optional + The path to the stat file. + rcond : float, optional + The condition number for the regression of atomic energy. + atom_ener : List[float], optional + Specifying atomic energy contribution in vacuum. The `set_davg_zero` key in the descrptor should be set. + + """ + if stat_file_path is not None: + stat_file_path = stat_file_path / "bias_atom_e" + if stat_file_path is not None and stat_file_path.is_file(): + bias_atom_e = stat_file_path.load_numpy() + else: + if callable(merged): + # only get data for once + sampled = merged() + else: + sampled = merged + energy = [item["energy"] for item in sampled] + data_mixed_type = "real_natoms_vec" in sampled[0] + if data_mixed_type: + input_natoms = [item["real_natoms_vec"] for item in sampled] + else: + input_natoms = [item["natoms"] for item in sampled] + # 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 atom_ener is not None and len(atom_ener) > 0: + assigned_atom_ener = np.array( + [ee if ee is not None else np.nan for ee in atom_ener] + ) + else: + assigned_atom_ener = None + bias_atom_e, _ = compute_stats_from_redu( + merged_energy, + merged_natoms, + assigned_bias=assigned_atom_ener, + rcond=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]) + return torch.tensor(bias_atom_e, device=env.DEVICE) diff --git a/source/tests/common/dpmodel/test_linear_atomic_model.py b/source/tests/common/dpmodel/test_linear_atomic_model.py index cc08a3b3dd..832d1de106 100644 --- a/source/tests/common/dpmodel/test_linear_atomic_model.py +++ b/source/tests/common/dpmodel/test_linear_atomic_model.py @@ -10,7 +10,7 @@ DPAtomicModel, ) from deepmd.dpmodel.atomic_model.linear_atomic_model import ( - DPZBLLinearAtomicModel, + DPZBLLinearEnergyAtomicModel, ) from deepmd.dpmodel.atomic_model.pairtab_atomic_model import ( PairTabAtomicModel, @@ -58,7 +58,7 @@ def test_pairwise(self, mock_loadtxt): ) dp_model = DPAtomicModel(ds, ft, type_map=type_map) - wgt_model = DPZBLLinearAtomicModel( + wgt_model = DPZBLLinearEnergyAtomicModel( dp_model, zbl_model, sw_rmin=0.1, @@ -151,14 +151,14 @@ def setUp(self, mock_loadtxt): zbl_model = PairTabAtomicModel( file_path, self.rcut, sum(self.sel), type_map=type_map ) - self.md0 = DPZBLLinearAtomicModel( + self.md0 = DPZBLLinearEnergyAtomicModel( dp_model, zbl_model, sw_rmin=0.1, sw_rmax=0.25, type_map=type_map, ) - self.md1 = DPZBLLinearAtomicModel.deserialize(self.md0.serialize()) + self.md1 = DPZBLLinearEnergyAtomicModel.deserialize(self.md0.serialize()) def test_self_consistency(self): ret0 = self.md0.forward_atomic(self.coord_ext, self.atype_ext, self.nlist) diff --git a/source/tests/pt/model/test_linear_atomic_model.py b/source/tests/pt/model/test_linear_atomic_model.py index e0904097e3..adc682a41f 100644 --- a/source/tests/pt/model/test_linear_atomic_model.py +++ b/source/tests/pt/model/test_linear_atomic_model.py @@ -8,11 +8,11 @@ import torch from deepmd.dpmodel.atomic_model import ( - DPZBLLinearAtomicModel as DPDPZBLLinearAtomicModel, + DPZBLLinearEnergyAtomicModel as DPDPZBLLinearEnergyAtomicModel, ) from deepmd.pt.model.atomic_model import ( DPAtomicModel, - DPZBLLinearAtomicModel, + DPZBLLinearEnergyAtomicModel, PairTabAtomicModel, ) from deepmd.pt.model.descriptor.se_a import ( @@ -70,10 +70,10 @@ def test_pairwise(self, mock_loadtxt): type_map = ["foo", "bar"] zbl_model = PairTabAtomicModel( - tab_file=file_path, rcut=0.3, sel=2, type_map=type_map + tab_file=file_path, rcut=0.3, sel=2, type_map=type_map[::-1] ) dp_model = DPAtomicModel(ds, ft, type_map=type_map).to(env.DEVICE) - wgt_model = DPZBLLinearAtomicModel( + wgt_model = DPZBLLinearEnergyAtomicModel( dp_model, zbl_model, sw_rmin=0.1, @@ -145,17 +145,17 @@ def setUp(self, mock_loadtxt): zbl_model = PairTabAtomicModel( file_path, self.rcut, sum(self.sel), type_map=type_map ) - self.md0 = DPZBLLinearAtomicModel( + self.md0 = DPZBLLinearEnergyAtomicModel( dp_model, zbl_model, sw_rmin=0.1, sw_rmax=0.25, type_map=type_map, ).to(env.DEVICE) - self.md1 = DPZBLLinearAtomicModel.deserialize(self.md0.serialize()).to( + self.md1 = DPZBLLinearEnergyAtomicModel.deserialize(self.md0.serialize()).to( env.DEVICE ) - self.md2 = DPDPZBLLinearAtomicModel.deserialize(self.md0.serialize()) + self.md2 = DPDPZBLLinearEnergyAtomicModel.deserialize(self.md0.serialize()) self.md3 = DPZBLModel( dp_model, zbl_model, sw_rmin=0.1, sw_rmax=0.25, type_map=type_map ) @@ -185,5 +185,23 @@ def test_jit(self): self.assertEqual(md3.get_type_map(), ["foo", "bar"]) +class TestRemmapMethod(unittest.TestCase): + def test_valid(self): + atype = torch.randint(0, 3, (4, 20), device=env.DEVICE) + commonl = ["H", "O", "S"] + originl = ["Si", "H", "O", "S"] + mapping = DPZBLLinearEnergyAtomicModel.remap_atype(originl, commonl) + new_atype = mapping[atype] + + def trans(atype, map): + idx = atype.flatten().tolist() + res = [] + for i in idx: + res.append(map[i]) + return res + + assert trans(atype, commonl) == trans(new_atype, originl) + + if __name__ == "__main__": unittest.main(warnings="ignore") diff --git a/source/tests/pt/model/test_pairtab_atomic_model.py b/source/tests/pt/model/test_pairtab_atomic_model.py index 0576f89910..322de51a2c 100644 --- a/source/tests/pt/model/test_pairtab_atomic_model.py +++ b/source/tests/pt/model/test_pairtab_atomic_model.py @@ -229,7 +229,7 @@ def test_extrapolation_nonzero_rmax(self, mock_loadtxt) -> None: ) model = PairTabAtomicModel( - tab_file=file_path, rcut=rcut, sel=2, type_map=["S"] + tab_file=file_path, rcut=rcut, sel=2, type_map=["H"] ) results.append( model.forward_atomic(extended_coord, extended_atype, nlist)["energy"] diff --git a/source/tests/pt/model/water/zbl.json b/source/tests/pt/model/water/zbl.json new file mode 100644 index 0000000000..cb5602d92d --- /dev/null +++ b/source/tests/pt/model/water/zbl.json @@ -0,0 +1,92 @@ +{ + "_comment1": " model parameters", + "model": { + "use_srtab": "H2O_tab_potential.txt", + "smin_alpha": 0.1, + "sw_rmin": 0.8, + "sw_rmax": 1.0, + "type_map": [ + "O", + "H" + ], + "descriptor": { + "type": "se_e2_a", + "sel": [ + 46, + 92 + ], + "rcut_smth": 0.50, + "rcut": 6.00, + "neuron": [ + 25, + 50, + 100 + ], + "resnet_dt": false, + "axis_neuron": 16, + "type_one_side": true, + "precision": "float64", + "seed": 1, + "_comment2": " that's all" + }, + "fitting_net": { + "neuron": [ + 240, + 240, + 240 + ], + "resnet_dt": true, + "precision": "float64", + "seed": 1, + "_comment3": " that's all" + }, + "_comment4": " that's all" + }, + + "learning_rate": { + "type": "exp", + "decay_steps": 5000, + "start_lr": 0.001, + "stop_lr": 3.51e-8, + "_comment5": "that's all" + }, + + "loss": { + "type": "ener", + "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, + "_comment6": " that's all" + }, + + "training": { + "training_data": { + "systems": [ + "../data/data_0/", + "../data/data_1/", + "../data/data_2/" + ], + "batch_size": "auto", + "_comment7": "that's all" + }, + "validation_data": { + "systems": [ + "../data/data_3" + ], + "batch_size": 1, + "numb_btch": 3, + "_comment8": "that's all" + }, + "numb_steps": 1000000, + "seed": 10, + "disp_file": "lcurve.out", + "disp_freq": 100, + "save_freq": 1000, + "_comment9": "that's all" + }, + + "_comment10": "that's all" +} diff --git a/source/tests/pt/test_finetune.py b/source/tests/pt/test_finetune.py index 226fed3c65..d21a44acc7 100644 --- a/source/tests/pt/test_finetune.py +++ b/source/tests/pt/test_finetune.py @@ -17,7 +17,10 @@ DeepEval, ) from deepmd.pt.model.model import ( + DPZBLModel, + EnergyModel, get_model, + get_zbl_model, ) from deepmd.utils.data_system import ( DeepmdDataSystem, @@ -27,23 +30,48 @@ ) from .model.test_permutation import ( - model_dpa1, model_dpa2, model_se_e2_a, + model_zbl, ) class FinetuneTest: def test_finetune_change_energy_bias(self): # get model - model = get_model(self.model_config) - model.fitting_net.bias_atom_e = torch.rand_like(model.fitting_net.bias_atom_e) - energy_bias_before = deepcopy( - model.fitting_net.bias_atom_e.detach().cpu().numpy().reshape(-1) - ) - bias_atom_e_input = deepcopy( - model.fitting_net.bias_atom_e.detach().cpu().numpy().reshape(-1) - ) + if "use_srtab" in self.model_config: + model = get_zbl_model(self.model_config) + else: + model = get_model(self.model_config) + if isinstance(model, EnergyModel): + model.fitting_net.bias_atom_e = torch.rand_like( + model.fitting_net.bias_atom_e + ) + energy_bias_before = deepcopy( + model.fitting_net.bias_atom_e.detach().cpu().numpy().reshape(-1) + ) + bias_atom_e_input = deepcopy( + model.fitting_net.bias_atom_e.detach().cpu().numpy().reshape(-1) + ) + elif isinstance(model, DPZBLModel): + model.dp_model.fitting_net.bias_atom_e = torch.rand_like( + model.dp_model.fitting_net.bias_atom_e + ) + energy_bias_before = deepcopy( + model.dp_model.fitting_net.bias_atom_e.detach() + .cpu() + .numpy() + .reshape(-1) + ) + bias_atom_e_input = deepcopy( + model.dp_model.fitting_net.bias_atom_e.detach() + .cpu() + .numpy() + .reshape(-1) + ) + else: + bias_atom_e_input = None + model = torch.jit.script(model) tmp_model = tempfile.NamedTemporaryFile(delete=False, suffix=".pth") torch.jit.save(model, tmp_model.name) @@ -109,7 +137,8 @@ def tearDown(self) -> None: FinetuneTest.tearDown(self) -class TestEnergyModelDPA1(unittest.TestCase, FinetuneTest): +@unittest.skip("change bias not implemented yet.") +class TestEnergyZBLModelSeA(unittest.TestCase, FinetuneTest): def setUp(self): self.data_file = [str(Path(__file__).parent / "water/data/data_0")] self.data = DeepmdDataSystem( @@ -118,7 +147,7 @@ def setUp(self): test_size=1, ) self.data.add("energy", ndof=1, atomic=False, must=True, high_prec=True) - self.model_config = model_dpa1 + self.model_config = model_zbl def tearDown(self) -> None: FinetuneTest.tearDown(self) diff --git a/source/tests/pt/test_training.py b/source/tests/pt/test_training.py index bcb8c6c188..db69a1bcea 100644 --- a/source/tests/pt/test_training.py +++ b/source/tests/pt/test_training.py @@ -21,6 +21,7 @@ model_dpa2, model_hybrid, model_se_e2_a, + model_zbl, ) @@ -66,6 +67,7 @@ def test_trainable(self): torch.testing.assert_close( model_dict_before_training[key], model_dict_after_training[key] ) + self.tearDown() def tearDown(self): @@ -94,6 +96,22 @@ def tearDown(self) -> None: DPTrainTest.tearDown(self) +class TestEnergyZBLModelSeA(unittest.TestCase, DPTrainTest): + def setUp(self): + input_json = str(Path(__file__).parent / "water/zbl.json") + with open(input_json) as f: + self.config = json.load(f) + data_file = [str(Path(__file__).parent / "water/data/data_0")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_zbl) + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + + def tearDown(self) -> None: + DPTrainTest.tearDown(self) + + class TestFparam(unittest.TestCase, DPTrainTest): """Test if `fparam` can be loaded correctly.""" From 2d48d1f2d729e2c863bc37f9b57bbb7be822f5e1 Mon Sep 17 00:00:00 2001 From: Jinzhe Zeng Date: Thu, 7 Mar 2024 06:12:04 -0500 Subject: [PATCH 5/6] pt: improve nlist performance (#3425) 1. use inv_ex instead of inv. `inv_ex` does not check errors. We can assume the input is correct. 2. pass CPU box for `torch.arange`; 3. avoid torch.tensor. --------- Signed-off-by: Jinzhe Zeng --- deepmd/pt/train/training.py | 2 +- deepmd/pt/utils/nlist.py | 33 ++++++++++++++++++--------------- deepmd/pt/utils/region.py | 2 +- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 2a80956b9d..f066c4fe37 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -961,7 +961,7 @@ def get_data(self, is_train=True, task_key="Default"): batch_data = next(iter(self.validation_data[task_key])) for key in batch_data.keys(): - if key == "sid" or key == "fid": + if key == "sid" or key == "fid" or key == "box": continue elif not isinstance(batch_data[key], list): if batch_data[key] is not None: diff --git a/deepmd/pt/utils/nlist.py b/deepmd/pt/utils/nlist.py index cfc75d9438..56a062f1b8 100644 --- a/deepmd/pt/utils/nlist.py +++ b/deepmd/pt/utils/nlist.py @@ -27,14 +27,16 @@ def extend_input_and_build_neighbor_list( ): nframes, nloc = atype.shape[:2] if box is not None: + box_gpu = box.to(coord.device, non_blocking=True) coord_normalized = normalize_coord( coord.view(nframes, nloc, 3), - box.reshape(nframes, 3, 3), + box_gpu.reshape(nframes, 3, 3), ) else: + box_gpu = None coord_normalized = coord.clone() extended_coord, extended_atype, mapping = extend_coord_with_ghosts( - coord_normalized, atype, box, rcut + coord_normalized, atype, box_gpu, rcut, box ) nlist = build_neighbor_list( extended_coord, @@ -262,6 +264,7 @@ def extend_coord_with_ghosts( atype: torch.Tensor, cell: Optional[torch.Tensor], rcut: float, + cell_cpu: Optional[torch.Tensor] = None, ): """Extend the coordinates of the atoms by appending peridoc images. The number of images is large enough to ensure all the neighbors @@ -277,6 +280,8 @@ def extend_coord_with_ghosts( simulation cell tensor of shape [-1, 9]. rcut : float the cutoff radius + cell_cpu : torch.Tensor + cell on cpu for performance Returns ------- @@ -299,27 +304,25 @@ def extend_coord_with_ghosts( else: coord = coord.view([nf, nloc, 3]) cell = cell.view([nf, 3, 3]) + cell_cpu = cell_cpu.view([nf, 3, 3]) if cell_cpu is not None else cell # nf x 3 - to_face = to_face_distance(cell) + to_face = to_face_distance(cell_cpu) # nf x 3 # *2: ghost copies on + and - directions # +1: central cell nbuff = torch.ceil(rcut / to_face).to(torch.long) # 3 nbuff = torch.max(nbuff, dim=0, keepdim=False).values - xi = torch.arange(-nbuff[0], nbuff[0] + 1, 1, device=device) - yi = torch.arange(-nbuff[1], nbuff[1] + 1, 1, device=device) - zi = torch.arange(-nbuff[2], nbuff[2] + 1, 1, device=device) - xyz = xi.view(-1, 1, 1, 1) * torch.tensor( - [1, 0, 0], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=device - ) - xyz = xyz + yi.view(1, -1, 1, 1) * torch.tensor( - [0, 1, 0], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=device - ) - xyz = xyz + zi.view(1, 1, -1, 1) * torch.tensor( - [0, 0, 1], dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=device - ) + nbuff_cpu = nbuff.cpu() + xi = torch.arange(-nbuff_cpu[0], nbuff_cpu[0] + 1, 1, device="cpu") + yi = torch.arange(-nbuff_cpu[1], nbuff_cpu[1] + 1, 1, device="cpu") + zi = torch.arange(-nbuff_cpu[2], nbuff_cpu[2] + 1, 1, device="cpu") + eye_3 = torch.eye(3, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device="cpu") + xyz = xi.view(-1, 1, 1, 1) * eye_3[0] + xyz = xyz + yi.view(1, -1, 1, 1) * eye_3[1] + xyz = xyz + zi.view(1, 1, -1, 1) * eye_3[2] xyz = xyz.view(-1, 3) + xyz = xyz.to(device=device, non_blocking=True) # ns x 3 shift_idx = xyz[torch.argsort(torch.norm(xyz, dim=1))] ns, _ = shift_idx.shape diff --git a/deepmd/pt/utils/region.py b/deepmd/pt/utils/region.py index b07d2f73bf..9d811acb9b 100644 --- a/deepmd/pt/utils/region.py +++ b/deepmd/pt/utils/region.py @@ -21,7 +21,7 @@ def phys2inter( the internal coordinates """ - rec_cell = torch.linalg.inv(cell) + rec_cell, _ = torch.linalg.inv_ex(cell) return torch.matmul(coord, rec_cell) From 09bd522fbec7a42f26886d0daed1853203372a35 Mon Sep 17 00:00:00 2001 From: Duo <50307526+iProzd@users.noreply.github.com> Date: Fri, 8 Mar 2024 01:23:51 +0800 Subject: [PATCH 6/6] Support DPSpin for AtomicModel (#3301) This PR support DPSpin for AtomicModel: - [x] `Spin` base class to handle spin-related information. - [x] `SpinModel` implementation in pt, which can generally take any backbone model, with input and output process for output like `energy`, `dipole` and etc. - [x] `SpinEnergyModel` implementation inherited from `SpinModel` for `energy` calclulation. - [x] Input improvement, now DPSpin model can take real `spin` as input with `vitual_scale`, which can support much more flexible input. (Also with dataset support for `spin` input.) - [x] `EnergySpinLoss` class to process `energy`, `force_real`, `force_mag`, `virial`. - [x] Add `protection` for environment calculations. TODO: - [x] Support `forward_lower` for `SpinModel`. - [x] Add `data_stat` with general `exclude_types` for descriptors and fittings. - [x] Add examples and UTs. - [x] numpy model - [x] Support multi-task training for `SpinModel` models. Will fix in next PRs: - tf consistency test (?) --------- Signed-off-by: Duo <50307526+iProzd@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- deepmd/backend/pytorch.py | 4 +- deepmd/dpmodel/descriptor/se_e2_a.py | 8 +- deepmd/dpmodel/descriptor/se_r.py | 6 +- deepmd/dpmodel/fitting/ener_fitting.py | 1 + deepmd/dpmodel/model/__init__.py | 4 + deepmd/dpmodel/model/model.py | 58 +- deepmd/dpmodel/model/spin_model.py | 394 ++++++++++++ deepmd/dpmodel/output_def.py | 72 ++- deepmd/dpmodel/utils/env_mat.py | 14 +- deepmd/dpmodel/utils/exclude_mask.py | 9 + deepmd/dpmodel/utils/nlist.py | 2 + deepmd/entrypoints/test.py | 84 ++- deepmd/infer/deep_eval.py | 19 +- deepmd/infer/deep_pot.py | 28 +- deepmd/pt/infer/deep_eval.py | 174 +++++- deepmd/pt/loss/__init__.py | 4 + deepmd/pt/loss/ener_spin.py | 245 ++++++++ .../pt/model/atomic_model/dp_atomic_model.py | 19 +- .../atomic_model/pairtab_atomic_model.py | 2 +- deepmd/pt/model/descriptor/dpa1.py | 8 +- deepmd/pt/model/descriptor/dpa2.py | 10 + deepmd/pt/model/descriptor/env_mat.py | 20 +- deepmd/pt/model/descriptor/repformers.py | 17 + deepmd/pt/model/descriptor/se_a.py | 6 + deepmd/pt/model/descriptor/se_atten.py | 17 + deepmd/pt/model/descriptor/se_r.py | 14 +- deepmd/pt/model/model/__init__.py | 51 +- deepmd/pt/model/model/dp_zbl_model.py | 7 +- deepmd/pt/model/model/spin_model.py | 560 ++++++++++++++++++ deepmd/pt/model/task/ener.py | 2 +- deepmd/pt/model/task/fitting.py | 6 +- deepmd/pt/train/training.py | 28 +- deepmd/pt/train/wrapper.py | 24 +- deepmd/pt/utils/env_mat_stat.py | 19 +- deepmd/pt/utils/exclude_mask.py | 11 +- deepmd/pt/utils/multi_task.py | 6 +- deepmd/pt/utils/nlist.py | 3 +- deepmd/pt/utils/stat.py | 16 +- deepmd/tf/descriptor/se_a.py | 7 + deepmd/tf/descriptor/se_r.py | 5 + deepmd/tf/infer/deep_eval.py | 4 + deepmd/utils/argcheck.py | 36 +- deepmd/utils/data.py | 2 +- deepmd/utils/spin.py | 199 +++++++ .../spin/data_reformat/data_0/set.000/box.npy | Bin 0 -> 4448 bytes .../data_reformat/data_0/set.000/coord.npy | Bin 0 -> 46208 bytes .../data_reformat/data_0/set.000/energy.npy | Bin 0 -> 608 bytes .../data_reformat/data_0/set.000/force.npy | Bin 0 -> 46208 bytes .../data_0/set.000/force_mag.npy | Bin 0 -> 46208 bytes .../data_reformat/data_0/set.000/spin.npy | Bin 0 -> 46208 bytes examples/spin/data_reformat/data_0/type.raw | 32 + .../spin/data_reformat/data_0/type_map.raw | 2 + .../spin/data_reformat/data_1/set.000/box.npy | Bin 0 -> 4448 bytes .../data_reformat/data_1/set.000/coord.npy | Bin 0 -> 46208 bytes .../data_reformat/data_1/set.000/energy.npy | Bin 0 -> 608 bytes .../data_reformat/data_1/set.000/force.npy | Bin 0 -> 46208 bytes .../data_1/set.000/force_mag.npy | Bin 0 -> 46208 bytes .../data_reformat/data_1/set.000/spin.npy | Bin 0 -> 46208 bytes examples/spin/data_reformat/data_1/type.raw | 32 + .../spin/data_reformat/data_1/type_map.raw | 2 + .../spin/data_reformat/data_2/set.000/box.npy | Bin 0 -> 2360 bytes .../data_reformat/data_2/set.000/coord.npy | Bin 0 -> 23936 bytes .../data_reformat/data_2/set.000/energy.npy | Bin 0 -> 376 bytes .../data_reformat/data_2/set.000/force.npy | Bin 0 -> 23936 bytes .../data_2/set.000/force_mag.npy | Bin 0 -> 23936 bytes .../data_reformat/data_2/set.000/spin.npy | Bin 0 -> 23936 bytes examples/spin/data_reformat/data_2/type.raw | 32 + .../spin/data_reformat/data_2/type_map.raw | 2 + .../se_e2_a/{input.json => input_tf.json} | 0 examples/spin/se_e2_a/input_torch.json | 90 +++ .../tests/common/dpmodel/test_output_def.py | 156 ++++- source/tests/common/test_examples.py | 3 +- source/tests/common/test_spin.py | 172 ++++++ .../consistent/descriptor/test_se_e2_a.py | 19 + .../tests/pt/NiO/data/data_0/set.000/box.npy | Bin 0 -> 4448 bytes .../pt/NiO/data/data_0/set.000/coord.npy | Bin 0 -> 46208 bytes .../pt/NiO/data/data_0/set.000/energy.npy | Bin 0 -> 608 bytes .../pt/NiO/data/data_0/set.000/force.npy | Bin 0 -> 46208 bytes .../pt/NiO/data/data_0/set.000/force_mag.npy | Bin 0 -> 46208 bytes .../tests/pt/NiO/data/data_0/set.000/spin.npy | Bin 0 -> 46208 bytes source/tests/pt/NiO/data/data_0/type.raw | 32 + source/tests/pt/NiO/data/data_0/type_map.raw | 2 + .../tests/pt/NiO/data/single/set.000/box.npy | Bin 0 -> 200 bytes .../pt/NiO/data/single/set.000/coord.npy | Bin 0 -> 896 bytes .../pt/NiO/data/single/set.000/energy.npy | Bin 0 -> 136 bytes .../pt/NiO/data/single/set.000/force.npy | Bin 0 -> 896 bytes .../pt/NiO/data/single/set.000/force_mag.npy | Bin 0 -> 896 bytes .../tests/pt/NiO/data/single/set.000/spin.npy | Bin 0 -> 896 bytes source/tests/pt/NiO/data/single/type.raw | 32 + source/tests/pt/NiO/data/single/type_map.raw | 2 + source/tests/pt/model/test_autodiff.py | 86 ++- source/tests/pt/model/test_deeppot.py | 3 +- source/tests/pt/model/test_embedding_net.py | 15 +- source/tests/pt/model/test_ener_spin_model.py | 420 +++++++++++++ source/tests/pt/model/test_forward_lower.py | 177 ++++++ source/tests/pt/model/test_null_input.py | 22 +- source/tests/pt/model/test_permutation.py | 94 ++- source/tests/pt/model/test_rot.py | 136 +++-- source/tests/pt/model/test_smooth.py | 106 ++-- source/tests/pt/model/test_trans.py | 62 +- source/tests/pt/model/test_unused_params.py | 11 +- source/tests/pt/test_dp_test.py | 136 ++++- source/tests/pt/test_init_frz_model.py | 6 +- source/tests/pt/test_loss.py | 215 ++++++- source/tests/pt/test_stat.py | 4 +- source/tests/tf/test_deeppot_a.py | 4 +- 106 files changed, 3959 insertions(+), 373 deletions(-) create mode 100644 deepmd/dpmodel/model/spin_model.py create mode 100644 deepmd/pt/loss/ener_spin.py create mode 100644 deepmd/pt/model/model/spin_model.py create mode 100644 deepmd/utils/spin.py create mode 100644 examples/spin/data_reformat/data_0/set.000/box.npy create mode 100644 examples/spin/data_reformat/data_0/set.000/coord.npy create mode 100644 examples/spin/data_reformat/data_0/set.000/energy.npy create mode 100644 examples/spin/data_reformat/data_0/set.000/force.npy create mode 100644 examples/spin/data_reformat/data_0/set.000/force_mag.npy create mode 100644 examples/spin/data_reformat/data_0/set.000/spin.npy create mode 100644 examples/spin/data_reformat/data_0/type.raw create mode 100644 examples/spin/data_reformat/data_0/type_map.raw create mode 100644 examples/spin/data_reformat/data_1/set.000/box.npy create mode 100644 examples/spin/data_reformat/data_1/set.000/coord.npy create mode 100644 examples/spin/data_reformat/data_1/set.000/energy.npy create mode 100644 examples/spin/data_reformat/data_1/set.000/force.npy create mode 100644 examples/spin/data_reformat/data_1/set.000/force_mag.npy create mode 100644 examples/spin/data_reformat/data_1/set.000/spin.npy create mode 100644 examples/spin/data_reformat/data_1/type.raw create mode 100644 examples/spin/data_reformat/data_1/type_map.raw create mode 100644 examples/spin/data_reformat/data_2/set.000/box.npy create mode 100644 examples/spin/data_reformat/data_2/set.000/coord.npy create mode 100644 examples/spin/data_reformat/data_2/set.000/energy.npy create mode 100644 examples/spin/data_reformat/data_2/set.000/force.npy create mode 100644 examples/spin/data_reformat/data_2/set.000/force_mag.npy create mode 100644 examples/spin/data_reformat/data_2/set.000/spin.npy create mode 100644 examples/spin/data_reformat/data_2/type.raw create mode 100644 examples/spin/data_reformat/data_2/type_map.raw rename examples/spin/se_e2_a/{input.json => input_tf.json} (100%) create mode 100644 examples/spin/se_e2_a/input_torch.json create mode 100644 source/tests/common/test_spin.py create mode 100644 source/tests/pt/NiO/data/data_0/set.000/box.npy create mode 100644 source/tests/pt/NiO/data/data_0/set.000/coord.npy create mode 100644 source/tests/pt/NiO/data/data_0/set.000/energy.npy create mode 100644 source/tests/pt/NiO/data/data_0/set.000/force.npy create mode 100644 source/tests/pt/NiO/data/data_0/set.000/force_mag.npy create mode 100644 source/tests/pt/NiO/data/data_0/set.000/spin.npy create mode 100644 source/tests/pt/NiO/data/data_0/type.raw create mode 100644 source/tests/pt/NiO/data/data_0/type_map.raw create mode 100644 source/tests/pt/NiO/data/single/set.000/box.npy create mode 100644 source/tests/pt/NiO/data/single/set.000/coord.npy create mode 100644 source/tests/pt/NiO/data/single/set.000/energy.npy create mode 100644 source/tests/pt/NiO/data/single/set.000/force.npy create mode 100644 source/tests/pt/NiO/data/single/set.000/force_mag.npy create mode 100644 source/tests/pt/NiO/data/single/set.000/spin.npy create mode 100644 source/tests/pt/NiO/data/single/type.raw create mode 100644 source/tests/pt/NiO/data/single/type_map.raw create mode 100644 source/tests/pt/model/test_ener_spin_model.py create mode 100644 source/tests/pt/model/test_forward_lower.py diff --git a/deepmd/backend/pytorch.py b/deepmd/backend/pytorch.py index 676694172b..fb7d30e994 100644 --- a/deepmd/backend/pytorch.py +++ b/deepmd/backend/pytorch.py @@ -29,8 +29,8 @@ @Backend.register("pt") @Backend.register("pytorch") -class TensorFlowBackend(Backend): - """TensorFlow backend.""" +class PyTorchBackend(Backend): + """PyTorch backend.""" name = "PyTorch" """The formal name of the backend.""" diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index a068a2e366..531aa09f3a 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -111,6 +111,8 @@ class DescrptSeA(NativeOP, BaseDescriptor): exclude_types : List[List[int]] The excluded pairs of types which have no interaction with each other. For example, `[[0, 1]]` means no interaction between type 0 and type 1. + env_protection: float + Protection parameter to prevent division by zero errors during environment matrix calculations. set_davg_zero Set the shift of embedding net input to zero. activation_function @@ -149,6 +151,7 @@ def __init__( trainable: bool = True, type_one_side: bool = True, exclude_types: List[List[int]] = [], + env_protection: float = 0.0, set_davg_zero: bool = False, activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, @@ -169,6 +172,7 @@ def __init__( self.resnet_dt = resnet_dt self.trainable = trainable self.type_one_side = type_one_side + self.env_protection = env_protection self.set_davg_zero = set_davg_zero self.activation_function = activation_function self.precision = precision @@ -192,7 +196,7 @@ def __init__( self.resnet_dt, self.precision, ) - self.env_mat = EnvMat(self.rcut, self.rcut_smth) + self.env_mat = EnvMat(self.rcut, self.rcut_smth, protection=self.env_protection) self.nnei = np.sum(self.sel) self.davg = np.zeros( [self.ntypes, self.nnei, 4], dtype=PRECISION_DICT[self.precision] @@ -378,6 +382,7 @@ def serialize(self) -> dict: "trainable": self.trainable, "type_one_side": self.type_one_side, "exclude_types": self.exclude_types, + "env_protection": self.env_protection, "set_davg_zero": self.set_davg_zero, "activation_function": self.activation_function, # make deterministic @@ -406,7 +411,6 @@ def deserialize(cls, data: dict) -> "DescrptSeA": obj["davg"] = variables["davg"] obj["dstd"] = variables["dstd"] obj.embeddings = NetworkCollection.deserialize(embeddings) - obj.env_mat = EnvMat.deserialize(env_mat) return obj @classmethod diff --git a/deepmd/dpmodel/descriptor/se_r.py b/deepmd/dpmodel/descriptor/se_r.py index 2dbf495d14..3128a28493 100644 --- a/deepmd/dpmodel/descriptor/se_r.py +++ b/deepmd/dpmodel/descriptor/se_r.py @@ -106,6 +106,7 @@ def __init__( trainable: bool = True, type_one_side: bool = True, exclude_types: List[List[int]] = [], + env_protection: float = 0.0, set_davg_zero: bool = False, activation_function: str = "tanh", precision: str = DEFAULT_PRECISION, @@ -133,6 +134,7 @@ def __init__( self.precision = precision self.spin = spin self.emask = PairExcludeMask(self.ntypes, self.exclude_types) + self.env_protection = env_protection in_dim = 1 # not considiering type embedding self.embeddings = NetworkCollection( @@ -150,7 +152,7 @@ def __init__( self.resnet_dt, self.precision, ) - self.env_mat = EnvMat(self.rcut, self.rcut_smth) + self.env_mat = EnvMat(self.rcut, self.rcut_smth, protection=self.env_protection) self.nnei = np.sum(self.sel) self.davg = np.zeros( [self.ntypes, self.nnei, 1], dtype=PRECISION_DICT[self.precision] @@ -305,6 +307,7 @@ def serialize(self) -> dict: "trainable": self.trainable, "type_one_side": self.type_one_side, "exclude_types": self.exclude_types, + "env_protection": self.env_protection, "set_davg_zero": self.set_davg_zero, "activation_function": self.activation_function, # make deterministic @@ -333,7 +336,6 @@ def deserialize(cls, data: dict) -> "DescrptSeR": obj["davg"] = variables["davg"] obj["dstd"] = variables["dstd"] obj.embeddings = NetworkCollection.deserialize(embeddings) - obj.env_mat = EnvMat.deserialize(env_mat) return obj @classmethod diff --git a/deepmd/dpmodel/fitting/ener_fitting.py b/deepmd/dpmodel/fitting/ener_fitting.py index de41bebf6d..3a0e9909b9 100644 --- a/deepmd/dpmodel/fitting/ener_fitting.py +++ b/deepmd/dpmodel/fitting/ener_fitting.py @@ -63,6 +63,7 @@ def __init__( use_aparam_as_mask=use_aparam_as_mask, spin=spin, mixed_types=mixed_types, + exclude_types=exclude_types, ) @classmethod diff --git a/deepmd/dpmodel/model/__init__.py b/deepmd/dpmodel/model/__init__.py index dda174fa4e..cb796e6d35 100644 --- a/deepmd/dpmodel/model/__init__.py +++ b/deepmd/dpmodel/model/__init__.py @@ -16,8 +16,12 @@ from .make_model import ( make_model, ) +from .spin_model import ( + SpinModel, +) __all__ = [ "DPModel", + "SpinModel", "make_model", ] diff --git a/deepmd/dpmodel/model/model.py b/deepmd/dpmodel/model/model.py index 6f06785c56..3fdf5b802b 100644 --- a/deepmd/dpmodel/model/model.py +++ b/deepmd/dpmodel/model/model.py @@ -8,10 +8,16 @@ from deepmd.dpmodel.model.dp_model import ( DPModel, ) +from deepmd.dpmodel.model.spin_model import ( + SpinModel, +) +from deepmd.utils.spin import ( + Spin, +) -def get_model(data: dict) -> DPModel: - """Get a DPModel from a dictionary. +def get_standard_model(data: dict) -> DPModel: + """Get a standard DPModel from a dictionary. Parameters ---------- @@ -30,6 +36,7 @@ def get_model(data: dict) -> DPModel: fitting = EnergyFittingNet( ntypes=descriptor.get_ntypes(), dim_descrpt=descriptor.get_dim_out(), + mixed_types=descriptor.mixed_types(), **data["fitting_net"], ) else: @@ -41,3 +48,50 @@ def get_model(data: dict) -> DPModel: atom_exclude_types=data.get("atom_exclude_types", []), pair_exclude_types=data.get("pair_exclude_types", []), ) + + +def get_spin_model(data: dict) -> SpinModel: + """Get a spin model from a dictionary. + + Parameters + ---------- + data : dict + The data to construct the model. + """ + # include virtual spin and placeholder types + data["type_map"] += [item + "_spin" for item in data["type_map"]] + spin = Spin( + use_spin=data["spin"]["use_spin"], + virtual_scale=data["spin"]["virtual_scale"], + ) + pair_exclude_types = spin.get_pair_exclude_types( + exclude_types=data.get("pair_exclude_types", None) + ) + data["pair_exclude_types"] = pair_exclude_types + # for descriptor data stat + data["descriptor"]["exclude_types"] = pair_exclude_types + atom_exclude_types = spin.get_atom_exclude_types( + exclude_types=data.get("atom_exclude_types", None) + ) + data["atom_exclude_types"] = atom_exclude_types + if "env_protection" not in data["descriptor"]: + data["descriptor"]["env_protection"] = 1e-6 + if data["descriptor"]["type"] in ["se_e2_a"]: + # only expand sel for se_e2_a + data["descriptor"]["sel"] += data["descriptor"]["sel"] + backbone_model = get_standard_model(data) + return SpinModel(backbone_model=backbone_model, spin=spin) + + +def get_model(data: dict): + """Get a model from a dictionary. + + Parameters + ---------- + data : dict + The data to construct the model. + """ + if "spin" in data: + return get_spin_model(data) + else: + return get_standard_model(data) diff --git a/deepmd/dpmodel/model/spin_model.py b/deepmd/dpmodel/model/spin_model.py new file mode 100644 index 0000000000..5b31b64fdf --- /dev/null +++ b/deepmd/dpmodel/model/spin_model.py @@ -0,0 +1,394 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + Dict, + List, + Optional, +) + +import numpy as np + +from deepmd.dpmodel.model.dp_model import ( + DPModel, +) +from deepmd.utils.spin import ( + Spin, +) + + +class SpinModel: + """A spin model wrapper, with spin input preprocess and output split.""" + + def __init__( + self, + backbone_model, + spin: Spin, + ): + super().__init__() + self.backbone_model = backbone_model + self.spin = spin + self.ntypes_real = self.spin.ntypes_real + self.virtual_scale_mask = self.spin.get_virtual_scale_mask() + self.spin_mask = self.spin.get_spin_mask() + + def process_spin_input(self, coord, atype, spin): + """Generate virtual coordinates and types, concat into the input.""" + nframes, nloc = coord.shape[:-1] + atype_spin = np.concatenate([atype, atype + self.ntypes_real], axis=-1) + virtual_coord = coord + spin * self.virtual_scale_mask[atype].reshape( + [nframes, nloc, 1] + ) + coord_spin = np.concatenate([coord, virtual_coord], axis=-2) + return coord_spin, atype_spin + + def process_spin_input_lower( + self, + extended_coord: np.ndarray, + extended_atype: np.ndarray, + extended_spin: np.ndarray, + nlist: np.ndarray, + mapping: Optional[np.ndarray] = None, + ): + """ + Add `extended_spin` into `extended_coord` to generate virtual atoms, and extend `nlist` and `mapping`. + Note that the final `extended_coord_updated` with shape [nframes, nall + nall, 3] has the following order: + - [:, :nloc]: original nloc real atoms. + - [:, nloc: nloc + nloc]: virtual atoms corresponding to nloc real atoms. + - [:, nloc + nloc: nloc + nall]: ghost real atoms. + - [:, nloc + nall: nall + nall]: virtual atoms corresponding to ghost real atoms. + """ + nframes, nall = extended_coord.shape[:2] + nloc = nlist.shape[1] + virtual_extended_coord = ( + extended_coord + + extended_spin + * self.virtual_scale_mask[extended_atype].reshape([nframes, nall, 1]) + ) + virtual_extended_atype = extended_atype + self.ntypes_real + extended_coord_updated = self.concat_switch_virtual( + extended_coord, virtual_extended_coord, nloc + ) + extended_atype_updated = self.concat_switch_virtual( + extended_atype, virtual_extended_atype, nloc + ) + if mapping is not None: + virtual_mapping = mapping + nloc + mapping_updated = self.concat_switch_virtual(mapping, virtual_mapping, nloc) + else: + mapping_updated = None + # extend the nlist + nlist_updated = self.extend_nlist(extended_atype, nlist) + return ( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping_updated, + ) + + def process_spin_output( + self, atype, out_tensor, add_mag: bool = True, virtual_scale: bool = True + ): + """Split the output both real and virtual atoms, and scale the latter.""" + nframes, nloc_double = out_tensor.shape[:2] + nloc = nloc_double // 2 + if virtual_scale: + virtual_scale_mask = self.virtual_scale_mask + else: + virtual_scale_mask = self.spin_mask + atomic_mask = virtual_scale_mask[atype].reshape([nframes, nloc, 1]) + out_real, out_mag = np.split(out_tensor, [nloc], axis=1) + if add_mag: + out_real = out_real + out_mag + out_mag = (out_mag.reshape([nframes, nloc, -1]) * atomic_mask).reshape( + out_mag.shape + ) + return out_real, out_mag, atomic_mask > 0.0 + + def process_spin_output_lower( + self, + extended_atype, + extended_out_tensor, + nloc: int, + add_mag: bool = True, + virtual_scale: bool = True, + ): + """Split the extended output of both real and virtual atoms with switch, and scale the latter.""" + nframes, nall_double = extended_out_tensor.shape[:2] + nall = nall_double // 2 + if virtual_scale: + virtual_scale_mask = self.virtual_scale_mask + else: + virtual_scale_mask = self.spin_mask + atomic_mask = virtual_scale_mask[extended_atype].reshape([nframes, nall, 1]) + extended_out_real = np.concatenate( + [ + extended_out_tensor[:, :nloc], + extended_out_tensor[:, nloc + nloc : nloc + nall], + ], + axis=1, + ) + extended_out_mag = np.concatenate( + [ + extended_out_tensor[:, nloc : nloc + nloc], + extended_out_tensor[:, nloc + nall :], + ], + axis=1, + ) + if add_mag: + extended_out_real = extended_out_real + extended_out_mag + extended_out_mag = ( + extended_out_mag.reshape([nframes, nall, -1]) * atomic_mask + ).reshape(extended_out_mag.shape) + return extended_out_real, extended_out_mag, atomic_mask > 0.0 + + @staticmethod + def extend_nlist(extended_atype, nlist): + nframes, nloc, nnei = nlist.shape + nall = extended_atype.shape[1] + nlist_mask = nlist != -1 + nlist[nlist == -1] = 0 + nlist_shift = nlist + nall + nlist[~nlist_mask] = -1 + nlist_shift[~nlist_mask] = -1 + self_spin = np.arange(0, nloc, dtype=nlist.dtype) + nall + self_spin = self_spin.reshape(1, -1, 1).repeat(nframes, axis=0) + # self spin + real neighbor + virtual neighbor + # nf x nloc x (1 + nnei + nnei) + extended_nlist = np.concatenate([self_spin, nlist, nlist_shift], axis=-1) + # nf x (nloc + nloc) x (1 + nnei + nnei) + extended_nlist = np.concatenate( + [extended_nlist, -1 * np.ones_like(extended_nlist)], axis=-2 + ) + # update the index for switch + first_part_index = (nloc <= extended_nlist) & (extended_nlist < nall) + second_part_index = (nall <= extended_nlist) & (extended_nlist < (nall + nloc)) + extended_nlist[first_part_index] += nloc + extended_nlist[second_part_index] -= nall - nloc + return extended_nlist + + @staticmethod + def concat_switch_virtual(extended_tensor, extended_tensor_virtual, nloc: int): + nframes, nall = extended_tensor.shape[:2] + out_shape = list(extended_tensor.shape) + out_shape[1] *= 2 + extended_tensor_updated = np.zeros( + out_shape, + dtype=extended_tensor.dtype, + ) + extended_tensor_updated[:, :nloc] = extended_tensor[:, :nloc] + extended_tensor_updated[:, nloc : nloc + nloc] = extended_tensor_virtual[ + :, :nloc + ] + extended_tensor_updated[:, nloc + nloc : nloc + nall] = extended_tensor[ + :, nloc: + ] + extended_tensor_updated[:, nloc + nall :] = extended_tensor_virtual[:, nloc:] + return extended_tensor_updated.reshape(out_shape) + + def get_type_map(self) -> List[str]: + """Get the type map.""" + tmap = self.backbone_model.get_type_map() + ntypes = len(tmap) // 2 # ignore the virtual type + return tmap[:ntypes] + + def get_rcut(self): + """Get the cut-off radius.""" + return self.backbone_model.get_rcut() + + def get_dim_fparam(self): + """Get the number (dimension) of frame parameters of this atomic model.""" + return self.backbone_model.get_dim_fparam() + + def get_dim_aparam(self): + """Get the number (dimension) of atomic parameters of this atomic model.""" + return self.backbone_model.get_dim_aparam() + + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return self.backbone_model.get_sel_type() + + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + If False, the shape is (nframes, nloc, ndim). + """ + return self.backbone_model.is_aparam_nall() + + def model_output_type(self) -> List[str]: + """Get the output type for the model.""" + return self.backbone_model.model_output_type() + + def get_model_def_script(self) -> str: + """Get the model definition script.""" + return self.backbone_model.get_model_def_script() + + def get_nnei(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + # for C++ interface + if not self.backbone_model.mixed_types(): + return self.backbone_model.get_nnei() // 2 # ignore the virtual selected + else: + return self.backbone_model.get_nnei() + + def get_nsel(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + if not self.backbone_model.mixed_types(): + return self.backbone_model.get_nsel() // 2 # ignore the virtual selected + else: + return self.backbone_model.get_nsel() + + @staticmethod + def has_spin() -> bool: + """Returns whether it has spin input and output.""" + return True + + def __getattr__(self, name): + """Get attribute from the wrapped model.""" + if name in self.__dict__: + return self.__dict__[name] + else: + return getattr(self.backbone_model, name) + + def serialize(self) -> dict: + return { + "backbone_model": self.backbone_model.serialize(), + "spin": self.spin.serialize(), + } + + @classmethod + def deserialize(cls, data) -> "SpinModel": + backbone_model_obj = DPModel.deserialize(data["backbone_model"]) + spin = Spin.deserialize(data["spin"]) + return cls( + backbone_model=backbone_model_obj, + spin=spin, + ) + + def call( + self, + coord, + atype, + spin, + box: Optional[np.ndarray] = None, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, np.ndarray]: + """Return model prediction. + + Parameters + ---------- + coord + The coordinates of the atoms. + shape: nf x (nloc x 3) + atype + The type of atoms. shape: nf x nloc + spin + The spins of the atoms. + shape: nf x (nloc x 3) + box + The simulation box. shape: nf x 9 + fparam + frame parameter. nf x ndf + aparam + atomic parameter. nf x nloc x nda + do_atomic_virial + If calculate the atomic virial. + + Returns + ------- + ret_dict + The result dict of type Dict[str,np.ndarray]. + The keys are defined by the `ModelOutputDef`. + + """ + nframes, nloc = coord.shape[:2] + coord_updated, atype_updated = self.process_spin_input(coord, atype, spin) + model_predict = self.backbone_model.call( + coord_updated, + atype_updated, + box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + model_output_type = self.backbone_model.model_output_type() + if "mask" in model_output_type: + model_output_type.pop(model_output_type.index("mask")) + var_name = model_output_type[0] + model_predict[f"{var_name}"] = np.split( + model_predict[f"{var_name}"], [nloc], axis=1 + )[0] + # for now omit the grad output + return model_predict + + def call_lower( + self, + extended_coord: np.ndarray, + extended_atype: np.ndarray, + extended_spin: np.ndarray, + nlist: np.ndarray, + mapping: Optional[np.ndarray] = None, + fparam: Optional[np.ndarray] = None, + aparam: Optional[np.ndarray] = None, + do_atomic_virial: bool = False, + ): + """Return model prediction. Lower interface that takes + extended atomic coordinates, types and spins, nlist, and mapping + as input, and returns the predictions on the extended region. + The predictions are not reduced. + + Parameters + ---------- + extended_coord + coordinates in extended region. nf x (nall x 3). + extended_atype + atomic type in extended region. nf x nall. + extended_spin + spins in extended region. nf x (nall x 3). + nlist + neighbor list. nf x nloc x nsel. + mapping + maps the extended indices to local indices. nf x nall. + fparam + frame parameter. nf x ndf + aparam + atomic parameter. nf x nloc x nda + do_atomic_virial + whether calculate atomic virial + + Returns + ------- + result_dict + the result dict, defined by the `FittingOutputDef`. + + """ + nframes, nloc = nlist.shape[:2] + ( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping_updated, + ) = self.process_spin_input_lower( + extended_coord, extended_atype, extended_spin, nlist, mapping=mapping + ) + model_predict = self.backbone_model.call_lower( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping=mapping_updated, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + model_output_type = self.backbone_model.model_output_type() + if "mask" in model_output_type: + model_output_type.pop(model_output_type.index("mask")) + var_name = model_output_type[0] + model_predict[f"{var_name}"] = np.split( + model_predict[f"{var_name}"], [nloc], axis=1 + )[0] + # for now omit the grad output + return model_predict diff --git a/deepmd/dpmodel/output_def.py b/deepmd/dpmodel/output_def.py index ac41513246..cbebb4908a 100644 --- a/deepmd/dpmodel/output_def.py +++ b/deepmd/dpmodel/output_def.py @@ -125,6 +125,8 @@ class OutputVariableOperation(IntEnum): """Derivative w.r.t. cell.""" _SEC_DERV_R = 8 """Second derivative w.r.t. coordinates.""" + MAG = 16 + """Magnetic output.""" class OutputVariableCategory(IntEnum): @@ -142,6 +144,10 @@ class OutputVariableCategory(IntEnum): """Virial, the transposed negative gradient with cell tensor times cell tensor, see eq 40 JCP 159, 054801 (2023). """ DERV_R_DERV_R = OutputVariableOperation.DERV_R | OutputVariableOperation._SEC_DERV_R """Hession matrix, the second derivative w.r.t. coordinates.""" + DERV_R_MAG = OutputVariableOperation.DERV_R | OutputVariableOperation.MAG + """Magnetic part of negative derivative w.r.t. coordinates. (e.g. magnetic force)""" + DERV_C_MAG = OutputVariableOperation.DERV_C | OutputVariableOperation.MAG + """Magnetic part of atomic component of the virial.""" class OutputVariableDef: @@ -176,8 +182,10 @@ class OutputVariableDef: If the variable is defined for each atom. category : int The category of the output variable. - hessian : bool + r_hessian : bool If hessian is requred + magnetic : bool + If the derivatives of variable have magnetic parts. """ def __init__( @@ -190,6 +198,7 @@ def __init__( atomic: bool = True, category: int = OutputVariableCategory.OUT.value, r_hessian: bool = False, + magnetic: bool = False, ): self.name = name self.shape = list(shape) @@ -208,6 +217,7 @@ def __init__( raise ValueError("a reduciable variable should be atomic") self.category = category self.r_hessian = r_hessian + self.magnetic = magnetic if self.r_hessian: if not self.reduciable: raise ValueError("only reduciable variable can calculate hessian") @@ -271,6 +281,7 @@ def __init__( self.def_derv_r, self.def_derv_c = do_derivative(self.def_outp.get_data()) self.def_hess_r, _ = do_derivative(self.def_derv_r) self.def_derv_c_redu = do_reduce(self.def_derv_c) + self.def_mask = do_mask(self.def_outp.get_data()) self.var_defs: Dict[str, OutputVariableDef] = {} for ii in [ self.def_outp.get_data(), @@ -279,6 +290,7 @@ def __init__( self.def_derv_r, self.def_derv_c_redu, self.def_hess_r, + self.def_mask, ]: self.var_defs.update(ii) @@ -324,12 +336,16 @@ def get_deriv_name(name: str) -> Tuple[str, str]: return name + "_derv_r", name + "_derv_c" +def get_deriv_name_mag(name: str) -> Tuple[str, str]: + return name + "_derv_r_mag", name + "_derv_c_mag" + + def get_hessian_name(name: str) -> str: return name + "_derv_r_derv_r" def apply_operation(var_def: OutputVariableDef, op: OutputVariableOperation) -> int: - """Apply a operation to the category of a variable definition. + """Apply an operation to the category of a variable definition. Parameters ---------- @@ -401,6 +417,31 @@ def do_reduce( return def_redu +def do_mask( + def_outp_data: Dict[str, OutputVariableDef], +) -> Dict[str, OutputVariableDef]: + def_mask: Dict[str, OutputVariableDef] = {} + # for deep eval when has atomic mask + def_mask["mask"] = OutputVariableDef( + name="mask", + shape=[1], + reduciable=False, + r_differentiable=False, + c_differentiable=False, + ) + for kk, vv in def_outp_data.items(): + if vv.magnetic: + # for deep eval when has atomic mask for magnetic atoms + def_mask["mask_mag"] = OutputVariableDef( + name="mask_mag", + shape=[1], + reduciable=False, + r_differentiable=False, + c_differentiable=False, + ) + return def_mask + + def do_derivative( def_outp_data: Dict[str, OutputVariableDef], ) -> Tuple[Dict[str, OutputVariableDef], Dict[str, OutputVariableDef]]: @@ -408,6 +449,7 @@ def do_derivative( def_derv_c: Dict[str, OutputVariableDef] = {} for kk, vv in def_outp_data.items(): rkr, rkc = get_deriv_name(kk) + rkrm, rkcm = get_deriv_name_mag(kk) if vv.r_differentiable: def_derv_r[rkr] = OutputVariableDef( rkr, @@ -420,9 +462,22 @@ def do_derivative( atomic=True, category=apply_operation(vv, OutputVariableOperation.DERV_R), ) + if vv.magnetic: + def_derv_r[rkrm] = OutputVariableDef( + rkrm, + vv.shape + [3], # noqa: RUF005 + reduciable=False, + r_differentiable=( + vv.r_hessian and vv.category == OutputVariableCategory.OUT.value + ), + c_differentiable=False, + atomic=True, + category=apply_operation(vv, OutputVariableOperation.DERV_R), + magnetic=True, + ) + if vv.c_differentiable: assert vv.r_differentiable - rkr, rkc = get_deriv_name(kk) def_derv_c[rkc] = OutputVariableDef( rkc, vv.shape + [9], # noqa: RUF005 @@ -432,4 +487,15 @@ def do_derivative( atomic=True, category=apply_operation(vv, OutputVariableOperation.DERV_C), ) + if vv.magnetic: + def_derv_r[rkcm] = OutputVariableDef( + rkcm, + vv.shape + [9], # noqa: RUF005 + reduciable=True, + r_differentiable=False, + c_differentiable=False, + atomic=True, + category=apply_operation(vv, OutputVariableOperation.DERV_C), + magnetic=True, + ) return def_derv_r, def_derv_c diff --git a/deepmd/dpmodel/utils/env_mat.py b/deepmd/dpmodel/utils/env_mat.py index 5fb4ac4107..0c2ca43c40 100644 --- a/deepmd/dpmodel/utils/env_mat.py +++ b/deepmd/dpmodel/utils/env_mat.py @@ -33,6 +33,7 @@ def _make_env_mat( rcut: float, ruct_smth: float, radial_only: bool = False, + protection: float = 0.0, ): """Make smooth environment matrix.""" nf, nloc, nnei = nlist.shape @@ -53,8 +54,8 @@ def _make_env_mat( length = np.linalg.norm(diff, axis=-1, keepdims=True) # for index 0 nloc atom length = length + ~np.expand_dims(mask, -1) - t0 = 1 / length - t1 = diff / length**2 + t0 = 1 / (length + protection) + t1 = diff / (length + protection) ** 2 weight = compute_smooth_weight(length, ruct_smth, rcut) weight = weight * np.expand_dims(mask, -1) if radial_only: @@ -69,9 +70,11 @@ def __init__( self, rcut, rcut_smth, + protection: float = 0.0, ): self.rcut = rcut self.rcut_smth = rcut_smth + self.protection = protection def call( self, @@ -120,7 +123,12 @@ def call( def _call(self, nlist, coord_ext, radial_only): em, diff, ww = _make_env_mat( - nlist, coord_ext, self.rcut, self.rcut_smth, radial_only + nlist, + coord_ext, + self.rcut, + self.rcut_smth, + radial_only=radial_only, + protection=self.protection, ) return em, ww diff --git a/deepmd/dpmodel/utils/exclude_mask.py b/deepmd/dpmodel/utils/exclude_mask.py index 360f190e13..ff668b8153 100644 --- a/deepmd/dpmodel/utils/exclude_mask.py +++ b/deepmd/dpmodel/utils/exclude_mask.py @@ -24,6 +24,12 @@ def __init__( # (ntypes) self.type_mask = self.type_mask.reshape([-1]) + def get_exclude_types(self): + return self.exclude_types + + def get_type_mask(self): + return self.type_mask + def build_type_exclude_mask( self, atype: np.ndarray, @@ -75,6 +81,9 @@ def __init__( # (ntypes+1 x ntypes+1) self.type_mask = self.type_mask.reshape([-1]) + def get_exclude_types(self): + return self.exclude_types + def build_type_exclude_mask( self, nlist: np.ndarray, diff --git a/deepmd/dpmodel/utils/nlist.py b/deepmd/dpmodel/utils/nlist.py index 657d6ecee2..1aa1820495 100644 --- a/deepmd/dpmodel/utils/nlist.py +++ b/deepmd/dpmodel/utils/nlist.py @@ -69,6 +69,8 @@ def build_neighbor_list( ) assert list(diff.shape) == [batch_size, nloc, nall, 3] rr = np.linalg.norm(diff, axis=-1) + # if central atom has two zero distances, sorting sometimes can not exclude itself + rr -= np.eye(nloc, nall, dtype=diff.dtype)[np.newaxis, :, :] nlist = np.argsort(rr, axis=-1) rr = np.sort(rr, axis=-1) rr = rr[:, :, 1:] diff --git a/deepmd/entrypoints/test.py b/deepmd/entrypoints/test.py index efc75e31a7..ccf8b1da1e 100644 --- a/deepmd/entrypoints/test.py +++ b/deepmd/entrypoints/test.py @@ -298,6 +298,9 @@ def test_ener( ) if dp.get_dim_aparam() > 0: data.add("aparam", dp.get_dim_aparam(), atomic=True, must=True, high_prec=False) + if dp.has_spin: + data.add("spin", 3, atomic=True, must=True, high_prec=False) + data.add("force_mag", 3, atomic=True, must=False, high_prec=False) test_data = data.get_test() mixed_type = data.mixed_type @@ -311,6 +314,10 @@ def test_ener( efield = test_data["efield"][:numb_test].reshape([numb_test, -1]) else: efield = None + if dp.has_spin: + spin = test_data["spin"][:numb_test].reshape([numb_test, -1]) + else: + spin = None if not data.pbc: box = None if mixed_type: @@ -335,6 +342,7 @@ def test_ener( atomic=has_atom_ener, efield=efield, mixed_type=mixed_type, + spin=spin, ) energy = ret[0] force = ret[1] @@ -347,26 +355,50 @@ def test_ener( av = ret[4] ae = ae.reshape([numb_test, -1]) av = av.reshape([numb_test, -1]) - if dp.get_ntypes_spin() != 0: - ntypes_real = dp.get_ntypes() - dp.get_ntypes_spin() - nloc = natoms - nloc_real = sum([np.count_nonzero(atype == ii) for ii in range(ntypes_real)]) - force_r = np.split( - force, indices_or_sections=[nloc_real * 3, nloc * 3], axis=1 - )[0] - force_m = np.split( - force, indices_or_sections=[nloc_real * 3, nloc * 3], axis=1 - )[1] - test_force_r = np.split( - test_data["force"][:numb_test], - indices_or_sections=[nloc_real * 3, nloc * 3], - axis=1, - )[0] - test_force_m = np.split( - test_data["force"][:numb_test], - indices_or_sections=[nloc_real * 3, nloc * 3], - axis=1, - )[1] + if dp.has_spin: + force_m = ret[5] + force_m = force_m.reshape([numb_test, -1]) + mask_mag = ret[6] + mask_mag = mask_mag.reshape([numb_test, -1]) + else: + if dp.has_spin: + force_m = ret[3] + force_m = force_m.reshape([numb_test, -1]) + mask_mag = ret[4] + mask_mag = mask_mag.reshape([numb_test, -1]) + out_put_spin = dp.get_ntypes_spin() != 0 or dp.has_spin + if out_put_spin: + if dp.get_ntypes_spin() != 0: # old tf support for spin + ntypes_real = dp.get_ntypes() - dp.get_ntypes_spin() + nloc = natoms + nloc_real = sum( + [np.count_nonzero(atype == ii) for ii in range(ntypes_real)] + ) + force_r = np.split( + force, indices_or_sections=[nloc_real * 3, nloc * 3], axis=1 + )[0] + force_m = np.split( + force, indices_or_sections=[nloc_real * 3, nloc * 3], axis=1 + )[1] + test_force_r = np.split( + test_data["force"][:numb_test], + indices_or_sections=[nloc_real * 3, nloc * 3], + axis=1, + )[0] + test_force_m = np.split( + test_data["force"][:numb_test], + indices_or_sections=[nloc_real * 3, nloc * 3], + axis=1, + )[1] + else: # pt support for spin + force_r = force + test_force_r = test_data["force"][:numb_test] + # The shape of force_m and test_force_m are [-1, 3], + # which is designed for mixed_type cases + force_m = force_m.reshape(-1, 3)[mask_mag.reshape(-1)] + test_force_m = test_data["force_mag"][:numb_test].reshape(-1, 3)[ + mask_mag.reshape(-1) + ] diff_e = energy - test_data["energy"][:numb_test].reshape([-1, 1]) mae_e = mae(diff_e) @@ -385,7 +417,7 @@ def test_ener( diff_ae = test_data["atom_ener"][:numb_test].reshape([-1]) - ae.reshape([-1]) mae_ae = mae(diff_ae) rmse_ae = rmse(diff_ae) - if dp.get_ntypes_spin() != 0: + if out_put_spin: mae_fr = mae(force_r - test_force_r) mae_fm = mae(force_m - test_force_m) rmse_fr = rmse(force_r - test_force_r) @@ -396,16 +428,16 @@ def test_ener( log.info(f"Energy RMSE : {rmse_e:e} eV") log.info(f"Energy MAE/Natoms : {mae_ea:e} eV") log.info(f"Energy RMSE/Natoms : {rmse_ea:e} eV") - if dp.get_ntypes_spin() == 0: + if not out_put_spin: log.info(f"Force MAE : {mae_f:e} eV/A") log.info(f"Force RMSE : {rmse_f:e} eV/A") else: log.info(f"Force atom MAE : {mae_fr:e} eV/A") - log.info(f"Force spin MAE : {mae_fm:e} eV/uB") log.info(f"Force atom RMSE : {rmse_fr:e} eV/A") + log.info(f"Force spin MAE : {mae_fm:e} eV/uB") log.info(f"Force spin RMSE : {rmse_fm:e} eV/uB") - if data.pbc: + if data.pbc and not out_put_spin: log.info(f"Virial MAE : {mae_v:e} eV") log.info(f"Virial RMSE : {rmse_v:e} eV") log.info(f"Virial MAE/Natoms : {mae_va:e} eV") @@ -437,7 +469,7 @@ def test_ener( header="%s: data_e pred_e" % system, append=append_detail, ) - if dp.get_ntypes_spin() == 0: + if not out_put_spin: pf = np.concatenate( ( np.reshape(test_data["force"][:numb_test], [-1, 3]), @@ -497,7 +529,7 @@ def test_ener( "pred_vyy pred_vyz pred_vzx pred_vzy pred_vzz", append=append_detail, ) - if dp.get_ntypes_spin() == 0: + if not out_put_spin: return { "mae_e": (mae_e, energy.size), "mae_ea": (mae_ea, energy.size), diff --git a/deepmd/infer/deep_eval.py b/deepmd/infer/deep_eval.py index de964b88b9..065982a870 100644 --- a/deepmd/infer/deep_eval.py +++ b/deepmd/infer/deep_eval.py @@ -57,7 +57,9 @@ class DeepEvalBackend(ABC): "energy": "atom_energy", "energy_redu": "energy", "energy_derv_r": "force", + "energy_derv_r_mag": "force_mag", "energy_derv_c": "atom_virial", + "energy_derv_c_mag": "atom_virial_mag", "energy_derv_c_redu": "virial", "polar": "polar", "polar_redu": "global_polar", @@ -71,6 +73,8 @@ class DeepEvalBackend(ABC): "dipole_derv_c_redu": "virial", "dos": "atom_dos", "dos_redu": "dos", + "mask_mag": "mask_mag", + "mask": "mask", } @abstractmethod @@ -262,9 +266,13 @@ def get_has_efield(self): """Check if the model has efield.""" return False + def get_has_spin(self): + """Check if the model has spin atom types.""" + return False + @abstractmethod def get_ntypes_spin(self) -> int: - """Get the number of spin atom types of this model.""" + """Get the number of spin atom types of this model. Only used in old implement.""" class DeepEval(ABC): @@ -317,6 +325,8 @@ def __init__( neighbor_list=neighbor_list, **kwargs, ) + if self.deep_eval.get_has_spin() and hasattr(self, "output_def_mag"): + self.deep_eval.output_def = self.output_def_mag @property @abstractmethod @@ -518,6 +528,11 @@ def has_efield(self) -> bool: """Check if the model has efield.""" return self.deep_eval.get_has_efield() + @property + def has_spin(self) -> bool: + """Check if the model has spin.""" + return self.deep_eval.get_has_spin() + def get_ntypes_spin(self) -> int: - """Get the number of spin atom types of this model.""" + """Get the number of spin atom types of this model. Only used in old implement.""" return self.deep_eval.get_ntypes_spin() diff --git a/deepmd/infer/deep_pot.py b/deepmd/infer/deep_pot.py index e955a3ed65..bc0bfc9599 100644 --- a/deepmd/infer/deep_pot.py +++ b/deepmd/infer/deep_pot.py @@ -70,6 +70,25 @@ def output_def(self) -> ModelOutputDef: ) ) + @property + def output_def_mag(self) -> ModelOutputDef: + """Get the output definition of this model with magnetic parts.""" + return ModelOutputDef( + FittingOutputDef( + [ + OutputVariableDef( + "energy", + shape=[1], + reduciable=True, + r_differentiable=True, + c_differentiable=True, + atomic=True, + magnetic=True, + ), + ] + ) + ) + def eval( self, coords: np.ndarray, @@ -162,7 +181,7 @@ def eval( natoms_real = natoms atomic_energy = results["energy"].reshape(nframes, natoms_real, 1) atomic_virial = results["energy_derv_c"].reshape(nframes, natoms, 9) - return ( + result = ( energy, force, virial, @@ -170,11 +189,16 @@ def eval( atomic_virial, ) else: - return ( + result = ( energy, force, virial, ) + if self.deep_eval.get_has_spin(): + force_mag = results["energy_derv_r_mag"].reshape(nframes, natoms, 3) + mask_mag = results["mask_mag"].reshape(nframes, natoms, 1) + result = (*list(result), force_mag, mask_mag) + return result __all__ = ["DeepPot"] diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index bf6a5b0306..b8031993c0 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -124,6 +124,9 @@ def __init__( self.auto_batch_size = auto_batch_size else: raise TypeError("auto_batch_size should be bool, int, or AutoBatchSize") + self._has_spin = getattr(self.dp.model["Default"], "has_spin", False) + if callable(self._has_spin): + self._has_spin = self._has_spin() def get_rcut(self) -> float: """Get the cutoff radius of this model.""" @@ -182,9 +185,13 @@ def get_has_efield(self): return False def get_ntypes_spin(self): - """Get the number of spin atom types of this model.""" + """Get the number of spin atom types of this model. Only used in old implement.""" return 0 + def get_has_spin(self): + """Check if the model has spin atom types.""" + return self._has_spin + def eval( self, coords: np.ndarray, @@ -240,14 +247,20 @@ def eval( coords, atom_types, len(atom_types.shape) > 1 ) request_defs = self._get_request_defs(atomic) - out = self._eval_func(self._eval_model, numb_test, natoms)( - coords, - cells, - atom_types, - fparam, - aparam, - request_defs, - ) + if "spin" not in kwargs or kwargs["spin"] is None: + out = self._eval_func(self._eval_model, numb_test, natoms)( + coords, cells, atom_types, fparam, aparam, request_defs + ) + else: + out = self._eval_func(self._eval_model_spin, numb_test, natoms)( + coords, + cells, + atom_types, + np.array(kwargs["spin"]), + fparam, + aparam, + request_defs, + ) return dict( zip( [x.name for x in request_defs], @@ -280,6 +293,7 @@ def _get_request_defs(self, atomic: bool) -> List[OutputVariableDef]: for x in self.output_def.var_defs.values() if x.category in ( + OutputVariableCategory.OUT, OutputVariableCategory.REDU, OutputVariableCategory.DERV_R, OutputVariableCategory.DERV_C_REDU, @@ -399,6 +413,82 @@ def _eval_model( results.append(np.full(np.abs(shape), np.nan)) # this is kinda hacky return tuple(results) + def _eval_model_spin( + self, + coords: np.ndarray, + cells: Optional[np.ndarray], + atom_types: np.ndarray, + spins: np.ndarray, + fparam: Optional[np.ndarray], + aparam: Optional[np.ndarray], + request_defs: List[OutputVariableDef], + ): + model = self.dp.to(DEVICE) + + nframes = coords.shape[0] + if len(atom_types.shape) == 1: + natoms = len(atom_types) + atom_types = np.tile(atom_types, nframes).reshape(nframes, -1) + else: + natoms = len(atom_types[0]) + + coord_input = torch.tensor( + coords.reshape([-1, natoms, 3]), + dtype=GLOBAL_PT_FLOAT_PRECISION, + device=DEVICE, + ) + type_input = torch.tensor(atom_types, dtype=torch.long, device=DEVICE) + spin_input = torch.tensor( + spins.reshape([-1, natoms, 3]), + dtype=GLOBAL_PT_FLOAT_PRECISION, + device=DEVICE, + ) + if cells is not None: + box_input = torch.tensor( + cells.reshape([-1, 3, 3]), + dtype=GLOBAL_PT_FLOAT_PRECISION, + device=DEVICE, + ) + 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, + spin=spin_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] + + results = [] + for odef in request_defs: + pt_name = self._OUTDEF_DP2BACKEND[odef.name] + if pt_name in batch_output: + shape = self._get_output_shape(odef, nframes, natoms) + out = batch_output[pt_name].reshape(shape).detach().cpu().numpy() + results.append(out) + else: + shape = self._get_output_shape(odef, nframes, natoms) + results.append(np.full(np.abs(shape), np.nan)) # this is kinda hacky + return tuple(results) + def _get_output_shape(self, odef, nframes, natoms): if odef.category == OutputVariableCategory.DERV_C_REDU: # virial @@ -427,6 +517,7 @@ def eval_model( coords: Union[np.ndarray, torch.Tensor], cells: Optional[Union[np.ndarray, torch.Tensor]], atom_types: Union[np.ndarray, torch.Tensor, List[int]], + spins: Optional[Union[np.ndarray, torch.Tensor]] = None, atomic: bool = False, infer_batch_size: int = 2, denoise: bool = False, @@ -435,6 +526,7 @@ def eval_model( energy_out = [] atomic_energy_out = [] force_out = [] + force_mag_out = [] virial_out = [] atomic_virial_out = [] updated_coord_out = [] @@ -447,11 +539,15 @@ def eval_model( if isinstance(coords, torch.Tensor): if cells is not None: assert isinstance(cells, torch.Tensor), err_msg + if spins is not None: + assert isinstance(spins, torch.Tensor), err_msg assert isinstance(atom_types, torch.Tensor) or isinstance(atom_types, list) atom_types = torch.tensor(atom_types, dtype=torch.long, device=DEVICE) elif isinstance(coords, np.ndarray): if cells is not None: assert isinstance(cells, np.ndarray), err_msg + if spins is not None: + assert isinstance(spins, np.ndarray), err_msg assert isinstance(atom_types, np.ndarray) or isinstance(atom_types, list) atom_types = np.array(atom_types, dtype=np.int32) return_tensor = False @@ -471,6 +567,16 @@ def eval_model( coord_input = torch.tensor( coords.reshape([-1, natoms, 3]), dtype=GLOBAL_PT_FLOAT_PRECISION, device=DEVICE ) + spin_input = None + if spins is not None: + spin_input = torch.tensor( + spins.reshape([-1, natoms, 3]), + dtype=GLOBAL_PT_FLOAT_PRECISION, + device=DEVICE, + ) + has_spin = getattr(model, "has_spin", False) + if callable(has_spin): + has_spin = has_spin() type_input = torch.tensor(atom_types, dtype=torch.long, device=DEVICE) box_input = None if cells is None: @@ -486,9 +592,20 @@ def eval_model( batch_coord = coord_input[ii * infer_batch_size : (ii + 1) * infer_batch_size] batch_atype = type_input[ii * infer_batch_size : (ii + 1) * infer_batch_size] batch_box = None + batch_spin = None + if spin_input is not None: + batch_spin = spin_input[ii * infer_batch_size : (ii + 1) * infer_batch_size] if pbc: batch_box = box_input[ii * infer_batch_size : (ii + 1) * infer_batch_size] - batch_output = model(batch_coord, batch_atype, box=batch_box) + input_dict = { + "coord": batch_coord, + "atype": batch_atype, + "box": batch_box, + "do_atomic_virial": atomic, + } + if has_spin: + input_dict["spin"] = batch_spin + batch_output = model(**input_dict) if isinstance(batch_output, tuple): batch_output = batch_output[0] if not return_tensor: @@ -500,6 +617,8 @@ def eval_model( ) if "force" in batch_output: force_out.append(batch_output["force"].detach().cpu().numpy()) + if "force_mag" in batch_output: + force_mag_out.append(batch_output["force_mag"].detach().cpu().numpy()) if "virial" in batch_output: virial_out.append(batch_output["virial"].detach().cpu().numpy()) if "atom_virial" in batch_output: @@ -519,6 +638,8 @@ def eval_model( atomic_energy_out.append(batch_output["atom_energy"]) if "force" in batch_output: force_out.append(batch_output["force"]) + if "force_mag" in batch_output: + force_mag_out.append(batch_output["force_mag"]) if "virial" in batch_output: virial_out.append(batch_output["virial"]) if "atom_virial" in batch_output: @@ -539,6 +660,11 @@ def eval_model( force_out = ( np.concatenate(force_out) if force_out else np.zeros([nframes, natoms, 3]) ) + force_mag_out = ( + np.concatenate(force_mag_out) + if force_mag_out + else np.zeros([nframes, natoms, 3]) + ) virial_out = ( np.concatenate(virial_out) if virial_out else np.zeros([nframes, 3, 3]) ) @@ -573,6 +699,13 @@ def eval_model( [nframes, natoms, 3], dtype=GLOBAL_PT_FLOAT_PRECISION, device=DEVICE ) ) + force_mag_out = ( + torch.cat(force_mag_out) + if force_mag_out + else torch.zeros( + [nframes, natoms, 3], dtype=GLOBAL_PT_FLOAT_PRECISION, device=DEVICE + ) + ) virial_out = ( torch.cat(virial_out) if virial_out @@ -592,13 +725,14 @@ def eval_model( if denoise: return updated_coord_out, logits_out else: - if not atomic: - return energy_out, force_out, virial_out - else: - return ( - energy_out, - force_out, - virial_out, - atomic_energy_out, - atomic_virial_out, - ) + results_dict = { + "energy": energy_out, + "force": force_out, + "virial": virial_out, + } + if has_spin: + results_dict["force_mag"] = force_mag_out + if atomic: + results_dict["atom_energy"] = atomic_energy_out + results_dict["atom_virial"] = atomic_virial_out + return results_dict diff --git a/deepmd/pt/loss/__init__.py b/deepmd/pt/loss/__init__.py index d2f6ab9e52..9c8bbc9a2a 100644 --- a/deepmd/pt/loss/__init__.py +++ b/deepmd/pt/loss/__init__.py @@ -5,6 +5,9 @@ from .ener import ( EnergyStdLoss, ) +from .ener_spin import ( + EnergySpinLoss, +) from .loss import ( TaskLoss, ) @@ -15,6 +18,7 @@ __all__ = [ "DenoiseLoss", "EnergyStdLoss", + "EnergySpinLoss", "TensorLoss", "TaskLoss", ] diff --git a/deepmd/pt/loss/ener_spin.py b/deepmd/pt/loss/ener_spin.py new file mode 100644 index 0000000000..b94acf26ea --- /dev/null +++ b/deepmd/pt/loss/ener_spin.py @@ -0,0 +1,245 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +from typing import ( + List, +) + +import torch +import torch.nn.functional as F + +from deepmd.pt.loss.loss import ( + TaskLoss, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.env import ( + GLOBAL_PT_FLOAT_PRECISION, +) +from deepmd.utils.data import ( + DataRequirementItem, +) + + +class EnergySpinLoss(TaskLoss): + def __init__( + self, + starter_learning_rate=1.0, + start_pref_e=0.0, + limit_pref_e=0.0, + start_pref_fr=0.0, + limit_pref_fr=0.0, + start_pref_fm=0.0, + limit_pref_fm=0.0, + start_pref_v=0.0, + limit_pref_v=0.0, + start_pref_ae: float = 0.0, + limit_pref_ae: float = 0.0, + start_pref_pf: float = 0.0, + limit_pref_pf: float = 0.0, + use_l1_all: bool = False, + inference=False, + **kwargs, + ): + """Construct a layer to compute loss on energy, real force, magnetic force and virial.""" + super().__init__() + self.starter_learning_rate = starter_learning_rate + self.has_e = (start_pref_e != 0.0 and limit_pref_e != 0.0) or inference + self.has_fr = (start_pref_fr != 0.0 and limit_pref_fr != 0.0) or inference + self.has_fm = (start_pref_fm != 0.0 and limit_pref_fm != 0.0) or inference + + # TODO need support for virial, atomic energy and atomic pref + self.has_v = (start_pref_v != 0.0 and limit_pref_v != 0.0) or inference + self.has_ae = (start_pref_ae != 0.0 and limit_pref_ae != 0.0) or inference + self.has_pf = (start_pref_pf != 0.0 and limit_pref_pf != 0.0) or inference + + self.start_pref_e = start_pref_e + self.limit_pref_e = limit_pref_e + self.start_pref_fr = start_pref_fr + self.limit_pref_fr = limit_pref_fr + self.start_pref_fm = start_pref_fm + self.limit_pref_fm = limit_pref_fm + self.start_pref_v = start_pref_v + self.limit_pref_v = limit_pref_v + self.use_l1_all = use_l1_all + self.inference = inference + + def forward(self, model_pred, label, natoms, learning_rate, mae=False): + """Return energy loss with magnetic labels. + + Parameters + ---------- + model_pred : dict[str, torch.Tensor] + Model predictions. + label : dict[str, torch.Tensor] + Labels. + natoms : int + The local atom number. + + Returns + ------- + loss: torch.Tensor + Loss for model to minimize. + more_loss: dict[str, torch.Tensor] + Other losses for display. + """ + coef = learning_rate / self.starter_learning_rate + pref_e = self.limit_pref_e + (self.start_pref_e - self.limit_pref_e) * coef + pref_fr = self.limit_pref_fr + (self.start_pref_fr - self.limit_pref_fr) * coef + pref_fm = self.limit_pref_fm + (self.start_pref_fm - self.limit_pref_fm) * coef + pref_v = self.limit_pref_v + (self.start_pref_v - self.limit_pref_v) * coef + loss = torch.tensor(0.0, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE) + more_loss = {} + # more_loss['log_keys'] = [] # showed when validation on the fly + # more_loss['test_keys'] = [] # showed when doing dp test + atom_norm = 1.0 / natoms + if self.has_e and "energy" in model_pred and "energy" in label: + if not self.use_l1_all: + l2_ener_loss = torch.mean( + torch.square(model_pred["energy"] - label["energy"]) + ) + if not self.inference: + more_loss["l2_ener_loss"] = l2_ener_loss.detach() + loss += atom_norm * (pref_e * l2_ener_loss) + rmse_e = l2_ener_loss.sqrt() * atom_norm + more_loss["rmse_e"] = rmse_e.detach() + # more_loss['log_keys'].append('rmse_e') + else: # use l1 and for all atoms + l1_ener_loss = F.l1_loss( + model_pred["energy"].reshape(-1), + label["energy"].reshape(-1), + reduction="sum", + ) + loss += pref_e * l1_ener_loss + more_loss["mae_e"] = F.l1_loss( + model_pred["energy"].reshape(-1), + label["energy"].reshape(-1), + reduction="mean", + ).detach() + # more_loss['log_keys'].append('rmse_e') + if mae: + mae_e = ( + torch.mean(torch.abs(model_pred["energy"] - label["energy"])) + * atom_norm + ) + more_loss["mae_e"] = mae_e.detach() + mae_e_all = torch.mean( + torch.abs(model_pred["energy"] - label["energy"]) + ) + more_loss["mae_e_all"] = mae_e_all.detach() + + if self.has_fr and "force" in model_pred and "force" in label: + if not self.use_l1_all: + diff_fr = label["force"] - model_pred["force"] + l2_force_real_loss = torch.mean(torch.square(diff_fr)) + if not self.inference: + more_loss["l2_force_r_loss"] = l2_force_real_loss.detach() + loss += (pref_fr * l2_force_real_loss).to(GLOBAL_PT_FLOAT_PRECISION) + rmse_fr = l2_force_real_loss.sqrt() + more_loss["rmse_fr"] = rmse_fr.detach() + if mae: + mae_fr = torch.mean(torch.abs(diff_fr)) + more_loss["mae_fr"] = mae_fr.detach() + else: + l1_force_real_loss = F.l1_loss( + label["force"], model_pred["force"], reduction="none" + ) + more_loss["mae_fr"] = l1_force_real_loss.mean().detach() + l1_force_real_loss = l1_force_real_loss.sum(-1).mean(-1).sum() + loss += (pref_fr * l1_force_real_loss).to(GLOBAL_PT_FLOAT_PRECISION) + + if self.has_fm and "force_mag" in model_pred and "force_mag" in label: + nframes = model_pred["force_mag"].shape[0] + atomic_mask = model_pred["mask_mag"].expand([-1, -1, 3]) + label_force_mag = label["force_mag"][atomic_mask].view(nframes, -1, 3) + model_pred_force_mag = model_pred["force_mag"][atomic_mask].view( + nframes, -1, 3 + ) + if not self.use_l1_all: + diff_fm = label_force_mag - model_pred_force_mag + l2_force_mag_loss = torch.mean(torch.square(diff_fm)) + if not self.inference: + more_loss["l2_force_m_loss"] = l2_force_mag_loss.detach() + loss += (pref_fm * l2_force_mag_loss).to(GLOBAL_PT_FLOAT_PRECISION) + rmse_fm = l2_force_mag_loss.sqrt() + more_loss["rmse_fm"] = rmse_fm.detach() + if mae: + mae_fm = torch.mean(torch.abs(diff_fm)) + more_loss["mae_fm"] = mae_fm.detach() + else: + l1_force_mag_loss = F.l1_loss( + label_force_mag, model_pred_force_mag, reduction="none" + ) + more_loss["mae_fm"] = l1_force_mag_loss.mean().detach() + l1_force_mag_loss = l1_force_mag_loss.sum(-1).mean(-1).sum() + loss += (pref_fm * l1_force_mag_loss).to(GLOBAL_PT_FLOAT_PRECISION) + + if not self.inference: + more_loss["rmse"] = torch.sqrt(loss.detach()) + return loss, more_loss + + @property + def label_requirement(self) -> List[DataRequirementItem]: + """Return data label requirements needed for this loss calculation.""" + label_requirement = [] + if self.has_e: + label_requirement.append( + DataRequirementItem( + "energy", + ndof=1, + atomic=False, + must=False, + high_prec=True, + ) + ) + if self.has_fr: + label_requirement.append( + DataRequirementItem( + "force", + ndof=3, + atomic=True, + must=False, + high_prec=False, + ) + ) + if self.has_fm: + label_requirement.append( + DataRequirementItem( + "force_mag", + ndof=3, + atomic=True, + must=False, + high_prec=False, + ) + ) + if self.has_v: + label_requirement.append( + DataRequirementItem( + "virial", + ndof=9, + atomic=False, + must=False, + high_prec=False, + ) + ) + if self.has_ae: + label_requirement.append( + DataRequirementItem( + "atom_ener", + ndof=1, + atomic=True, + must=False, + high_prec=False, + ) + ) + if self.has_pf: + label_requirement.append( + DataRequirementItem( + "atom_pref", + ndof=1, + atomic=True, + must=False, + high_prec=False, + repeat=3, + ) + ) + return label_requirement diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index 807f8433e5..cad1e1cc88 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import copy +import functools import logging from typing import ( Dict, @@ -204,9 +205,23 @@ def compute_or_load_stat( # descriptors and fitting net with different type_map # should not share the same parameters stat_file_path /= " ".join(self.type_map) - self.descriptor.compute_input_stats(sampled_func, stat_file_path) + + @functools.lru_cache + def wrapped_sampler(): + sampled = sampled_func() + if self.pair_excl is not None: + pair_exclude_types = self.pair_excl.get_exclude_types() + for sample in sampled: + sample["pair_exclude_types"] = list(pair_exclude_types) + if self.atom_excl is not None: + atom_exclude_types = self.atom_excl.get_exclude_types() + for sample in sampled: + sample["atom_exclude_types"] = list(atom_exclude_types) + return sampled + + self.descriptor.compute_input_stats(wrapped_sampler, stat_file_path) if self.fitting_net is not None: - self.fitting_net.compute_output_stats(sampled_func, stat_file_path) + self.fitting_net.compute_output_stats(wrapped_sampler, stat_file_path) @torch.jit.export def get_dim_fparam(self) -> int: diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index 215bb25de5..19a67fc8ff 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -230,7 +230,7 @@ def compute_or_load_stat( """ bias_atom_e = compute_output_stats( - merged, stat_file_path, self.rcond, self.atom_ener + merged, self.ntypes, stat_file_path, self.rcond, self.atom_ener ) self.bias_atom_e.copy_( torch.tensor(bias_atom_e, device=env.DEVICE).view([self.ntypes, 1]) diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py index 1b32467540..21275317dc 100644 --- a/deepmd/pt/model/descriptor/dpa1.py +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -3,6 +3,7 @@ Callable, List, Optional, + Tuple, Union, ) @@ -55,13 +56,14 @@ def __init__( temperature=None, return_rot=False, concat_output_tebd: bool = True, + env_protection: float = 0.0, type: Optional[str] = None, # not implemented resnet_dt: bool = False, type_one_side: bool = True, precision: str = "default", trainable: bool = True, - exclude_types: Optional[List[List[int]]] = None, + exclude_types: List[Tuple[int, int]] = [], stripped_type_embedding: bool = False, smooth_type_embdding: bool = False, ): @@ -72,8 +74,6 @@ def __init__( raise NotImplementedError("type_one_side is not supported.") if precision != "default" and precision != "float64": raise NotImplementedError("precison is not supported.") - if exclude_types is not None and exclude_types != []: - raise NotImplementedError("exclude_types is not supported.") if stripped_type_embedding: raise NotImplementedError("stripped_type_embedding is not supported.") if smooth_type_embdding: @@ -102,6 +102,8 @@ def __init__( normalize=normalize, temperature=temperature, return_rot=return_rot, + exclude_types=exclude_types, + env_protection=env_protection, ) self.type_embedding = TypeEmbedNet(ntypes, tebd_dim) self.tebd_dim = tebd_dim diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index a80cc4a445..fb792a51e2 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -3,6 +3,7 @@ Callable, List, Optional, + Tuple, Union, ) @@ -77,7 +78,9 @@ def __init__( repformer_update_style: str = "res_avg", repformer_set_davg_zero: bool = True, # TODO repformer_add_type_ebd_to_seq: bool = False, + env_protection: float = 0.0, trainable: bool = True, + exclude_types: List[Tuple[int, int]] = [], type: Optional[ str ] = None, # work around the bad design in get_trainer and DpLoaderSet! @@ -175,6 +178,9 @@ def __init__( repformers block: concatenate the type embedding at the output. trainable : bool If the parameters in the descriptor are trainable. + exclude_types : List[Tuple[int, int]] = [], + The excluded pairs of types which have no interaction with each other. + For example, `[[0, 1]]` means no interaction between type 0 and type 1. Returns ------- @@ -205,6 +211,8 @@ def __init__( tebd_input_mode="concat", # tebd_input_mode='dot_residual_s', set_davg_zero=repinit_set_davg_zero, + exclude_types=exclude_types, + env_protection=env_protection, activation_function=repinit_activation, ) self.repformers = DescrptBlockRepformers( @@ -236,6 +244,8 @@ def __init__( set_davg_zero=repformer_set_davg_zero, smooth=True, add_type_ebd_to_seq=repformer_add_type_ebd_to_seq, + exclude_types=exclude_types, + env_protection=env_protection, ) self.type_embedding = TypeEmbedNet(ntypes, tebd_dim) if self.repinit.dim_out == self.repformers.dim_in: diff --git a/deepmd/pt/model/descriptor/env_mat.py b/deepmd/pt/model/descriptor/env_mat.py index 4e6ffb7785..e89e7467d3 100644 --- a/deepmd/pt/model/descriptor/env_mat.py +++ b/deepmd/pt/model/descriptor/env_mat.py @@ -8,7 +8,12 @@ def _make_env_mat( - nlist, coord, rcut: float, ruct_smth: float, radial_only: bool = False + nlist, + coord, + rcut: float, + ruct_smth: float, + radial_only: bool = False, + protection: float = 0.0, ): """Make smooth environment matrix.""" bsz, natoms, nnei = nlist.shape @@ -25,8 +30,8 @@ def _make_env_mat( length = torch.linalg.norm(diff, dim=-1, keepdim=True) # for index 0 nloc atom length = length + ~mask.unsqueeze(-1) - t0 = 1 / length - t1 = diff / length**2 + t0 = 1 / (length + protection) + t1 = diff / (length + protection) ** 2 weight = compute_smooth_weight(length, ruct_smth, rcut) weight = weight * mask.unsqueeze(-1) if radial_only: @@ -45,6 +50,7 @@ def prod_env_mat( rcut: float, rcut_smth: float, radial_only: bool = False, + protection: float = 0.0, ): """Generate smooth environment matrix from atom coordinates and other context. @@ -56,13 +62,19 @@ def prod_env_mat( - rcut: Cut-off radius. - rcut_smth: Smooth hyper-parameter for pair force & energy. - radial_only: Whether to return a full description or a radial-only descriptor. + - protection: Protection parameter to prevent division by zero errors during calculations. Returns ------- - env_mat: Shape is [nframes, natoms[1]*nnei*4]. """ _env_mat_se_a, diff, switch = _make_env_mat( - nlist, extended_coord, rcut, rcut_smth, radial_only + nlist, + extended_coord, + rcut, + rcut_smth, + radial_only, + protection=protection, ) # shape [n_atom, dim, 4 or 1] t_avg = mean[atype] # [n_atom, dim, 4 or 1] t_std = stddev[atype] # [n_atom, dim, 4 or 1] diff --git a/deepmd/pt/model/descriptor/repformers.py b/deepmd/pt/model/descriptor/repformers.py index 3e8bf72f77..a908d2e057 100644 --- a/deepmd/pt/model/descriptor/repformers.py +++ b/deepmd/pt/model/descriptor/repformers.py @@ -4,6 +4,7 @@ Dict, List, Optional, + Tuple, Union, ) @@ -24,6 +25,9 @@ from deepmd.pt.utils.env_mat_stat import ( EnvMatStatSe, ) +from deepmd.pt.utils.exclude_mask import ( + PairExcludeMask, +) from deepmd.pt.utils.utils import ( get_activation_fn, ) @@ -83,6 +87,8 @@ def __init__( set_davg_zero: bool = True, # TODO smooth: bool = True, add_type_ebd_to_seq: bool = False, + exclude_types: List[Tuple[int, int]] = [], + env_protection: float = 0.0, type: Optional[str] = None, ): """ @@ -114,6 +120,9 @@ def __init__( self.act = get_activation_fn(activation_function) self.direct_dist = direct_dist self.add_type_ebd_to_seq = add_type_ebd_to_seq + # order matters, placed after the assignment of self.ntypes + self.reinit_exclude(exclude_types) + self.env_protection = env_protection self.g2_embd = mylinear(1, self.g2_dim) layers = [] @@ -211,6 +220,13 @@ def dim_emb(self): """Returns the embedding dimension g2.""" return self.get_dim_emb() + def reinit_exclude( + self, + exclude_types: List[Tuple[int, int]] = [], + ): + self.exclude_types = exclude_types + self.emask = PairExcludeMask(self.ntypes, exclude_types=exclude_types) + def forward( self, nlist: torch.Tensor, @@ -233,6 +249,7 @@ def forward( self.stddev, self.rcut, self.rcut_smth, + protection=self.env_protection, ) nlist_mask = nlist != -1 sw = torch.squeeze(sw, -1) diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index c4b2c772f8..e17b7c5d54 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -79,6 +79,7 @@ def __init__( precision: str = "float64", resnet_dt: bool = False, exclude_types: List[Tuple[int, int]] = [], + env_protection: float = 0.0, old_impl: bool = False, type_one_side: bool = True, **kwargs, @@ -95,6 +96,7 @@ def __init__( precision=precision, resnet_dt=resnet_dt, exclude_types=exclude_types, + env_protection=env_protection, old_impl=old_impl, type_one_side=type_one_side, **kwargs, @@ -249,6 +251,7 @@ def serialize(self) -> dict: "embeddings": obj.filter_layers.serialize(), "env_mat": DPEnvMat(obj.rcut, obj.rcut_smth).serialize(), "exclude_types": obj.exclude_types, + "env_protection": obj.env_protection, "@variables": { "davg": obj["davg"].detach().cpu().numpy(), "dstd": obj["dstd"].detach().cpu().numpy(), @@ -310,6 +313,7 @@ def __init__( precision: str = "float64", resnet_dt: bool = False, exclude_types: List[Tuple[int, int]] = [], + env_protection: float = 0.0, old_impl: bool = False, type_one_side: bool = True, trainable: bool = True, @@ -336,6 +340,7 @@ def __init__( self.prec = PRECISION_DICT[self.precision] self.resnet_dt = resnet_dt self.old_impl = old_impl + self.env_protection = env_protection self.ntypes = len(sel) self.type_one_side = type_one_side # order matters, placed after the assignment of self.ntypes @@ -539,6 +544,7 @@ def forward( self.stddev, self.rcut, self.rcut_smth, + protection=self.env_protection, ) if self.old_impl: diff --git a/deepmd/pt/model/descriptor/se_atten.py b/deepmd/pt/model/descriptor/se_atten.py index db9202c7fc..051c66385c 100644 --- a/deepmd/pt/model/descriptor/se_atten.py +++ b/deepmd/pt/model/descriptor/se_atten.py @@ -4,6 +4,7 @@ Dict, List, Optional, + Tuple, Union, ) @@ -26,6 +27,9 @@ from deepmd.pt.utils.env_mat_stat import ( EnvMatStatSe, ) +from deepmd.pt.utils.exclude_mask import ( + PairExcludeMask, +) from deepmd.utils.env_mat_stat import ( StatItem, ) @@ -61,6 +65,8 @@ def __init__( normalize=True, temperature=None, return_rot=False, + exclude_types: List[Tuple[int, int]] = [], + env_protection: float = 0.0, type: Optional[str] = None, ): """Construct an embedding net of type `se_atten`. @@ -96,6 +102,7 @@ def __init__( self.normalize = normalize self.temperature = temperature self.return_rot = return_rot + self.env_protection = env_protection if isinstance(sel, int): sel = [sel] @@ -106,6 +113,8 @@ def __init__( self.split_sel = self.sel self.nnei = sum(sel) self.ndescrpt = self.nnei * 4 + # order matters, placed after the assignment of self.ntypes + self.reinit_exclude(exclude_types) self.dpa1_attention = NeighborWiseAttention( self.attn_layer, self.nnei, @@ -249,6 +258,13 @@ def get_stats(self) -> Dict[str, StatItem]: ) return self.stats + def reinit_exclude( + self, + exclude_types: List[Tuple[int, int]] = [], + ): + self.exclude_types = exclude_types + self.emask = PairExcludeMask(self.ntypes, exclude_types=exclude_types) + def forward( self, nlist: torch.Tensor, @@ -284,6 +300,7 @@ def forward( self.stddev, self.rcut, self.rcut_smth, + protection=self.env_protection, ) # [nfxnlocxnnei, self.ndescrpt] dmatrix = dmatrix.view(-1, self.ndescrpt) diff --git a/deepmd/pt/model/descriptor/se_r.py b/deepmd/pt/model/descriptor/se_r.py index 5a4920b0e6..ff922e0649 100644 --- a/deepmd/pt/model/descriptor/se_r.py +++ b/deepmd/pt/model/descriptor/se_r.py @@ -64,6 +64,7 @@ def __init__( precision: str = "float64", resnet_dt: bool = False, exclude_types: List[Tuple[int, int]] = [], + env_protection: float = 0.0, old_impl: bool = False, trainable: bool = True, **kwargs, @@ -81,7 +82,9 @@ def __init__( self.old_impl = False # this does not support old implementation. self.exclude_types = exclude_types self.ntypes = len(sel) - self.emask = PairExcludeMask(len(sel), exclude_types=exclude_types) + # order matters, placed after the assignment of self.ntypes + self.reinit_exclude(exclude_types) + self.env_protection = env_protection self.sel = sel self.sec = torch.tensor( @@ -253,6 +256,13 @@ def __getitem__(self, key): else: raise KeyError(key) + def reinit_exclude( + self, + exclude_types: List[Tuple[int, int]] = [], + ): + self.exclude_types = exclude_types + self.emask = PairExcludeMask(self.ntypes, exclude_types=exclude_types) + def forward( self, coord_ext: torch.Tensor, @@ -302,6 +312,7 @@ def forward( self.rcut, self.rcut_smth, True, + protection=self.env_protection, ) assert self.filter_layers is not None @@ -362,6 +373,7 @@ def serialize(self) -> dict: "embeddings": self.filter_layers.serialize(), "env_mat": DPEnvMat(self.rcut, self.rcut_smth).serialize(), "exclude_types": self.exclude_types, + "env_protection": self.env_protection, "@variables": { "davg": self["davg"].detach().cpu().numpy(), "dstd": self["dstd"].detach().cpu().numpy(), diff --git a/deepmd/pt/model/model/__init__.py b/deepmd/pt/model/model/__init__.py index cd53f0a6b3..8e4352e60c 100644 --- a/deepmd/pt/model/model/__init__.py +++ b/deepmd/pt/model/model/__init__.py @@ -22,6 +22,9 @@ from deepmd.pt.model.task import ( BaseFitting, ) +from deepmd.utils.spin import ( + Spin, +) from .dp_model import ( DPModel, @@ -41,6 +44,40 @@ from .model import ( BaseModel, ) +from .spin_model import ( + SpinEnergyModel, + SpinModel, +) + + +def get_spin_model(model_params): + model_params = copy.deepcopy(model_params) + # include virtual spin and placeholder types + model_params["type_map"] += [item + "_spin" for item in model_params["type_map"]] + spin = Spin( + use_spin=model_params["spin"]["use_spin"], + virtual_scale=model_params["spin"]["virtual_scale"], + ) + pair_exclude_types = spin.get_pair_exclude_types( + exclude_types=model_params.get("pair_exclude_types", None) + ) + model_params["pair_exclude_types"] = pair_exclude_types + # for descriptor data stat + model_params["descriptor"]["exclude_types"] = pair_exclude_types + atom_exclude_types = spin.get_atom_exclude_types( + exclude_types=model_params.get("atom_exclude_types", None) + ) + model_params["atom_exclude_types"] = atom_exclude_types + if ( + "env_protection" not in model_params["descriptor"] + or model_params["descriptor"]["env_protection"] == 0.0 + ): + model_params["descriptor"]["env_protection"] = 1e-6 + if model_params["descriptor"]["type"] in ["se_e2_a"]: + # only expand sel for se_e2_a + model_params["descriptor"]["sel"] += model_params["descriptor"]["sel"] + backbone_model = get_standard_model(model_params) + return SpinEnergyModel(backbone_model=backbone_model, spin=spin) def get_zbl_model(model_params): @@ -87,7 +124,7 @@ def get_zbl_model(model_params): ) -def get_model(model_params): +def get_standard_model(model_params): model_params = copy.deepcopy(model_params) ntypes = len(model_params["type_map"]) # descriptor @@ -120,12 +157,22 @@ def get_model(model_params): return model +def get_model(model_params): + if "spin" in model_params: + return get_spin_model(model_params) + elif "use_srtab" in model_params: + return get_zbl_model(model_params) + else: + return get_standard_model(model_params) + + __all__ = [ "BaseModel", "get_model", - "get_zbl_model", "DPModel", "EnergyModel", + "SpinModel", + "SpinEnergyModel", "DPZBLModel", "make_model", "make_hessian_model", diff --git a/deepmd/pt/model/model/dp_zbl_model.py b/deepmd/pt/model/model/dp_zbl_model.py index cacf59c16c..fdf9334119 100644 --- a/deepmd/pt/model/model/dp_zbl_model.py +++ b/deepmd/pt/model/model/dp_zbl_model.py @@ -92,15 +92,16 @@ def forward_lower( model_predict["atom_energy"] = model_ret["energy"] model_predict["energy"] = model_ret["energy_redu"] if self.do_grad_r("energy"): - model_predict["force"] = model_ret["energy_derv_r"].squeeze(-2) + model_predict["extended_force"] = model_ret["energy_derv_r"].squeeze(-2) if self.do_grad_c("energy"): model_predict["virial"] = model_ret["energy_derv_c_redu"].squeeze(-2) if do_atomic_virial: - model_predict["atom_virial"] = model_ret["energy_derv_c"].squeeze(-3) + model_predict["extended_virial"] = model_ret["energy_derv_c"].squeeze( + -3 + ) else: assert model_ret["dforce"] is not None model_predict["dforce"] = model_ret["dforce"] - model_predict = model_ret return model_predict @classmethod diff --git a/deepmd/pt/model/model/spin_model.py b/deepmd/pt/model/model/spin_model.py new file mode 100644 index 0000000000..df2f48e2e4 --- /dev/null +++ b/deepmd/pt/model/model/spin_model.py @@ -0,0 +1,560 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import functools +from typing import ( + Dict, + List, + Optional, +) + +import torch + +from deepmd.pt.utils.utils import ( + to_torch_tensor, +) +from deepmd.utils.path import ( + DPPath, +) +from deepmd.utils.spin import ( + Spin, +) + +from .dp_model import ( + DPModel, +) + + +class SpinModel(torch.nn.Module): + """A spin model wrapper, with spin input preprocess and output split.""" + + def __init__( + self, + backbone_model, + spin: Spin, + ): + super().__init__() + self.backbone_model = backbone_model + self.spin = spin + self.ntypes_real = self.spin.ntypes_real + self.virtual_scale_mask = to_torch_tensor(self.spin.get_virtual_scale_mask()) + self.spin_mask = to_torch_tensor(self.spin.get_spin_mask()) + + def process_spin_input(self, coord, atype, spin): + """Generate virtual coordinates and types, concat into the input.""" + nframes, nloc = coord.shape[:-1] + atype_spin = torch.concat([atype, atype + self.ntypes_real], dim=-1) + virtual_coord = coord + spin * self.virtual_scale_mask[atype].reshape( + [nframes, nloc, 1] + ) + coord_spin = torch.concat([coord, virtual_coord], dim=-2) + return coord_spin, atype_spin + + def process_spin_input_lower( + self, + extended_coord, + extended_atype, + extended_spin, + nlist, + mapping: Optional[torch.Tensor] = None, + ): + """ + Add `extended_spin` into `extended_coord` to generate virtual atoms, and extend `nlist` and `mapping`. + Note that the final `extended_coord_updated` with shape [nframes, nall + nall, 3] has the following order: + - [:, :nloc]: original nloc real atoms. + - [:, nloc: nloc + nloc]: virtual atoms corresponding to nloc real atoms. + - [:, nloc + nloc: nloc + nall]: ghost real atoms. + - [:, nloc + nall: nall + nall]: virtual atoms corresponding to ghost real atoms. + """ + nframes, nall = extended_coord.shape[:2] + nloc = nlist.shape[1] + virtual_extended_coord = ( + extended_coord + + extended_spin + * self.virtual_scale_mask[extended_atype].reshape([nframes, nall, 1]) + ) + virtual_extended_atype = extended_atype + self.ntypes_real + extended_coord_updated = self.concat_switch_virtual( + extended_coord, virtual_extended_coord, nloc + ) + extended_atype_updated = self.concat_switch_virtual( + extended_atype, virtual_extended_atype, nloc + ) + if mapping is not None: + virtual_mapping = mapping + nloc + mapping_updated = self.concat_switch_virtual(mapping, virtual_mapping, nloc) + else: + mapping_updated = None + # extend the nlist + nlist_updated = self.extend_nlist(extended_atype, nlist) + return ( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping_updated, + ) + + def process_spin_output( + self, atype, out_tensor, add_mag: bool = True, virtual_scale: bool = True + ): + """ + Split the output both real and virtual atoms, and scale the latter. + add_mag: whether to add magnetic tensor onto the real tensor. + Default: True. e.g. Ture for forces and False for atomic virials on real atoms. + virtual_scale: whether to scale the magnetic tensor with virtual scale factor. + Default: True. e.g. Ture for forces and False for atomic virials on virtual atoms. + """ + nframes, nloc_double = out_tensor.shape[:2] + nloc = nloc_double // 2 + if virtual_scale: + virtual_scale_mask = self.virtual_scale_mask + else: + virtual_scale_mask = self.spin_mask + atomic_mask = virtual_scale_mask[atype].reshape([nframes, nloc, 1]) + out_real, out_mag = torch.split(out_tensor, [nloc, nloc], dim=1) + if add_mag: + out_real = out_real + out_mag + out_mag = (out_mag.view([nframes, nloc, -1]) * atomic_mask).view(out_mag.shape) + return out_real, out_mag, atomic_mask > 0.0 + + def process_spin_output_lower( + self, + extended_atype, + extended_out_tensor, + nloc: int, + add_mag: bool = True, + virtual_scale: bool = True, + ): + """ + Split the extended output of both real and virtual atoms with switch, and scale the latter. + add_mag: whether to add magnetic tensor onto the real tensor. + Default: True. e.g. Ture for forces and False for atomic virials on real atoms. + virtual_scale: whether to scale the magnetic tensor with virtual scale factor. + Default: True. e.g. Ture for forces and False for atomic virials on virtual atoms. + """ + nframes, nall_double = extended_out_tensor.shape[:2] + nall = nall_double // 2 + if virtual_scale: + virtual_scale_mask = self.virtual_scale_mask + else: + virtual_scale_mask = self.spin_mask + atomic_mask = virtual_scale_mask[extended_atype].reshape([nframes, nall, 1]) + extended_out_real = torch.cat( + [ + extended_out_tensor[:, :nloc], + extended_out_tensor[:, nloc + nloc : nloc + nall], + ], + dim=1, + ) + extended_out_mag = torch.cat( + [ + extended_out_tensor[:, nloc : nloc + nloc], + extended_out_tensor[:, nloc + nall :], + ], + dim=1, + ) + if add_mag: + extended_out_real = extended_out_real + extended_out_mag + extended_out_mag = ( + extended_out_mag.view([nframes, nall, -1]) * atomic_mask + ).view(extended_out_mag.shape) + return extended_out_real, extended_out_mag, atomic_mask > 0.0 + + @staticmethod + def extend_nlist(extended_atype, nlist): + nframes, nloc, nnei = nlist.shape + nall = extended_atype.shape[1] + nlist_mask = nlist != -1 + nlist[nlist == -1] = 0 + nlist_shift = nlist + nall + nlist[~nlist_mask] = -1 + nlist_shift[~nlist_mask] = -1 + self_spin = torch.arange(0, nloc, dtype=nlist.dtype, device=nlist.device) + nall + self_spin = self_spin.view(1, -1, 1).expand(nframes, -1, -1) + # self spin + real neighbor + virtual neighbor + # nf x nloc x (1 + nnei + nnei) + extended_nlist = torch.cat([self_spin, nlist, nlist_shift], dim=-1) + # nf x (nloc + nloc) x (1 + nnei + nnei) + extended_nlist = torch.cat( + [extended_nlist, -1 * torch.ones_like(extended_nlist)], dim=-2 + ) + # update the index for switch + first_part_index = (nloc <= extended_nlist) & (extended_nlist < nall) + second_part_index = (nall <= extended_nlist) & (extended_nlist < (nall + nloc)) + extended_nlist[first_part_index] += nloc + extended_nlist[second_part_index] -= nall - nloc + return extended_nlist + + @staticmethod + def concat_switch_virtual(extended_tensor, extended_tensor_virtual, nloc: int): + """ + Concat real and virtual extended tensors, and switch all the local ones to the first nloc * 2 atoms. + - [:, :nloc]: original nloc real atoms. + - [:, nloc: nloc + nloc]: virtual atoms corresponding to nloc real atoms. + - [:, nloc + nloc: nloc + nall]: ghost real atoms. + - [:, nloc + nall: nall + nall]: virtual atoms corresponding to ghost real atoms. + """ + nframes, nall = extended_tensor.shape[:2] + out_shape = list(extended_tensor.shape) + out_shape[1] *= 2 + extended_tensor_updated = torch.zeros( + out_shape, + dtype=extended_tensor.dtype, + device=extended_tensor.device, + ) + extended_tensor_updated[:, :nloc] = extended_tensor[:, :nloc] + extended_tensor_updated[:, nloc : nloc + nloc] = extended_tensor_virtual[ + :, :nloc + ] + extended_tensor_updated[:, nloc + nloc : nloc + nall] = extended_tensor[ + :, nloc: + ] + extended_tensor_updated[:, nloc + nall :] = extended_tensor_virtual[:, nloc:] + return extended_tensor_updated.view(out_shape) + + @staticmethod + def expand_aparam(aparam, nloc: int): + """Expand the atom parameters for virtual atoms if necessary.""" + nframes, natom, numb_aparam = aparam.shape[1:] + if natom == nloc: # good + pass + elif natom < nloc: # for spin with virtual atoms + aparam = torch.concat( + [ + aparam, + torch.zeros( + [nframes, nloc - natom, numb_aparam], + device=aparam.device, + dtype=aparam.dtype, + ), + ], + dim=1, + ) + else: + raise ValueError( + f"get an input aparam with {aparam.shape[1]} inputs, ", + f"which is larger than {nloc} atoms.", + ) + return aparam + + @torch.jit.export + def get_type_map(self) -> List[str]: + """Get the type map.""" + tmap = self.backbone_model.get_type_map() + ntypes = len(tmap) // 2 # ignore the virtual type + return tmap[:ntypes] + + @torch.jit.export + def get_rcut(self): + """Get the cut-off radius.""" + return self.backbone_model.get_rcut() + + @torch.jit.export + def get_dim_fparam(self): + """Get the number (dimension) of frame parameters of this atomic model.""" + return self.backbone_model.get_dim_fparam() + + @torch.jit.export + def get_dim_aparam(self): + """Get the number (dimension) of atomic parameters of this atomic model.""" + return self.backbone_model.get_dim_aparam() + + @torch.jit.export + def get_sel_type(self) -> List[int]: + """Get the selected atom types of this model. + Only atoms with selected atom types have atomic contribution + to the result of the model. + If returning an empty list, all atom types are selected. + """ + return self.backbone_model.get_sel_type() + + @torch.jit.export + def is_aparam_nall(self) -> bool: + """Check whether the shape of atomic parameters is (nframes, nall, ndim). + If False, the shape is (nframes, nloc, ndim). + """ + return self.backbone_model.is_aparam_nall() + + @torch.jit.export + def model_output_type(self) -> List[str]: + """Get the output type for the model.""" + return self.backbone_model.model_output_type() + + @torch.jit.export + def get_model_def_script(self) -> str: + """Get the model definition script.""" + return self.backbone_model.get_model_def_script() + + @torch.jit.export + def get_nnei(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + # for C++ interface + if not self.backbone_model.mixed_types(): + return self.backbone_model.get_nnei() // 2 # ignore the virtual selected + else: + return self.backbone_model.get_nnei() + + @torch.jit.export + def get_nsel(self) -> int: + """Returns the total number of selected neighboring atoms in the cut-off radius.""" + if not self.backbone_model.mixed_types(): + return self.backbone_model.get_nsel() // 2 # ignore the virtual selected + else: + return self.backbone_model.get_nsel() + + @torch.jit.export + def has_spin(self) -> bool: + """Returns whether it has spin input and output.""" + return True + + def __getattr__(self, name): + """Get attribute from the wrapped model.""" + if ( + name == "backbone_model" + ): # torch.nn.Module will exclude modules to self.__dict__["_modules"] + return self.__dict__["_modules"]["backbone_model"] + elif name in self.__dict__: + return self.__dict__[name] + else: + return getattr(self.backbone_model, name) + + def compute_or_load_stat( + self, + sampled_func, + stat_file_path: Optional[DPPath] = None, + ): + """ + Compute or load the statistics parameters of the model, + such as mean and standard deviation of descriptors or the energy bias of the fitting net. + When `sampled` is provided, all the statistics parameters will be calculated (or re-calculated for update), + and saved in the `stat_file_path`(s). + When `sampled` is not provided, it will check the existence of `stat_file_path`(s) + and load the calculated statistics parameters. + + Parameters + ---------- + sampled_func + The lazy sampled function to get data frames from different data systems. + stat_file_path + The dictionary of paths to the statistics files. + """ + + @functools.lru_cache + def spin_sampled_func(): + sampled = sampled_func() + spin_sampled = [] + for sys in sampled: + coord_updated, atype_updated = self.process_spin_input( + sys["coord"], sys["atype"], sys["spin"] + ) + tmp_dict = { + "coord": coord_updated, + "atype": atype_updated, + } + if "natoms" in sys: + natoms = sys["natoms"] + tmp_dict["natoms"] = torch.cat( + [2 * natoms[:, :2], natoms[:, 2:], natoms[:, 2:]], dim=-1 + ) + for item_key in sys.keys(): + if item_key not in ["coord", "atype", "spin", "natoms"]: + tmp_dict[item_key] = sys[item_key] + spin_sampled.append(tmp_dict) + return spin_sampled + + self.backbone_model.compute_or_load_stat(spin_sampled_func, stat_file_path) + + def forward_common( + self, + coord, + atype, + spin, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + nframes, nloc = coord.shape[:2] + coord_updated, atype_updated = self.process_spin_input(coord, atype, spin) + model_ret = self.backbone_model.forward_common( + coord_updated, + atype_updated, + box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + model_output_type = self.backbone_model.model_output_type() + if "mask" in model_output_type: + model_output_type.pop(model_output_type.index("mask")) + var_name = model_output_type[0] + model_ret[f"{var_name}"] = torch.split( + model_ret[f"{var_name}"], [nloc, nloc], dim=1 + )[0] + if self.backbone_model.do_grad_r(var_name): + ( + model_ret[f"{var_name}_derv_r"], + model_ret[f"{var_name}_derv_r_mag"], + model_ret["mask_mag"], + ) = self.process_spin_output(atype, model_ret[f"{var_name}_derv_r"]) + if self.backbone_model.do_grad_c(var_name) and do_atomic_virial: + ( + model_ret[f"{var_name}_derv_c"], + model_ret[f"{var_name}_derv_c_mag"], + model_ret["mask_mag"], + ) = self.process_spin_output( + atype, + model_ret[f"{var_name}_derv_c"], + add_mag=False, + virtual_scale=False, + ) + return model_ret + + def forward_common_lower( + self, + extended_coord, + extended_atype, + extended_spin, + nlist, + mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ): + nframes, nloc = nlist.shape[:2] + ( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping_updated, + ) = self.process_spin_input_lower( + extended_coord, extended_atype, extended_spin, nlist, mapping=mapping + ) + model_ret = self.backbone_model.forward_common_lower( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping=mapping_updated, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + model_output_type = self.backbone_model.model_output_type() + if "mask" in model_output_type: + model_output_type.pop(model_output_type.index("mask")) + var_name = model_output_type[0] + model_ret[f"{var_name}"] = torch.split( + model_ret[f"{var_name}"], [nloc, nloc], dim=1 + )[0] + if self.backbone_model.do_grad_r(var_name): + ( + model_ret[f"{var_name}_derv_r"], + model_ret[f"{var_name}_derv_r_mag"], + model_ret["mask_mag"], + ) = self.process_spin_output_lower( + extended_atype, model_ret[f"{var_name}_derv_r"], nloc + ) + if self.backbone_model.do_grad_c(var_name) and do_atomic_virial: + ( + model_ret[f"{var_name}_derv_c"], + model_ret[f"{var_name}_derv_c_mag"], + model_ret["mask_mag"], + ) = self.process_spin_output_lower( + extended_atype, + model_ret[f"{var_name}_derv_c"], + nloc, + add_mag=False, + virtual_scale=False, + ) + return model_ret + + def serialize(self) -> dict: + return { + "backbone_model": self.backbone_model.serialize(), + "spin": self.spin.serialize(), + } + + @classmethod + def deserialize(cls, data) -> "SpinModel": + backbone_model_obj = DPModel.deserialize(data["backbone_model"]) + spin = Spin.deserialize(data["spin"]) + return cls( + backbone_model=backbone_model_obj, + spin=spin, + ) + + +class SpinEnergyModel(SpinModel): + """A spin model for energy.""" + + model_type = "ener" + + def __init__( + self, + backbone_model, + spin: Spin, + ): + super().__init__(backbone_model, spin) + + def forward( + self, + coord, + atype, + spin, + box: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ) -> Dict[str, torch.Tensor]: + if aparam is not None: + aparam = self.expand_aparam(aparam, coord.shape[1]) + model_ret = self.forward_common( + coord, + atype, + spin, + box, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + model_predict = {} + model_predict["atom_energy"] = model_ret["energy"] + model_predict["energy"] = model_ret["energy_redu"] + model_predict["mask_mag"] = model_ret["mask_mag"] + if self.backbone_model.do_grad_r("energy"): + model_predict["force"] = model_ret["energy_derv_r"].squeeze(-2) + model_predict["force_mag"] = model_ret["energy_derv_r_mag"].squeeze(-2) + # not support virial by far + return model_predict + + @torch.jit.export + def forward_lower( + self, + extended_coord, + extended_atype, + extended_spin, + nlist, + mapping: Optional[torch.Tensor] = None, + fparam: Optional[torch.Tensor] = None, + aparam: Optional[torch.Tensor] = None, + do_atomic_virial: bool = False, + ): + model_ret = self.forward_common_lower( + extended_coord, + extended_atype, + extended_spin, + nlist, + mapping=mapping, + fparam=fparam, + aparam=aparam, + do_atomic_virial=do_atomic_virial, + ) + model_predict = {} + model_predict["atom_energy"] = model_ret["energy"] + model_predict["energy"] = model_ret["energy_redu"] + model_predict["mask_mag"] = model_ret["mask_mag"] + if self.backbone_model.do_grad_r("energy"): + model_predict["extended_force"] = model_ret["energy_derv_r"].squeeze(-2) + model_predict["extended_force_mag"] = model_ret[ + "energy_derv_r_mag" + ].squeeze(-2) + # not support virial by far + return model_predict diff --git a/deepmd/pt/model/task/ener.py b/deepmd/pt/model/task/ener.py index a11f6410a4..b593ddc3cc 100644 --- a/deepmd/pt/model/task/ener.py +++ b/deepmd/pt/model/task/ener.py @@ -162,7 +162,7 @@ def compute_output_stats( """ bias_atom_e = compute_output_stats( - merged, stat_file_path, self.rcond, self.atom_ener + merged, self.ntypes, stat_file_path, self.rcond, self.atom_ener ) self.bias_atom_e.copy_( torch.tensor(bias_atom_e, device=env.DEVICE).view( diff --git a/deepmd/pt/model/task/fitting.py b/deepmd/pt/model/task/fitting.py index 22fb409cad..09f8563bfb 100644 --- a/deepmd/pt/model/task/fitting.py +++ b/deepmd/pt/model/task/fitting.py @@ -508,10 +508,10 @@ def _forward_common( assert self.aparam_inv_std is not None if aparam.shape[-1] != self.numb_aparam: raise ValueError( - "get an input aparam of dim {aparam.shape[-1]}, ", - "which is not consistent with {self.numb_aparam}.", + f"get an input aparam of dim {aparam.shape[-1]}, ", + f"which is not consistent with {self.numb_aparam}.", ) - aparam = aparam.view([nf, nloc, self.numb_aparam]) + aparam = aparam.view([nf, -1, self.numb_aparam]) nb, nloc, _ = aparam.shape t_aparam_avg = self._extend_a_avg_std(self.aparam_avg, nb, nloc) t_aparam_inv_std = self._extend_a_avg_std(self.aparam_inv_std, nb, nloc) diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index f066c4fe37..6938db9b3c 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -25,6 +25,7 @@ ) from deepmd.pt.loss import ( DenoiseLoss, + EnergySpinLoss, EnergyStdLoss, TensorLoss, ) @@ -207,27 +208,31 @@ def single_model_stat( _stat_file_path, _data_requirement, ): - _training_data.add_data_requirement(_data_requirement) - if _validation_data is not None: - _validation_data.add_data_requirement(_data_requirement) if _model.get_dim_fparam() > 0: fparam_requirement_items = [ DataRequirementItem( "fparam", _model.get_dim_fparam(), atomic=False, must=True ) ] - _training_data.add_data_requirement(fparam_requirement_items) - if _validation_data is not None: - _validation_data.add_data_requirement(fparam_requirement_items) + _data_requirement += fparam_requirement_items if _model.get_dim_aparam() > 0: aparam_requirement_items = [ DataRequirementItem( "aparam", _model.get_dim_aparam(), atomic=True, must=True ) ] - _training_data.add_data_requirement(aparam_requirement_items) - if _validation_data is not None: - _validation_data.add_data_requirement(aparam_requirement_items) + _data_requirement += aparam_requirement_items + has_spin = getattr(_model, "has_spin", False) + if callable(has_spin): + has_spin = has_spin() + if has_spin: + spin_requirement_items = [ + DataRequirementItem("spin", ndof=3, atomic=True, must=True) + ] + _data_requirement += spin_requirement_items + _training_data.add_data_requirement(_data_requirement) + if _validation_data is not None: + _validation_data.add_data_requirement(_data_requirement) if not resuming and self.rank == 0: @functools.lru_cache @@ -268,6 +273,9 @@ def get_loss(loss_params, start_lr, _ntypes, _model): if loss_type == "ener": loss_params["starter_learning_rate"] = start_lr return EnergyStdLoss(**loss_params) + elif loss_type == "ener_spin": + loss_params["starter_learning_rate"] = start_lr + return EnergySpinLoss(**loss_params) elif loss_type == "denoise": loss_params["ntypes"] = _ntypes return DenoiseLoss(**loss_params) @@ -973,8 +981,8 @@ def get_data(self, is_train=True, task_key="Default"): input_keys = [ "coord", "atype", - "box", "spin", + "box", "fparam", "aparam", ] diff --git a/deepmd/pt/train/wrapper.py b/deepmd/pt/train/wrapper.py index a455041526..c1040fb9e3 100644 --- a/deepmd/pt/train/wrapper.py +++ b/deepmd/pt/train/wrapper.py @@ -141,8 +141,8 @@ def forward( self, coord, atype, - box: Optional[torch.Tensor] = None, spin: Optional[torch.Tensor] = None, + box: Optional[torch.Tensor] = None, cur_lr: Optional[torch.Tensor] = None, label: Optional[torch.Tensor] = None, task_key: Optional[torch.Tensor] = None, @@ -157,14 +157,20 @@ def forward( assert ( 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, - fparam=fparam, - aparam=aparam, - ) + input_dict = { + "coord": coord, + "atype": atype, + "box": box, + "do_atomic_virial": do_atomic_virial, + "fparam": fparam, + "aparam": aparam, + } + has_spin = getattr(self.model[task_key], "has_spin", False) + if callable(has_spin): + has_spin = has_spin() + if has_spin: + input_dict["spin"] = spin + model_pred = self.model[task_key](**input_dict) natoms = atype.shape[-1] if not self.inference_only and not inference_only: loss, more_loss = self.loss[task_key]( diff --git a/deepmd/pt/utils/env_mat_stat.py b/deepmd/pt/utils/env_mat_stat.py index cd2943e6a8..47e17e9eaa 100644 --- a/deepmd/pt/utils/env_mat_stat.py +++ b/deepmd/pt/utils/env_mat_stat.py @@ -4,6 +4,8 @@ Dict, Iterator, List, + Tuple, + Union, ) import numpy as np @@ -18,6 +20,9 @@ from deepmd.pt.utils import ( env, ) +from deepmd.pt.utils.exclude_mask import ( + PairExcludeMask, +) from deepmd.pt.utils.nlist import ( extend_input_and_build_neighbor_list, ) @@ -73,13 +78,13 @@ def __init__(self, descriptor: "DescriptorBlock"): ) # se_r=1, se_a=4 def iter( - self, data: List[Dict[str, torch.Tensor]] + self, data: List[Dict[str, Union[torch.Tensor, List[Tuple[int, int]]]]] ) -> Iterator[Dict[str, StatItem]]: """Get the iterator of the environment matrix. Parameters ---------- - data : List[Dict[str, torch.Tensor]] + data : List[Dict[str, Union[torch.Tensor, List[Tuple[int, int]]]]] The data. Yields @@ -139,6 +144,7 @@ def iter( # TODO: export rcut_smth from DescriptorBlock self.descriptor.rcut_smth, radial_only, + protection=self.descriptor.env_protection, ) # reshape to nframes * nloc at the atom level, # so nframes/mixed_type do not matter @@ -156,9 +162,16 @@ def iter( self.descriptor.get_ntypes(), device=env.DEVICE, dtype=torch.int32 ).view(-1, 1), ) + if "pair_exclude_types" in system: + # shape: (1, nloc, nnei) + exclude_mask = PairExcludeMask( + self.descriptor.get_ntypes(), system["pair_exclude_types"] + )(nlist, extended_atype).view(1, coord.shape[0] * coord.shape[1], -1) + # shape: (ntypes, nloc, nnei) + type_idx = torch.logical_and(type_idx.unsqueeze(-1), exclude_mask) for type_i in range(self.descriptor.get_ntypes()): dd = env_mat[type_idx[type_i]] - dd = dd.reshape([-1, self.last_dim]) # typen_atoms * nnei, 4 + dd = dd.reshape([-1, self.last_dim]) # typen_atoms * unmasked_nnei, 4 env_mats = {} env_mats[f"r_{type_i}"] = dd[:, :1] if self.last_dim == 4: diff --git a/deepmd/pt/utils/exclude_mask.py b/deepmd/pt/utils/exclude_mask.py index 6df8df8dd0..9ddae3a416 100644 --- a/deepmd/pt/utils/exclude_mask.py +++ b/deepmd/pt/utils/exclude_mask.py @@ -37,6 +37,12 @@ def reinit( ) self.type_mask = to_torch_tensor(self.type_mask).view([-1]) + def get_exclude_types(self): + return self.exclude_types + + def get_type_mask(self): + return self.type_mask + def forward( self, atype: torch.Tensor, @@ -46,7 +52,7 @@ def forward( Parameters ---------- atype - The extended aotm types. shape: nf x natom + The extended atom types. shape: nf x natom Returns ------- @@ -97,6 +103,9 @@ def reinit( self.type_mask = to_torch_tensor(self.type_mask).view([-1]) self.no_exclusion = len(self._exclude_types) == 0 + def get_exclude_types(self): + return self._exclude_types + # may have a better place for this method... def forward( self, diff --git a/deepmd/pt/utils/multi_task.py b/deepmd/pt/utils/multi_task.py index ae3933a101..5f06d93208 100644 --- a/deepmd/pt/utils/multi_task.py +++ b/deepmd/pt/utils/multi_task.py @@ -143,8 +143,12 @@ def replace_one_item(params_dict, key_type, key_in_dict, suffix="", index=None): ) for shared_key in shared_links: shared_links[shared_key]["links"] = sorted( - shared_links[shared_key]["links"], key=lambda x: x["shared_level"] + shared_links[shared_key]["links"], + key=lambda x: x["shared_level"] + - ("spin" in model_config["model_dict"][x["model_key"]]) * 100, ) + # little trick to make spin models in the front to be the base models, + # because its type embeddings are more general. assert len(type_map_keys) == 1, "Multitask model must have only one type_map!" return model_config, shared_links diff --git a/deepmd/pt/utils/nlist.py b/deepmd/pt/utils/nlist.py index 56a062f1b8..d37931b65a 100644 --- a/deepmd/pt/utils/nlist.py +++ b/deepmd/pt/utils/nlist.py @@ -107,6 +107,8 @@ def build_neighbor_list( assert list(diff.shape) == [batch_size, nloc, nall, 3] # nloc x nall rr = torch.linalg.norm(diff, dim=-1) + # if central atom has two zero distances, sorting sometimes can not exclude itself + rr -= torch.eye(nloc, nall, dtype=rr.dtype, device=rr.device).unsqueeze(0) rr, nlist = torch.sort(rr, dim=-1) # nloc x (nall-1) rr = rr[:, :, 1:] @@ -335,7 +337,6 @@ def extend_coord_with_ghosts( extend_atype = torch.tile(atype.unsqueeze(-2), [1, ns, 1]) # nf x ns x nloc extend_aidx = torch.tile(aidx.unsqueeze(-2), [1, ns, 1]) - return ( extend_coord.reshape([nf, nall * 3]).to(device), extend_atype.view([nf, nall]).to(device), diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index 63abccc75d..5e631d9412 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -11,6 +11,7 @@ import torch from deepmd.pt.utils import ( + AtomExcludeMask, env, ) from deepmd.pt.utils.utils import ( @@ -71,6 +72,7 @@ def make_stat_input(datasets, dataloaders, nbatches): def compute_output_stats( merged: Union[Callable[[], List[dict]], List[dict]], + ntypes: int, stat_file_path: Optional[DPPath] = None, rcond: Optional[float] = None, atom_ener: Optional[List[float]] = None, @@ -87,6 +89,8 @@ def compute_output_stats( - Callable[[], List[dict]]: A lazy function that returns data samples in the above format only when needed. Since the sampling process can be slow and memory-intensive, the lazy function helps by only sampling once. + ntypes : int + The number of atom types. stat_file_path : DPPath, optional The path to the stat file. rcond : float, optional @@ -107,10 +111,14 @@ def compute_output_stats( sampled = merged energy = [item["energy"] for item in sampled] data_mixed_type = "real_natoms_vec" in sampled[0] - if data_mixed_type: - input_natoms = [item["real_natoms_vec"] for item in sampled] - else: - input_natoms = [item["natoms"] for item in sampled] + natoms_key = "natoms" if not data_mixed_type else "real_natoms_vec" + for system in sampled: + if "atom_exclude_types" in system: + type_mask = AtomExcludeMask( + ntypes, system["atom_exclude_types"] + ).get_type_mask() + system[natoms_key][:, 2:] *= type_mask.unsqueeze(0) + input_natoms = [item[natoms_key] for item in sampled] # shape: (nframes, ndim) merged_energy = to_numpy_array(torch.cat(energy)) # shape: (nframes, ntypes) diff --git a/deepmd/tf/descriptor/se_a.py b/deepmd/tf/descriptor/se_a.py index 0e15ba13a8..4635554610 100644 --- a/deepmd/tf/descriptor/se_a.py +++ b/deepmd/tf/descriptor/se_a.py @@ -154,6 +154,8 @@ class DescrptSeA(DescrptSe): Only for the purpose of backward compatibility, retrieves the old behavior of using the random seed multi_task If the model has multi fitting nets to train. + env_protection: float + Protection parameter to prevent division by zero errors during environment matrix calculations. References ---------- @@ -182,6 +184,7 @@ def __init__( multi_task: bool = False, spin: Optional[Spin] = None, stripped_type_embedding: bool = False, + env_protection: float = 0.0, # not implement!! **kwargs, ) -> None: """Constructor.""" @@ -189,6 +192,8 @@ def __init__( raise RuntimeError( f"rcut_smth ({rcut_smth:f}) should be no more than rcut ({rcut:f})!" ) + if env_protection != 0.0: + raise NotImplementedError("env_protection != 0.0 is not supported.") self.sel_a = sel self.rcut_r = rcut self.rcut_r_smth = rcut_smth @@ -206,6 +211,7 @@ def __init__( self.filter_np_precision = get_np_precision(precision) self.orig_exclude_types = exclude_types self.exclude_types = set() + self.env_protection = env_protection for tt in exclude_types: assert len(tt) == 2 self.exclude_types.add((tt[0], tt[1])) @@ -1436,6 +1442,7 @@ def serialize(self, suffix: str = "") -> dict: "trainable": self.trainable, "type_one_side": self.type_one_side, "exclude_types": list(self.orig_exclude_types), + "env_protection": self.env_protection, "set_davg_zero": self.set_davg_zero, "activation_function": self.activation_function_name, "precision": self.filter_precision.name, diff --git a/deepmd/tf/descriptor/se_r.py b/deepmd/tf/descriptor/se_r.py index ba1a261390..9f88ebe37d 100644 --- a/deepmd/tf/descriptor/se_r.py +++ b/deepmd/tf/descriptor/se_r.py @@ -104,6 +104,7 @@ def __init__( uniform_seed: bool = False, multi_task: bool = False, spin: Optional[Spin] = None, + env_protection: float = 0.0, # not implement!! **kwargs, ) -> None: """Constructor.""" @@ -111,6 +112,8 @@ def __init__( raise RuntimeError( f"rcut_smth ({rcut_smth:f}) should be no more than rcut ({rcut:f})!" ) + if env_protection != 0.0: + raise NotImplementedError("env_protection != 0.0 is not supported.") self.sel_r = sel self.rcut = rcut self.rcut_smth = rcut_smth @@ -125,6 +128,7 @@ def __init__( self.filter_precision = get_precision(precision) self.orig_exclude_types = exclude_types self.exclude_types = set() + self.env_protection = env_protection for tt in exclude_types: assert len(tt) == 2 self.exclude_types.add((tt[0], tt[1])) @@ -776,6 +780,7 @@ def serialize(self, suffix: str = "") -> dict: "trainable": self.trainable, "type_one_side": self.type_one_side, "exclude_types": list(self.orig_exclude_types), + "env_protection": self.env_protection, "set_davg_zero": self.set_davg_zero, "activation_function": self.activation_function_name, "precision": self.filter_precision.name, diff --git a/deepmd/tf/infer/deep_eval.py b/deepmd/tf/infer/deep_eval.py index 45eda3392f..b9db0863b5 100644 --- a/deepmd/tf/infer/deep_eval.py +++ b/deepmd/tf/infer/deep_eval.py @@ -4,6 +4,7 @@ ) from typing import ( TYPE_CHECKING, + Any, Callable, Dict, List, @@ -693,6 +694,7 @@ def eval( fparam: Optional[np.ndarray] = None, aparam: Optional[np.ndarray] = None, efield: Optional[np.ndarray] = None, + **kwargs: Dict[str, Any], ) -> Dict[str, np.ndarray]: """Evaluate the energy, force and virial by using this DP. @@ -724,6 +726,8 @@ def eval( efield The external field on atoms. The array should be of size nframes x natoms x 3 + **kwargs + Other parameters Returns ------- diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 8bc9104b16..5e8db431f8 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -96,11 +96,35 @@ def spin_args(): doc_use_spin = "Whether to use atomic spin model for each atom type" doc_spin_norm = "The magnitude of atomic spin for each atom type with spin" doc_virtual_len = "The distance between virtual atom representing spin and its corresponding real atom for each atom type with spin" + doc_virtual_scale = ( + "The scaling factor to determine the virtual distance between a virtual atom " + "representing spin and its corresponding real atom for each atom type with spin. " + "This factor is defined as the virtual distance divided by the magnitude of atomic spin " + "for each atom type with spin. The virtual coordinate is defined as the real coordinate " + "plus spin * virtual_scale. List of float values with shape of [ntypes] or [ntypes_spin] " + "or one single float value for all types, only used when use_spin is True for each atom type." + ) return [ Argument("use_spin", List[bool], doc=doc_use_spin), - Argument("spin_norm", List[float], doc=doc_spin_norm), - Argument("virtual_len", List[float], doc=doc_virtual_len), + Argument( + "spin_norm", + List[float], + optional=True, + doc=doc_only_tf_supported + doc_spin_norm, + ), + Argument( + "virtual_len", + List[float], + optional=True, + doc=doc_only_tf_supported + doc_virtual_len, + ), + Argument( + "virtual_scale", + List[float], + optional=True, + doc=doc_only_pt_supported + doc_virtual_scale, + ), ] @@ -203,6 +227,7 @@ def descrpt_se_a_args(): doc_trainable = "If the parameters in the embedding net is trainable" doc_seed = "Random seed for parameter initialization" doc_exclude_types = "The excluded pairs of types which have no interaction with each other. For example, `[[0, 1]]` means no interaction between type 0 and type 1." + doc_env_protection = "Protection parameter to prevent division by zero errors during environment matrix calculations. For example, when using paddings, there may be zero distances of neighbors, which may make division by zero error during environment matrix calculations without protection." doc_set_davg_zero = "Set the normalization average to zero. This option should be set when `atom_ener` in the energy fitting is used" return [ @@ -241,6 +266,13 @@ def descrpt_se_a_args(): default=[], doc=doc_exclude_types, ), + Argument( + "env_protection", + float, + optional=True, + default=0.0, + doc=doc_only_tf_supported + doc_env_protection, + ), Argument( "set_davg_zero", bool, optional=True, default=False, doc=doc_set_davg_zero ), diff --git a/deepmd/utils/data.py b/deepmd/utils/data.py index 194c6b1e24..1e1d7c2251 100644 --- a/deepmd/utils/data.py +++ b/deepmd/utils/data.py @@ -491,7 +491,7 @@ def reformat_data_torch(self, data): if "find_" in kk: pass else: - if self.data_dict[kk]["atomic"]: + if kk in data and self.data_dict[kk]["atomic"]: data[kk] = data[kk].reshape(-1, self.data_dict[kk]["ndof"]) data["atype"] = data["type"] if not self.pbc: diff --git a/deepmd/utils/spin.py b/deepmd/utils/spin.py new file mode 100644 index 0000000000..38e8da48da --- /dev/null +++ b/deepmd/utils/spin.py @@ -0,0 +1,199 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +from typing import ( + List, + Tuple, + Union, +) + +import numpy as np + + +class Spin: + """Class for spin, mainly processes the spin type-related information. + Atom types can be split into three kinds: + 1. Real types: real atom species, "Fe", "H", "O", etc. + 2. Spin types: atom species with spin, as virtual atoms in input, "Fe_spin", etc. + 3. Placeholder types: atom species without spin, as placeholders in input without contribution, + also name "H_spin", "O_spin", etc. + For any types in 2. or 3., the type index is `ntypes` plus index of its corresponding real type. + + Parameters + ---------- + use_spin: List[bool] + A list of boolean values indicating whether to use atomic spin for each atom type. + True for spin and False for not. List of bool values with shape of [ntypes]. + virtual_scale: List[float], float + The scaling factor to determine the virtual distance + between a virtual atom representing spin and its corresponding real atom + for each atom type with spin. This factor is defined as the virtual distance + divided by the magnitude of atomic spin for each atom type with spin. + The virtual coordinate is defined as the real coordinate plus spin * virtual_scale. + List of float values with shape of [ntypes] or [ntypes_spin] or one single float value for all types, + only used when use_spin is True for each atom type. + """ + + def __init__( + self, + use_spin: List[bool], + virtual_scale: Union[List[float], float], + ) -> None: + self.ntypes_real = len(use_spin) + self.ntypes_spin = use_spin.count(True) + self.use_spin = np.array(use_spin) + self.spin_mask = self.use_spin.astype(np.int64) + self.ntypes_real_and_spin = self.ntypes_real + self.ntypes_spin + self.ntypes_placeholder = self.ntypes_real - self.ntypes_spin + self.ntypes_input = 2 * self.ntypes_real # with placeholder for input types + self.real_type = np.arange(self.ntypes_real) + self.spin_type = np.arange(self.ntypes_real)[self.use_spin] + self.ntypes_real + self.real_and_spin_type = np.concatenate([self.real_type, self.spin_type]) + self.placeholder_type = ( + np.arange(self.ntypes_real)[~self.use_spin] + self.ntypes_real + ) + self.spin_placeholder_type = np.arange(self.ntypes_real) + self.ntypes_real + self.input_type = np.arange(self.ntypes_real * 2) + if isinstance(virtual_scale, list): + if len(virtual_scale) == self.ntypes_real: + self.virtual_scale = virtual_scale + elif len(virtual_scale) == self.ntypes_spin: + self.virtual_scale = np.zeros(self.ntypes_real) + self.virtual_scale[self.use_spin] = virtual_scale + else: + raise ValueError( + f"Invalid length of virtual_scale for spin atoms" + f": Expected {self.ntypes_real} or { self.ntypes_spin} but got {len(virtual_scale)}!" + ) + elif isinstance(virtual_scale, float): + self.virtual_scale = [virtual_scale for _ in range(self.ntypes_real)] + else: + raise ValueError(f"Invalid virtual scale type: {type(virtual_scale)}") + self.virtual_scale = np.array(self.virtual_scale) + self.virtual_scale_mask = (self.virtual_scale * self.use_spin).reshape([-1]) + self.pair_exclude_types = [] + self.init_pair_exclude_types_placeholder() + self.atom_exclude_types_ps = [] + self.init_atom_exclude_types_placeholder_spin() + self.atom_exclude_types_p = [] + self.init_atom_exclude_types_placeholder() + + def get_ntypes_real(self) -> int: + """Returns the number of real atom types.""" + return self.ntypes_real + + def get_ntypes_spin(self) -> int: + """Returns the number of atom types which contain spin.""" + return self.ntypes_spin + + def get_ntypes_real_and_spin(self) -> int: + """Returns the number of real atom types and types which contain spin.""" + return self.ntypes_real_and_spin + + def get_ntypes_input(self) -> int: + """Returns the number of double real atom types for input placeholder.""" + return self.ntypes_input + + def get_use_spin(self) -> List[bool]: + """Returns the list of whether to use spin for each atom type.""" + return self.use_spin + + def get_virtual_scale(self) -> np.ndarray: + """Returns the list of magnitude of atomic spin for each atom type.""" + return self.virtual_scale + + def init_pair_exclude_types_placeholder(self) -> None: + """ + Initialize the pair-wise exclusion types for descriptor. + The placeholder types for those without spin are excluded. + """ + ti_grid, tj_grid = np.meshgrid( + self.placeholder_type, self.input_type, indexing="ij" + ) + self.pair_exclude_types = ( + np.stack((ti_grid, tj_grid), axis=-1).reshape(-1, 2).tolist() + ) + + def init_atom_exclude_types_placeholder_spin(self) -> None: + """ + Initialize the atom-wise exclusion types for fitting. + Both the placeholder types and spin types are excluded. + """ + self.atom_exclude_types_ps = self.spin_placeholder_type.tolist() + + def init_atom_exclude_types_placeholder(self) -> None: + """ + Initialize the atom-wise exclusion types for fitting. + The placeholder types for those without spin are excluded. + """ + self.atom_exclude_types_p = self.placeholder_type.tolist() + + def get_pair_exclude_types(self, exclude_types=None) -> List[Tuple[int, int]]: + """ + Return the pair-wise exclusion types for descriptor. + The placeholder types for those without spin are excluded. + """ + if exclude_types is None: + return self.pair_exclude_types + else: + _exclude_types: List[Tuple[int, int]] = copy.deepcopy( + self.pair_exclude_types + ) + for tt in exclude_types: + assert len(tt) == 2 + _exclude_types.append((tt[0], tt[1])) + return _exclude_types + + def get_atom_exclude_types(self, exclude_types=None) -> List[int]: + """ + Return the atom-wise exclusion types for fitting before out_def. + Both the placeholder types and spin types are excluded. + """ + if exclude_types is None: + return self.atom_exclude_types_ps + else: + _exclude_types: List[int] = copy.deepcopy(self.atom_exclude_types_ps) + _exclude_types += exclude_types + _exclude_types = list(set(_exclude_types)) + return _exclude_types + + def get_atom_exclude_types_placeholder(self, exclude_types=None) -> List[int]: + """ + Return the atom-wise exclusion types for fitting after out_def. + The placeholder types for those without spin are excluded. + """ + if exclude_types is None: + return self.atom_exclude_types_p + else: + _exclude_types: List[int] = copy.deepcopy(self.atom_exclude_types_p) + _exclude_types += exclude_types + _exclude_types = list(set(_exclude_types)) + return _exclude_types + + def get_spin_mask(self): + """ + Return the spin mask of shape [ntypes], + with spin types being 1, and non-spin types being 0. + """ + return self.spin_mask + + def get_virtual_scale_mask(self): + """ + Return the virtual scale mask of shape [ntypes], + with spin types being its virtual scale, and non-spin types being 0. + """ + return self.virtual_scale_mask + + def serialize( + self, + ) -> dict: + return { + "use_spin": self.use_spin.tolist(), + "virtual_scale": self.virtual_scale.tolist(), + } + + @classmethod + def deserialize( + cls, + data: dict, + ) -> "Spin": + return cls(**data) diff --git a/examples/spin/data_reformat/data_0/set.000/box.npy b/examples/spin/data_reformat/data_0/set.000/box.npy new file mode 100644 index 0000000000000000000000000000000000000000..1f72eb7185497167688c573cd800c4962932eca2 GIT binary patch literal 4448 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$d2099snmP)#3giN=-9J?f*y>aq#DCuDyv7@_Kj2RHyNH>F`%ka;_$(`= z=%8wS$4hpNn1ieKwtHtotnEbtckbc`iqG>{nYm#)i$mF|i4DcZA`YYGjE2u>`Wej^ qqvghEc{o~MjMk^4?S#>G)M)!~wEaBVFBt6?jP?sg`vn8lF8}}r5Jfux literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_0/set.000/coord.npy b/examples/spin/data_reformat/data_0/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..4b60ae0e0bb627d0fd55de43e9e6fb0ff0d79ad3 GIT binary patch literal 46208 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 literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_0/set.000/energy.npy b/examples/spin/data_reformat/data_0/set.000/energy.npy new file mode 100644 index 0000000000000000000000000000000000000000..8754b6dad25e9c00588bb4fb0f1eec06cf10e043 GIT binary patch literal 608 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$d20EHL3bhL411@nt-EA%>Qx2SwuTW$H(sxe0TxkoW8L#;-VK|j?U|!$4 zd|@CzW1{}di$J=!G@|aziIf9vPuH!C0rEfkRlJY~(kA~(L-zv3?JvwPCHiB3Z#x2{s}pjL@d4=_-~1av_86ANKYs~SKcmLJyb8#Fc>U?~6F~mL zujj*O0{QHcAB|#xbWF6U$$Fr;*CD}YeL()J+u~g2Ksv2+t%?(np3O9K?o%LbC;8e0 zWPg*7ie~pSO1d`KOkoYM2A*1O<)>3xVPq z;Vh0bfV6LbT%j{a-SF^mycS~t+Z3l`Unju?$1jtWjZB1GZ zcLBv+cM2Em0Mbj+9qv5`($=?EuV4Yn*91;_U<{;h+o&tt0*W`! z_>t%X`?!h8Ax}kN93CU#bq+SL~a4npLF)}g2LZFufYx^ a-up=z4zXGXC+BgMx?0MK}A+?nWdyc%4o<) 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

I(#)m-&!|)N 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 literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_0/set.000/spin.npy b/examples/spin/data_reformat/data_0/set.000/spin.npy new file mode 100644 index 0000000000000000000000000000000000000000..c426f1c7f67fdedfbed3ffbf275952f9945bc042 GIT binary patch literal 46208 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 literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_1/set.000/coord.npy b/examples/spin/data_reformat/data_1/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..fc51107998e65aa7835697b31c0147af716af723 GIT binary patch literal 46208 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 literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_1/set.000/energy.npy b/examples/spin/data_reformat/data_1/set.000/energy.npy new file mode 100644 index 0000000000000000000000000000000000000000..a0eecad8d83f473d4e04ddc002f83b296cef9188 GIT binary patch literal 608 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$d20EHL3bhL41Fo9sTMsTjnR39Vyw_*iiIf9(MK@fQ1M)Yp7>4KpX-$5W zAax*}^lQeun?U)c_l&e%fc)}e7lsZX9qr}A76zox91F6&1EkgX@|J!Bs{3>Ci9!I7 zueDu%+f^Xn?URvMK9C;cvQ3s?=b{M8H0O>bwM=bS$G+)(1 zw^h!uK=5_SZGRwtZGhv&dqCR%>{B*pAZ@8_{|Dqx_w`4(qk(i;SlZGSpt;-= zXX3nrkt{v5%hAbQ4>NdXUl;#{Xsyo&+StKN1h bfy5Onk2%Q#`4iTER@DO1=O0y@g5m)Hx3L=m literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_1/set.000/force.npy b/examples/spin/data_reformat/data_1/set.000/force.npy new file mode 100644 index 0000000000000000000000000000000000000000..ec4a05f8f25ee759ad2e8c1d6c86ff210d38911a GIT binary patch literal 46208 zcmbSy_dif?*ncQwlgdg)$;jUGviIJ5@4fflD;bD#T%b6&4=UDtcvMfzrXhE@cJ!Vkrqb9D=G2|g#oc23S+>fCv@bM67b zp}|i64gtZgZvSsx-N`q^?O;8`(<#vH;GSDT=scUW1kZW4IJW=eMJAII6<=X_0fR13 zuFEadK^mvs7EzHdHX1tX9^P<;i^5AyW~02|yxBBt2*!W}q2`3a6EHGH5i^dAo*}L7Ee#`iZ;lK>x`=toNEREYgPz*Kq5C zd)yx{)r3U+>>po5?iYc+g@Pn6pJ!rRYRT!?sWc>}cp_*l@BqJzzjJt1brsvGh_BfU znZtR)_L|!?+)(Br)%E5MKxyc;sl$UIKpK!6@!l{T)6dlEB{@5xpI2wY%qvHr(T!`k%Vw0$6@+>Bk7e-(LVNl2-roliNLhF%`EiUR_UhS3M?E*k zd%}iZ-cn9DvgQ0m$Wj?&f3y*^lX=0r?e63`IU9_d3;QKbr;O)wB+{y5f-%U5fieDF zDils`SqZ8~0--#sYHW}R8niE7*s|8e=U!X{U+PRzZ<>m|s4E)IRSks5o+|>8A8E%7 ztF6#rjW$br(+AhD_eWY(lS3%6SNGA#GjNKfw&Jpv2c}S5U8Jqn0X}*Ok$ZIvkpE)D zY+f=1bSkChZFiCY`?&M#3^O2HP}4eaIvUii2)eyP6QO@4uwdm>1`HLPNO~^ik6E_% zz6-5-;CuXC@oYaQc#--UP>BYj`6V|fDc?eD)Qc0Z*N8~9Ysw7^p*8QM0lwAx0waS{Lbs~W0?~U_+)PjM7IlAJnN)UdgeEem~ z&JlEZa-1$n{p+9bYChKE;Eq2Wlx1ruuA#IpiTK|?ZAh?g+0EAEf>~kagO1;#aZvA& z$^GF_gy&{lPfi4(PTt^qzUV>_WWV+JRhJqp{j*s*KbMNErqYg$>X|^?RN;ww#qcdQ zoZCvq9@SSWiKl8J@WlB@!u=H|w74Vj+3sjP$hdpEa;vw2pE@nck=^Hz*M?S2x5?Va}%UzSa9)#K)|nrdy3HWzMVc8Nn$*@g&FemhKF zdw%b#UKq9%(ib@^WSs^e>FxA zYiV8MeyNy3hveC5-Xk1nXqm+RGx``DpDDC1i2bhc;|Q@-S)C02SpLj2|MonPt&3hE z7A3$yn80A`_{OOs!HVwpL!PtSC8L!Pp9;U zaUIc28roEt`g`3zjMf#tGG7ThsTdA&cXbZ)bo)R)L|BN~Iv~|KCspAu{!ksd->zt` ziR)90w}_h0LeaHfqvn}hU|qCS-{g86CoH@_UtLr}n#{7or|;BI*&$Y~-$@eQtn_s_ zGIN2iUno~a=vnBFq^2RM=YR(}QHpfTilAhZHcp@{fOI|IxMd_c!1n2zkWxNzSeaxd z;(tVdF3fKe>dA$H>-wFn#FkSqII*(|L7c$zT2J5T7(GNcmPy*gnIfI<)eAqDIq|7< zg;<@024<%32~50{gz>j=8-+J+QCycY@;Qd}fw_j)y@a4Fka@b{m^S9Aj|_5s;6cL*QgzneQ_!ZS{w4L3 zFJ}Fa-g!Q+hlEBSSR`ni@Ui~yRuKVDm@uAetGa3gjU88QtFr7svRy}ew_FBnCk{p4 z-W9+`J1Y5i!P2;Rw)(ZEa47H!>CBMLDZ`$yAwkBY{r=Bm0?Ph5{OBxA|D0HRL&3$b z0ZUR`EA3>t|#;hUel{*j!xYgW|uXIL9{b@emL^o-G=XMIt- zcTM43K|>Df$uoGmQZb-oM-Z~(=1!)Hali-VjNx-_#(4WrG@mYs9h z#mzj5XyfBMN&kW#>>~vnKFl%WYF_fM_P5H&py&U=WAHrah~B-ZZX}8(w@mPgS5%|YXab$_p;0l8b-zQHN_`Afs@Hfc@);@;ZNf}TD zxtHShVnh5ua+rq|vrmKiRe6TK0R=oAJtW$Cgda!fi&^bntKkP8Yq6f^Mli%uIl39G z2K2VdF@!Hwu=MY8@3m+n%sO^=BVU#lLtR#$-C|coljt>#1Yrj7Fgh}1d0Pa;qz#M+ z%>n6m-5O08gmJd0T5aTZJeY)eHIY-t;>N!u5S8{uCvI<7-_{(MdmU5a@i7l1HY}al zPntpFy~*K=p?MJ2(dR_}UKn%g*|kjgpovC!QuHs|3%_`dGbKdRTXq2yz)q*DG4U|Y>#&|#R4sl z%ouWKfnsxLFt!uI!p*f{?#DObUg(bJ#T;))pNq|@wrv3ti`lTd%si;KcaxCT?JThL z@jLt$@WAhTvjxBT(y)s)$0p!-1->)u<8w(ZhuLp*mc@p1mHd@31yidFU@ z7PV&H&G56udLGXqjgCkZ-TLv%#=4HAwq`_@G#~%1$vn zJI9qe`PdLH>g@(?ks4z5NPhCi=lXDx(mH*g*cmsIv=c7~`QkB-8u=~PC_K@r(ZUoL zf+t_h7=?7Z;3xA2iPoG8xEgp-%yat;crlmXxRhgrGsU529$GtM+E;#!Z=|8{*H)7= zDZmHns(OX9M%W5bE=+>gho45qS=p=yxRv+yx>!5rB- zo%U7ggW+`Nn;70P2l#OPo#Ss~AC$X%?baVAFUX7>p#JjO5$AH(Cz*7e&^xVRwAtDp zwVms^&%E=*n|AE#i%<2j)YW-ng6XTm&*#IhI3i{gMEf$8{c*4ClokFD&oJbul5s(+GS5`kM(FNi|U%{4{Y$K zJ|2FhpFT|YuH1ewFwysQ9C3Dm@7JX4OO7idFROgj6MlM7ww9^bl4FHxTWR}s2T?p@ z%<)mSgAE)Mt0F&pi=mUw)3Ch_NqDG%XWFQP@y@xxI9EOnY-N0A_NB!RAHQ^?X`l%P zqwfY(3OXDx7iOHiam^0q&zHR}K{gDI?l6DqE(SE|bKebqNn&L3K>sQWJ#K$V{#ESA zip|E-FFis;;hi57!JVxO+6Uw)M0U9<+-7&>kHmI@|gD0lhVRlAk@U z+9U=Zq=f2P4jO1B?zHgHQV$cR9@8sd;6lmehhY=<`#CY}n#s~QWyTM~6DpHWq4*wgE^4`zo489a1UuI6Z< z|HA+9s1OP{`R&-Np2d=s%GkGo=HM*uc63f#2w0w9$&U{Bq%fSB8Ky!ai2w2Ot=;~r zifO;hysKRFp?=DKsHxBx*H>iXvKp;`RJ=5`P?ZB-tk*2{O>^QLFVh3-zkK*FT7&ve zwg^6t7x>)PrvVn_>;7W}dO&9X`zmFkBHV0bb&>T}yFDkIbQoroY!O3NzWb4#A|y!tB1D^PEeg9f%dTDI8h|2| z%jYd-Z{Zm`1N~AnbtDlHcYV*140){%b}w@Zu&9QL)+P5AHrI}j-K#gk4pNHqwl25f z=tYmPO@jz{H2raNi0lHUOSO7SkTw9fS;9OcX)<0o z?u+WbT5d>%!RF=SGjDE%;{N`Z7thD?VK0X7XUN-n{M%O|l&R{8lFU4xQhQWjP}{V51stwbH~@I--C@9fp->)RLBNGovG=Gp0v z@3CN`f0*Is*#MZk@}S@rxe-{dC>aPkC*Yjco<{py4^S7kny1sr#M4_d=F7+r8Qs52 zYcm~SV~{1dqoz@r~&6~@qvwPXD~bQV_P}G8*1+`7z|XW;q_&IyXQ)V z_@H{EPv0UGZd`fVsdo2me+oma3)Q=1WRWckU_g8!5w8`Zq~g{Bw?S7qVz zt+j%u-b(q*zmm|vlT2mDCmyC*ol2HclfWpGRe??<4El}gXe2)c;&UBIx5p&jcvtuj zs|Pyc?{N3}5jF)3pAgPxIH`z38^`&V)#X9)<#)F?swQBrOqixxp^C~c8IC3^n*isj zV+)GWJecdq+%$bKkCoD8SO+_-alQUFd(6Bn(p9gP<^R;f*~N}!DsgjECQLmY)@q6m z8SHG13UUDP8^T)2eiKkwZw=DC!w;ur?r`2KU;~A>tW-P-%CLJ^pyCX%AvoQo*%+Qt z1!k`o&&YPwQL8Q0ZDoTf_cQE@?cj7PKtVxv-z z8m*^;0kJpxe_R4!ppiLb4rwT_p zwz;<_Ey1|xX~}wl6UwAmh)R78!T7Qh_X@S}iIc!r6-g=ne$!ibOBUPk< zN`Xo2^%Ng+U;TVs-ukx!<=t&_4}5sM zJ@d1|Bz461(rQNJqh3y2$x?!v+QO?Z=tXhkb@)9q4FlXsSiEoeS{a74=spP88N+?< zGKu~(W)QuurK?VAhX0htlv>Cv5kE|}E0YPqwD;8bNn=IyPUwF4R96Xy*1Wf5xz}O@$ZX_S5p+k*Iew1|6q&*btAiZby=|aO{3%)Ss-3;3{3RC7zon$N|TKXnUI1q zgd=9%1;&E9gv*&6kVoaZu6?E>*d+6>eB86a!gm5aLyW#qUGvHRzmQ0Hs6I4I@!c6K zdD+W}`hwx40!8Gp|4e~j%xPHtt`~f9xT)$DW{=cS(Rl^W62b0}!6oJ=9vJMA#N=q_ z0~c(Hw63{30Pm1$rtFm|0ArpfW|Z(V-67}SXwPw^E$-|q?;)^ zhNN`iXY`2rPp)$?93$a8{oy+9H77HfbJW2J%3s!uuB|Al`peGK+YR1_&G00ssKQBC z_xM0Ib@)c9c%d`O5k_Ej8!T;3qD{||#E$oKwS(p^ z_$9--%=)(y=FFJ*6Gcd(CL0CEnzSoOhdp;(E;5FktdkV&+@c7nwNqw~+;O(zE}8u! z3yjP>?)=k-4m=E$&&-XdfP~VJ$=(Yic*6FPX=5-0Dm6WyYB%{qmYvS0(OEYX&C<>+ zobv^RSmy@Pepl@6`AvWNn-TxB4W*Zk z3V%4z895&++-G95LblDX)pYko@xObzC9$8a@le?3S1&CTK~*nON{n849ONT`O!v!W4K%$(50$D3Jz&J=|R&$c#S5(V)`f}y;@NuWmx8kmbTT8nZeSeuSx#HX2M!;uJhJ>IAK$y5IP%|UGF~X#p7_<61YOSG zjM-W;@s`tg)yOqx_r>W6&YEyjRH}K_lHPxN)V0EsCHd$EA!YL&9Ic@6*rnj>%pAgK%k=xsE*;ERU%KSdr;1$=LG>u_f72j2Oynj|LkqXM*N%MLLSJ` zg8Y$VHPgExcq2Vb@q@kz5^NAki?CRs%Poaiju)ZGT5))be$F0uZ?CHROH1I!n+iD< z!VLKOsr_DSj~OzRF;0ESii1lu-7i<)M&RO52@&Z5@1k9kx)=FT6XQ( ze7o>X<)U#8#ujz>yu2TVC#6gsk6a1|x|2aHy_pvaDY>1R_T!!^k2Xj7`cYpnn_&4^EzY zsP)hkUYxq9)2V9<%%l_gS`dUcf6DQ2QrqIa=CzxDBCWAtBK&3QwJ@N6MDjde#sjEB zA}F1^9ic=mp}XHI3u%HJ)J5zpK|Zi+Bd)63xsxXAqa25DsYfi87S_g+0SvP}P=`6}lQdLI=2+6o2ZUvjar z^f-UCkp9k>U+ISHGftxP_bq@yME>>!F+KJht}DJ?X2-UBtJ|eUtZ;sWZ=si!7y4fI zuX!9I1d&J@-V&qpKov+bkYZige}hTG{K>|ag3k27tZyhU+QtQ%48118fSHd!tOiMu ziCjRd#hw>}D-S0vz!5lJ9o**HN(>_IwU3Hr&LbnaU2~7tWIv&Xp99nBoqmb!FD$m+ zr}5!8%H^9HyqGNTu7zBN0LMk9+^G_J>I!{Zu>$-12P6pFYfn!-Lva zXpbL5!BY%Xr%8V(oZpLEA+%)yrq-IX?nDdyT6k^XUIQnp(za+%?;VA`hFl_VQ%3x) zeKY-g|0#HQ^ZYGB`@8*Lz6{&QlrrJH{j8z13^qK{%M$8K+>~;CcXiEHJ9z`IRtOyS3 z{e4~ZqA(xWu1Fw%2)lB8zCGdR!%v$fzb>_^0qyhR^iW1+DDC{HDELVT-%fu!O+TfG z%R-K0k1N$-Z-#W`cC`}pbXOWApXP-tuRLeJB{IUeqwU1Cw7T@{G63U)AkGZ|Ji;i4M7(MU#Uam$IZ`U|T2=*&Nk%uj1fjBKLRcVFx zcPN5h^2452eVp)(GzIQ(OQL+@q0QbOym9^eIv#-*hB_ljc&`*JZ{)%T-CSfXd5+v@tTDlHQc4-w zG_2##cC%rO*Y@&hWo`T>7yssv1HkUaZgCQ-0QsF4`*-JKae(i^uh#igB&c^0PfeAD z4AJly{wIEL?h)yPtw8|J9DUAXN$Chr*=%`xbM3LX{Y_rXm_O*U&^bHf!Ce2iQ1#lw z4P&^n4-LvH;;Ag@ghJ*BoPV5a^t#X$+W+d*_0ZblW=1EHzc}#8ILvoTNRm+8PK|Jd zs|s$#GZYmL#Nd#@*NUFcp z&bUDUoz4kgennI>d~cRK><6(QlCns6E&+dnEYq9iYnZUY@$bRK8?Yo2=rh`p2!3Kp zCc7({pq=H!WfWPAG~p@636h>rdS_W`@0~OB_+BUT9ty$nZW*Eq`9iqGs*vI=<%~ki z{-r9y0jQNUw{0A&gS3m%gIfHw$lFNi5~S&jW_`oWl#79&QB|U#-0us%{>I_vUcPWh z^ttYgkt2F^o~6+rxB#M;#w`9*hyaN=m%GKqF?i~W%F}5=JMiM%T7TA&h<)~^JTf;@ z;LR@Y(Mv(@cp}wQ^$bY>ewX1=)H1YyFOUEGs5YdG308iOea1Xs^X?DY^)Pd^4jPT# zu}Fd+sd7Y1cG39Y`01jY2O)57Y;m3Awl#FVe?nw*LDk-U%|NA zc$(7wh6UtAU77G^Oac$V%Gj5F>3HW$A*4$iBeBcI<&z^^(B^gY$8xeFYKZ#3&kA?M z(0dx$<~GKta(nleZG@og&)pMBwBc%Uu(Pi_G4Kxf|%hT`K9q3Vm^X5T@C@XL& zJ$c$2e$zK_$8rY(#oE={&`2i`c{XxW_hT^dReIRgZHB|IZMxR^-)j3JMF{Phe6ekq(2s!k6`T)gvG$ z#31yllQ%48eEQRJ!4e(XV}7diI00j|@YyIrcSxd$VpOy@f{UJ?MhTgm(Z_FnsA*Xb zy&2e2&hI#a&;~n!n~FZ{K73^{ovI3tV`~~hj_0Dld%9mL99L1A?_ZBbdINrQzS^vz z5{j3F#PYdbMItw4?0>K5busQeG4rQEceIwk3k3@l@L*odIe$tE1dS{ow%B^(quKq} zs|}HGp+4967Iy}mX)YBXh_{D`mx;&uxujv`yUO?4NJ4O3eRtI*-T_>fp0V?XNy1pf zys28461+=MOyuH@Mb+X2pw4Ws{&X_Evj z8)Prs|GMFLeSM2ukqWSH_W!=4sD|Zl(_eA+9_ZPX%t}*Vw9(@~#XoXk)Yw>Wd@WEl z7!m zpA3Qd0?D^*v7X>Sw#stkpA&o(8#in&6hXEIC*2onB-qm&wKjUk47D9UPyR}E!e=1~ zuNi+^<4e=GGgtWnK{Zi%mF9gIEK|>IpSh3%X)0FHbtSea!yT6WijWUVy~)aQ-ygye zvTo7jRC_#WBJMZ9x~af#N__4}HZd5gTkmhI9PqrN?n7i!3D8IMNmW2j5(9L}p4ELc zMaMLw;|iB_5BTlR$veA>7{Mgp%S^6{?5PW(kf^uh47A~f!DMuR(kl#VJ;3t;oNs)ZRB_s*I1-*Qk z^ePiA3aiCxtnIO?#@Sjt+y}cdeok%I7hwCL3KA&GM)ii~Lg(FBc$PLUD7*rAx4U{_ zH7y1iR&JQ|s~g~L-B_Y0pORrnjrP`mC0=lT*pMKA&cH>p1+ac?lo7WufeQ0~+V&DKKA8?lbq-37$VSRlU#Y4WY*_7P;O_ zft4Tz8TK$Y7_R-<9$!p=1Yv*9aJ{{Z#h}9Rc_YptY)~R?nUHf6NTR02GEtkxP z)$`zJjS36nWHKx@_Y8QCrND{x`n*PtaAXb+WzlPJ0)q0OFS#2jV6YYZ(BPN~wn^_c z3Gu}sL617o`8an78tG7{H9yG5B-HQN?Abt7C*cP}_EeDe?A@8QNrcv&d5N9&FxXHg z)p5R+34)SG7RCz|(Uiu}DorW^e^S#VOj)G^!*3T+CHV*(bYEJJyzK%6r>CU^PiDY* zAFipL{T%qPY_XC@m<(G+4!4pF^3ZsHxXal$9s7hBS_tnZV)bf`-%EjT*eYCW{gvnf zU%4r}v+Ye_D)$6`3u!pa?$f^4jXIbwGGuQOwynToRc8Db>0Pw6M}5=4yXg8~&Squc zbyQ=!>7g$W3WX9`=C^4?;L8>SjFg@|&}oN>!=j4uu<`tWZd5M#pIuAxef>vaq_I9n zaHS=@*25^#ZnZGi@#f~P& zEB)zU-b;Gqey*tGTJ#>I8Q#Nrk5|Owe+oyY1Sr6qe%^nZbR!66k&kwylbOD3{ zpE}+YDFS0gqHIe_L;M!UK9Rqj3*;7-6FYndJbZiZVogl}9N#NVvpN-xNvAfo9@iM* zdW|w!hmJOe>>V!qSrY+6;}*Umo2n3gOp7m|Q3ssXh#Xg*g<~q)z``|OSA69hF>~k; z9o#9i>57{R$IDkb)0voz(L(+y!If?^d?`5EFTt1z!DHXtYgq!pJJUqm-Ayh3@Z8Vrl=s5e=v!EKC3 ze6OP!0p1<&pHWz z(=937Ydi4Qr|;)T&AQ-boSeT69)c_3v`bUlCMaZ2GV|)A5i&d%ADf6Y#?KEfm+k%$ z0fSV_fLj+*kyLW^sO&7HP zO1b{mI}RZ3lS~}55B%=E|D9ph6P$+Z0Vgg|)FExEP&G97@Pf(@2BQ9wjdB@PCi=qPjgANUms4eQi$yy)`&d#Kd z%e*4NC8+G2 ziei78AF{<%t?yI_g2$-zJ@-RNaN2PG{38ov(5*7s9dh%5KlU#lnE8kyHoE;wvPQtCjJ#GPTx?ueK=uYOybN-yc_K7fTvHB;&Q+kAe{b zNg)1TT;2Pk5XcjW*d{Ye#rPw&^Ov^ru-fsmUhb`8B<42>b<1l){tW5Q3$Lm%T}wSR zz99x&>6T(xhHBwa*j>i)x&icdV4jZCjX<}ldhVlb2k-kf?nc$C0Em=l(R*~DtCG^c zt30=047FS{vyY9#!S0j$g|8`B(Y#~VY`mfFARlPCy!NgW95uhPr$+_hYWhOiHQzLN zb-Z4tXr~mZGREi3lk!pX?b<5}-BJjBMQ~k*x&rPRpS<(i84hXs@f|jch7sS^ny^eaSh+CnA9O1f*jAz#BPwD*W?HJ2qa+%J#mpf; zDFTRMFTQqjO2(Gr$^Ea4vQRkKF<$&57*zA)JgYbD@wKJtMAL#UJ}lmrV5bVgT2q=K zk8_nUq`<&x9as2e!BIX z6}HgkaRjZVKweV0VPsS!*c>&pl4^;EuMQgNrR=GA$?sD_d3Y=Y4kgJLX(fS>?VE)V zizIaYwS*!k{oveE_3eGvy^FQ>KL&Wel`lOCuxu?Mp>aT@fLkiekoYSaZrZOiQ;A5 zlU{#xlu*}>>M0|$JA`oxf2^F3KkzdS^Su6|kE+M!UXEUm!M`rmMKx{Wz|f^)D7>7C z_YZ|;zb7e!l(p#(FY=19ZBS@ z`5|Xe*8e6PFBJ#n_qY<~u7tq!eVOmGoWW?695)ap>x9F2BtzZuERq)!8~-`b|3=a& z97K0B;Lqwpci62GIO^FTeuF<5lLGZ)|J(DxfV$M(BaY_ibZVC?+f?`9e=JI%w(`ak zFStTD#AXzDzA4emO4#DMdw8VKbwB8%yB~COF$CKC%iM@U!*F|BZQx8|Fl@THCzS@q zLdIXC+AG(?v5obyejslkp1k=X>8~vTF0aP9JzcTEw^^S${~q{5yYa2+r2Izch&om5 zkB#8yocT<6V(wcIPq8ePgdbck57NH2)fFYZO zK8W#NTw>Np#C4_F-EX7z;Iglf;E|(7 z7ylAH4F%T8GeA6L3gW`aA4D%`1ApQ-(Uh$N+*%nVlk@xn?ti7p+h`~T3k8SR*Z;Ec zgZ~_N_LD+f*Po!goG*n^j*a=k3rWBhy)!E;YXY}YwTN-5m=-aV2`gOcEQs-nn^( z%rPIHJoaWumYivtMly>(dhKne^x7=XnvaacU?$6 zxJA7G?`T{QUZZtev(LzdyV5pJ;amxLLgb-nv1bHym<$&2O6KB8aW9@1QsJm^u}@f? zr3|;8cAjIroCYG(h&8ONV6YT}yg*O+Mj>=u z?x!d<$b<)*uJaWBf$-aldb)w`A|%%-@M;|FQ$MSnEKlzNf6r117`mFku^E~87wl=U zF{e%~HtcqA-pz)R(kBd>w!NgHBNE|W1~p&vMK>tqYad*tG6Hq!)pK7oqA}o2k%{(( z7oH^;K6I$T1fQD`aF%~4grHTy?-`sSu%FJ>6_9xe?zQhe;dIK!dyzQ><#?-yV&ZA?m;X*RasU!r?+>^PDokyj382#I7HyGq*VKcaohjAUYmv`tduK0 zx}9kcXU3Kq)0=%@n~Sf!V^IjDp59J%_{@)Dx@A1?Z#iQxOZU#qkSPeUti1O+qKH&% zmy-k!8NpGnkw$P*#q-p|#HAT3FgH#Y7By-N2A}M2JP6T2>uv65{2IRCQe}Fl&R!RI zx!$-+$9RLCEd~47Gc)ksUtQ^8_X751Vr@2SB~)?MD1X!#4tm@(gAdjgaBo`K$Kl*@ zWA`Hx%}5+@t~~Ff+y`e6 ztKbUD^{@rfooBl$O$!PG%Im3VyOyYMP5TS+UqiIbR10M#;KP9Vum?LF5~y@?y2y+x z0Gu?Ja~PPEfJ;y`fXFQo5=~y*_472tH4BD@z(pBM{-f~a@w_x7AM1WPKxB@)|7KWD z-s6IODr$D#8=BZV9V~SDoe_ASTevb7#0Ff?nkZVLt)cFZ&i5h%Pq^@qkg+4}6dv=L zd;RLLB3wJe?Bv7Z3J>~?_U#m$;0xp7OZR)UK>9g##Vn~Ch&FFXj;eD(@8ffYDkl6O z?ER0qGUytmmD^=(je;!6h~lER>v11rlNIwb5r+A4oK z9^?OOJG^t!6{ps&w_gfXLE`JySAS0*@UHT2{Prpan7FVaO~qz}#J9%(Dhq32nEE-| zd-^Al@xKoVUCm1hog(^in%QD7^ITXnx8)opU%o}S^K@T9HKV|im&*um6w@-(&sw3~ zb>79s7YDvjrCyD{SUe=%=Rf*8%pY?4W_ALF6hQ6q@Yt=Ra-eG&z*tIu0`LE+e)g#A zEL0PobRRoqh$@P{_vyTzd+VD&1*c;*}M%T)7?1sqMZ#yZbV~pDjK1$qnW@u_qsQTrH5{|4J z$NW0UjRKQVS&17iF!VR2B)QoTulPDst@WB6;8Q(GZ5?}zICtDAVoU*}sdi(26PSWv zY)|~zeoKgYHs43SDFszZ6VFZ%iXp7q@r8cZgE6gG(E>Spkd2k>S_xB!RE4Ginnnlo zcps&mQ)mVxRyrB0>w*V5`m&f$fCX|XpZWu-Jk`L3#oFjPO19?_&6+KX><$8FoTjQEomT1F2 zU!<|RdWzz*D(oE>{_?Qf6wtGx&HJw#*fn4J{_eaZ_$A(X9i{0GA|`M4#5uJ=a)^I@ zCEEaRsh6seUGsoLhDRJfCJ7>a28oMQ^#RYIX^E|mcS8~uk&dI(0%)8^J(ubjgrB;f z-u%1h4OL0nd}PPt+7 zP9pjFkKP!o%s6{AO%u#tMu-Y0nZYxw@?t-?1K(ezWn|4I1qDbICG%N%G1cgJM)xES z4)ZIDcD>=i&h_+Ni7;hkR;cqIRpdsiFE1Z{zs87S%+CAg^jt9QBTxRkw?FJ=Dfj&I zYyq|C+*ka@lDK+GN6>`sB&MzC4=(DSfmdw5d<(cWQLA_4aioS7j=MNkbQB!;rq=&M z(s{UJ{eOSFqU=4gDtnWxjJl7#_uiY#>=oG&vS%q3Lb6o|_i3R)38h_R6e-jvO7eSO zzwaM#yKmR^x?Znyp6B^|9JAjkoN;g??&ZW+;{;rtZe!0N>VE#;vs|jv2Z`LmjP_tI zGsGye_()|Rz^lnStGy+RfX`>h%}rJlx}`*QON8{X?o`VpOR*9h^QfGn;xGiQQUNb+ zrVxB!b!SP|*9Wc7R+Y|2X<(@@!yVD~Gz`Dy-o@u*1tInqeF|iZ(X!~ba+e8_7c|nj zl(An2izk4%yN+i?~^% zw(zJk%M(WsHSYbpA#4t6Z6ZsVHFmI-8=9jc6M(yOVo#gf`C`oNW0>*J0lRXzM4pu- zVQk9t+lTW-@!tF0AQ@!=jHh@sW(u`%P?kb=@Vzo}9%UZ?8?AuxF89KB${2&h(eIb~ z4k%&i>q3G57y$(aEZ#nLcfr>OzQ+BTXTikmAZt}>HBjp}U*02q6ln6UpA8sugWG?V zeAK#~@z;tttUM0Km#NcKpX1KM%?Artv@c!|YhS z#TB>S$vY1tOTJ6H-ub|Qf3z(<<0T-y^}e{Rei#2VKYk}MLd>ta`nJe~4*q@8)ks*uoBU&0$RIV}B78&`_@?p>T(3O^FWpbezy6nnPIbriV{R0ph-*ts zTyh%l5lP}uhf-9ddE#5XBLwTNEu8{xq92+J-p75i81xszjyg^j!;;*|pg!I-s2H($ ztELwXJn3VHxq4oB@(4izWD5#aRQUm*k(d>D$z+tUcQ6&41S0!3G3(a z#$rpUkY$@*h_l(Gpsy`~%ffxE{c7zf=Fl7uX~`h=W%XpiCDoxMRraaGJ$n6)b=hmHZcH?MzXNFy+ zM7`4%Yqi!cu7*=(oXJ;-_wZw+KbJ{(1(xrK;d%2f1%AF`8`?Zq1Q)))H$LSp1reG8 z%ECdi_}{u(x01Cl#`C8xP8o)SQh#WNcy>6-teb84kK193NUw=!TR2=`xT-7N>x!ZW zMpEcLT!6a~((!k4{E=-W%It6BY0Mqo6;*W31xh5%#`MBsv5Q%POz9Jh&tj(;64{Dz zd#26Y=6(*UjSaHe1ramk=-|ml_Yw%tQh#K)!V6|e6|^?}qIg})rE4Qj1rp|mYL61R z4-ggPs%+-~g+GmIJ#k{dJQJq-?W_adq4C&P6-adG{E}biKB_`|{6wWrwgUPJ@3#7v zwFmoc5;RoKvO&+My052g<>30D%f;7W^nJPMfkwBMMP17uwp-tDu

I(#)m-&!|)N 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 literal 0 HcmV?d00001 diff --git a/source/tests/pt/NiO/data/data_0/set.000/spin.npy b/source/tests/pt/NiO/data/data_0/set.000/spin.npy new file mode 100644 index 0000000000000000000000000000000000000000..c426f1c7f67fdedfbed3ffbf275952f9945bc042 GIT binary patch literal 46208 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 literal 0 HcmV?d00001 diff --git a/source/tests/pt/NiO/data/single/set.000/coord.npy b/source/tests/pt/NiO/data/single/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..4060f0fc5368215559ee2bef69d35169d3207429 GIT binary patch literal 896 zcmbV{eJs>*9LIki^N`~nN^Muk9ovA9h}qCHH#V^Xdmu*eg$c9>rjB?s6w8^fV}i1RSQ742O6rhhff z0ph&-y5{q&K&V=!8OHut-`b7F1q?8p9&6B8K$q*tiK3V)hc1=H-b!ANBN&c-uM*Xf z=*P&nOO(v!+;GKMNEsh>8Bjj@K7CwDhiyMJj7%Ph z>Qwd55l_N_RDmL(Hx5EgKyIk^N_7r&BQa7m<+WzPk8Z-fk| zu4{rvX7F)jDr`QG0ww1z7lmNH_&GQ>0QYHH!haI`y^I*?%X}V`d=uyJabI^*uRaO) z+k?wL+X&2|u|tb@&7kDNKU1o?qo`-%XM2sp9u{==!tyivpx8<uRw#Jotp zC>hjIz$UZheu(ENBKNDpgyU>@a&7$DsI!UFYSvx8-=-((arLKNeU?4jGVoAqi8lc$+L~- z0T&``$D(k*?pdShG3+nzTK9LC2`o%ETuBOALO!;=p+iRQ@FFDl)u}=%w3;?G&|9c5 zNU_`h4s%)azPQF4)-cC^>AyXA37xUFU7xAm1TMFZ=U2RDg8OdIaDVLI%TFrqS8f20 zX!{zkP7`?2s~s2&Ttc#KDZKQ{T#yr5@e16R6LVQJ%~VL-TK+(Sxmege%fWp${nwEW b{GYXAs7Kl`ma{32W2Q#y*=I+w9Y0ds71TYuEi0=4M=k`;BO=Nl-pE%9|$$3UWSL6zw? zAFP&?zV=p!LAYn#=@TYCxGmahbw=|w?7QO=>Y26*WQ*4|M@;15m&NT)OJrQMZ$Bdb z%rPFB^vv|uY#|6%-tsy=<_cRg{d&Lu+#i(EMb9dzad>52a?Q=)9GpHsZE>3ujPFY{ zkv{kJXsv+s$Gu$8)vuVkPlm%6{q*YL5Aj&hmeJj@D;8RXkCy%Zga!0hj|a7833R{U z@eK>bz|L{2r~I)O6a$y;^?MXz3P+`hwM&H)o16}=i`hcDK{mCgC?(~ z^jBICzTxoAWe51sBc)VNkz9ae>Pc5}Bb@n>F=T6sL$O3V=jvM$mxfFxC{Bf<*OSRL z!&%?qb%S3nJ83<>c;01Uv&RjSjKc?zoz{fgzyzXhxc?O8N?8tyI3+aJ-Cpt9mY{{C<+$VaMfRRW){kea7uz$uIQ=T3y>|aD%cAVH zr_1dxw5Apq-zl|MyK(Mu{-1gK&z!$2e7I+}y`7?Ue)8r;_V!cXSR2<*+i%EbG;{NT zYWveC8men};_at@&6=72aoYZHhRoWy!W#S6UW${ZrbXJPzui=B5t_UIN~DOex@Cs_ zE7_x8o6ls~KmQytss4QVe$g$*G+7rE+b_#pQ73e>*xs*ax=H%a+Wkfb(JT*GE9`eX QP%1rdRc6mH3I;R;08ahYkN^Mx literal 0 HcmV?d00001 diff --git a/source/tests/pt/NiO/data/single/set.000/spin.npy b/source/tests/pt/NiO/data/single/set.000/spin.npy new file mode 100644 index 0000000000000000000000000000000000000000..88985f5d2c2a10f98160e21de8b8ab4694de2cba GIT binary patch literal 896 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$-ItrF%nmP)#3giN=ofD!%xH4YZ`_Ec&`QwA9`~PuW>zmE~Z2yDjFWMfY zzp}q`HLCF?P&_Dco$V~3c;(!kKW_rX*Phy4{23^oxO-jUWT5yFQJqKn_deRs%R2t@ zNAiRH#($TFtf;@be`;Q))m@-^duJ}acR=-iET^tb1*)&E+Bp9qQ2e^->QIn5@;k%6 z&j*StrZ3ET1Qh@Kz{TMgP`v$_Um?gn%rp6CUk8e}|9=+o7APK;sOdKeD84wAmE%eJ ztNpdCDZD^;+9&UrzyMTlpQve{ehVmWc1+G5Xudr!s{{woUVB#dSqhAkKkT>vbFsXo z@wxryoIZ74?Va`&zmx)=0o9-SE#vwHs6K0Jzurus`iM14_TK}FM_ri``Uxoh_OrxO zkocmEqwKeV;tEB|zup7IyOuHC0+}Dtv+Ej2{jTMPrB8w4oAu0oE&_@lH(0Fy9w>e} YB7YJv9PDrZ3GxGmr#-_c7|;*^0Gp!qKmY&$ literal 0 HcmV?d00001 diff --git a/source/tests/pt/NiO/data/single/type.raw b/source/tests/pt/NiO/data/single/type.raw new file mode 100644 index 0000000000..d9664c7a22 --- /dev/null +++ b/source/tests/pt/NiO/data/single/type.raw @@ -0,0 +1,32 @@ +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 diff --git a/source/tests/pt/NiO/data/single/type_map.raw b/source/tests/pt/NiO/data/single/type_map.raw new file mode 100644 index 0000000000..7eca995c31 --- /dev/null +++ b/source/tests/pt/NiO/data/single/type_map.raw @@ -0,0 +1,2 @@ +Ni +O diff --git a/source/tests/pt/model/test_autodiff.py b/source/tests/pt/model/test_autodiff.py index c32f202625..91fc3cabf6 100644 --- a/source/tests/pt/model/test_autodiff.py +++ b/source/tests/pt/model/test_autodiff.py @@ -7,11 +7,13 @@ from deepmd.pt.model.model import ( get_model, - get_zbl_model, ) from deepmd.pt.utils import ( env, ) +from deepmd.pt.utils.utils import ( + to_numpy_array, +) dtype = torch.float64 @@ -21,6 +23,7 @@ model_dpa2, model_hybrid, model_se_e2_a, + model_spin, model_zbl, ) @@ -59,34 +62,64 @@ def test( cell = (cell + cell.T) + 5.0 * torch.eye(3, device="cpu") coord = torch.rand([natoms, 3], dtype=dtype, device="cpu") coord = torch.matmul(coord, cell) + spin = torch.rand([natoms, 3], dtype=dtype, device="cpu") atype = torch.IntTensor([0, 0, 0, 1, 1]) # assumes input to be numpy tensor coord = coord.numpy() - - def np_infer( + spin = spin.numpy() + test_spin = getattr(self, "test_spin", False) + if not test_spin: + test_keys = ["energy", "force", "virial"] + else: + test_keys = ["energy", "force", "force_mag", "virial"] + + def np_infer_coord( coord, ): - e0, f0, v0 = eval_model( + result = eval_model( self.model, torch.tensor(coord, device=env.DEVICE).unsqueeze(0), cell.unsqueeze(0), atype, + spins=torch.tensor(spin, device=env.DEVICE).unsqueeze(0), ) - ret = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } # detach - ret = {kk: ret[kk].detach().cpu().numpy() for kk in ret} + ret = {key: to_numpy_array(result[key].squeeze(0)) for key in test_keys} return ret - def ff(_coord): - return np_infer(_coord)["energy"] + def np_infer_spin( + spin, + ): + result = eval_model( + self.model, + torch.tensor(coord, device=env.DEVICE).unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=torch.tensor(spin, device=env.DEVICE).unsqueeze(0), + ) + # detach + ret = {key: to_numpy_array(result[key].squeeze(0)) for key in test_keys} + return ret - fdf = -finite_difference(ff, coord, delta=delta).squeeze() - rff = np_infer(coord)["force"] - np.testing.assert_almost_equal(fdf, rff, decimal=places) + def ff_coord(_coord): + return np_infer_coord(_coord)["energy"] + + def ff_spin(_spin): + return np_infer_spin(_spin)["energy"] + + if not test_spin: + fdf = -finite_difference(ff_coord, coord, delta=delta).squeeze() + rff = np_infer_coord(coord)["force"] + np.testing.assert_almost_equal(fdf, rff, decimal=places) + else: + # real force + fdf = -finite_difference(ff_coord, coord, delta=delta).squeeze() + rff = np_infer_coord(coord)["force"] + np.testing.assert_almost_equal(fdf, rff, decimal=places) + # magnetic force + fdf = -finite_difference(ff_spin, spin, delta=delta).squeeze() + rff = np_infer_spin(spin)["force_mag"] + np.testing.assert_almost_equal(fdf, rff, decimal=places) class VirialTest: @@ -104,11 +137,12 @@ def test( # assumes input to be numpy tensor coord = coord.numpy() cell = cell.numpy() + test_keys = ["energy", "force", "virial"] def np_infer( new_cell, ): - e0, f0, v0 = eval_model( + result = eval_model( self.model, torch.tensor( stretch_box(coord, cell, new_cell), device="cpu" @@ -116,13 +150,9 @@ def np_infer( torch.tensor(new_cell, device="cpu").unsqueeze(0), atype, ) - ret = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } # detach - ret = {kk: ret[kk].detach().cpu().numpy() for kk in ret} + ret = {key: to_numpy_array(result[key].squeeze(0)) for key in test_keys} + # detach return ret def ff(bb): @@ -211,11 +241,19 @@ class TestEnergyModelZBLForce(unittest.TestCase, ForceTest): def setUp(self): model_params = copy.deepcopy(model_zbl) self.type_split = False - self.model = get_zbl_model(model_params).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) class TestEnergyModelZBLVirial(unittest.TestCase, VirialTest): def setUp(self): model_params = copy.deepcopy(model_zbl) self.type_split = False - self.model = get_zbl_model(model_params).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelSpinSeAForce(unittest.TestCase, ForceTest): + def setUp(self): + model_params = copy.deepcopy(model_spin) + self.type_split = False + self.test_spin = True + self.model = get_model(model_params).to(env.DEVICE) diff --git a/source/tests/pt/model/test_deeppot.py b/source/tests/pt/model/test_deeppot.py index 697ebb6411..68b1ff65d5 100644 --- a/source/tests/pt/model/test_deeppot.py +++ b/source/tests/pt/model/test_deeppot.py @@ -95,7 +95,8 @@ def test_dp_test(self): ).reshape(1, -1, 3) atype = np.array([0, 0, 0, 1, 1]).reshape(1, -1) - e, f, v, ae, av = dp.eval(coord, cell, atype, atomic=True) + ret = dp.eval(coord, cell, atype, atomic=True) + e, f, v, ae, av = ret[0], ret[1], ret[2], ret[3], ret[4] self.assertEqual(e.shape, (1, 1)) self.assertEqual(f.shape, (1, 5, 3)) self.assertEqual(v.shape, (1, 9)) diff --git a/source/tests/pt/model/test_embedding_net.py b/source/tests/pt/model/test_embedding_net.py index a1895718dd..63a3534c74 100644 --- a/source/tests/pt/model/test_embedding_net.py +++ b/source/tests/pt/model/test_embedding_net.py @@ -56,13 +56,22 @@ def get_single_batch(dataset, index=None): np_batch = dataset[index] pt_batch = {} - for key in ["coord", "box", "force", "energy", "virial", "atype", "natoms"]: + for key in [ + "coord", + "box", + "force", + "force_mag", + "energy", + "virial", + "atype", + "natoms", + ]: if key in np_batch.keys(): np_batch[key] = np.expand_dims(np_batch[key], axis=0) pt_batch[key] = torch.as_tensor(np_batch[key], device=env.DEVICE) - np_batch["coord"] = np_batch["coord"].reshape(1, -1) + if key in ["coord", "force", "force_mag"]: + np_batch[key] = np_batch[key].reshape(1, -1) np_batch["natoms"] = np_batch["natoms"][0] - np_batch["force"] = np_batch["force"].reshape(1, -1) return np_batch, pt_batch diff --git a/source/tests/pt/model/test_ener_spin_model.py b/source/tests/pt/model/test_ener_spin_model.py new file mode 100644 index 0000000000..2bd5c22aaf --- /dev/null +++ b/source/tests/pt/model/test_ener_spin_model.py @@ -0,0 +1,420 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest + +import numpy as np +import torch + +from deepmd.dpmodel.model import SpinModel as DPSpinModel +from deepmd.pt.model.model import ( + SpinEnergyModel, + get_model, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.nlist import ( + extend_input_and_build_neighbor_list, +) +from deepmd.pt.utils.utils import ( + to_numpy_array, +) + +from .test_permutation import ( + model_dpa1, + model_dpa2, + model_se_e2_a, + model_spin, +) + +dtype = torch.float64 + + +def reduce_tensor(extended_tensor, mapping, nloc: int): + nframes, nall = extended_tensor.shape[:2] + ext_dims = extended_tensor.shape[2:] + reduced_tensor = torch.zeros( + [nframes, nloc, *ext_dims], + dtype=extended_tensor.dtype, + device=extended_tensor.device, + ) + mldims = list(mapping.shape) + mapping = mapping.view(mldims + [1] * len(ext_dims)).expand( + [-1] * len(mldims) + list(ext_dims) + ) + # nf x nloc x (*ext_dims) + reduced_tensor = torch.scatter_reduce( + reduced_tensor, + 1, + index=mapping, + src=extended_tensor, + reduce="sum", + ) + return reduced_tensor + + +class SpinTest: + def setUp(self): + self.prec = 1e-10 + natoms = 5 + self.ntypes = 3 # ["O", "H", "B"] for test + self.cell = 4.0 * torch.eye(3, dtype=dtype, device=env.DEVICE).unsqueeze(0) + self.coord = 3.0 * torch.rand( + [natoms, 3], dtype=dtype, device=env.DEVICE + ).unsqueeze(0) + self.spin = 0.5 * torch.rand( + [natoms, 3], dtype=dtype, device=env.DEVICE + ).unsqueeze(0) + self.atype = torch.tensor( + [0, 0, 0, 1, 1], dtype=torch.int64, device=env.DEVICE + ).unsqueeze(0) + + self.expected_mask = torch.tensor( + [ + [True], + [True], + [True], + [False], + [False], + ], + dtype=torch.bool, + device=env.DEVICE, + ).unsqueeze(0) + self.expected_atype_with_spin = torch.tensor( + [0, 0, 0, 1, 1, 3, 3, 3, 4, 4], dtype=torch.int64, device=env.DEVICE + ).unsqueeze(0) + self.expected_nloc_spin_index = ( + torch.arange(natoms, natoms * 2, dtype=torch.int64, device=env.DEVICE) + .unsqueeze(0) + .unsqueeze(-1) + ) + + def test_output_shape( + self, + ): + result = self.model( + self.coord, + self.atype, + self.spin, + self.cell, + ) + # check magnetic mask + torch.testing.assert_close(result["mask_mag"], self.expected_mask) + # check output shape to assure split + nframes, nloc = self.coord.shape[:2] + torch.testing.assert_close(result["energy"].shape, [nframes, 1]) + torch.testing.assert_close(result["atom_energy"].shape, [nframes, nloc, 1]) + torch.testing.assert_close(result["force"].shape, [nframes, nloc, 3]) + torch.testing.assert_close(result["force_mag"].shape, [nframes, nloc, 3]) + + def test_input_output_process(self): + nframes, nloc = self.coord.shape[:2] + self.real_ntypes = self.model.spin.get_ntypes_real() + # 1. test forward input process + coord_updated, atype_updated = self.model.process_spin_input( + self.coord, self.atype, self.spin + ) + # compare atypes of real and virtual atoms + torch.testing.assert_close(atype_updated, self.expected_atype_with_spin) + # compare coords of real and virtual atoms + torch.testing.assert_close(coord_updated.shape, [nframes, nloc * 2, 3]) + torch.testing.assert_close(coord_updated[:, :nloc], self.coord) + virtual_scale = torch.tensor( + self.model.spin.get_virtual_scale_mask()[self.atype.cpu()], + dtype=dtype, + device=env.DEVICE, + ) + virtual_coord = self.coord + self.spin * virtual_scale.unsqueeze(-1) + torch.testing.assert_close(coord_updated[:, nloc:], virtual_coord) + + # 2. test forward output process + model_ret = self.model.backbone_model.forward_common( + coord_updated, + atype_updated, + self.cell, + do_atomic_virial=True, + ) + if self.model.do_grad_r("energy"): + force_all = model_ret["energy_derv_r"].squeeze(-2) + force_real, force_mag, _ = self.model.process_spin_output( + self.atype, force_all + ) + torch.testing.assert_close( + force_real, force_all[:, :nloc] + force_all[:, nloc:] + ) + torch.testing.assert_close( + force_mag, force_all[:, nloc:] * virtual_scale.unsqueeze(-1) + ) + + # 3. test forward_lower input process + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + self.coord, + self.atype, + self.model.get_rcut(), + self.model.get_sel(), + mixed_types=self.model.mixed_types(), + box=self.cell, + ) + nall = extended_coord.shape[1] + nnei = nlist.shape[-1] + extended_spin = torch.gather( + self.spin, index=mapping.unsqueeze(-1).tile((1, 1, 3)), dim=1 + ) + ( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping_updated, + ) = self.model.process_spin_input_lower( + extended_coord, extended_atype, extended_spin, nlist, mapping=mapping + ) + # compare atypes of real and virtual atoms + # Note that the real and virtual atoms corresponding to the local ones are switch to the first nloc * 2 atoms + torch.testing.assert_close(extended_atype_updated.shape, [nframes, nall * 2]) + torch.testing.assert_close( + extended_atype_updated[:, :nloc], extended_atype[:, :nloc] + ) + torch.testing.assert_close( + extended_atype_updated[:, nloc : nloc + nloc], + extended_atype[:, :nloc] + self.real_ntypes, + ) + torch.testing.assert_close( + extended_atype_updated[:, nloc + nloc : nloc + nall], + extended_atype[:, nloc:nall], + ) + torch.testing.assert_close( + extended_atype_updated[:, nloc + nall :], + extended_atype[:, nloc:nall] + self.real_ntypes, + ) + virtual_scale = torch.tensor( + self.model.spin.get_virtual_scale_mask()[extended_atype.cpu()], + dtype=dtype, + device=env.DEVICE, + ) + # compare coords of real and virtual atoms + virtual_coord = extended_coord + extended_spin * virtual_scale.unsqueeze(-1) + torch.testing.assert_close(extended_coord_updated.shape, [nframes, nall * 2, 3]) + torch.testing.assert_close( + extended_coord_updated[:, :nloc], extended_coord[:, :nloc] + ) + torch.testing.assert_close( + extended_coord_updated[:, nloc : nloc + nloc], virtual_coord[:, :nloc] + ) + torch.testing.assert_close( + extended_coord_updated[:, nloc + nloc : nloc + nall], + extended_coord[:, nloc:nall], + ) + torch.testing.assert_close( + extended_coord_updated[:, nloc + nall :], virtual_coord[:, nloc:nall] + ) + + # compare mapping + torch.testing.assert_close(mapping_updated.shape, [nframes, nall * 2]) + torch.testing.assert_close(mapping_updated[:, :nloc], mapping[:, :nloc]) + torch.testing.assert_close( + mapping_updated[:, nloc : nloc + nloc], mapping[:, :nloc] + nloc + ) + torch.testing.assert_close( + mapping_updated[:, nloc + nloc : nloc + nall], mapping[:, nloc:nall] + ) + torch.testing.assert_close( + mapping_updated[:, nloc + nall :], mapping[:, nloc:nall] + nloc + ) + + # compare nlist + torch.testing.assert_close( + nlist_updated.shape, [nframes, nloc * 2, nnei * 2 + 1] + ) + # self spin + torch.testing.assert_close( + nlist_updated[:, :nloc, :1], self.expected_nloc_spin_index + ) + # real and virtual neighbors + loc_atoms_mask = (nlist < nloc) & (nlist != -1) + ghost_atoms_mask = nlist >= nloc + real_neighbors = nlist.clone() + real_neighbors[ghost_atoms_mask] += nloc + torch.testing.assert_close( + nlist_updated[:, :nloc, 1 : 1 + nnei], real_neighbors + ) + virtual_neighbors = nlist.clone() + virtual_neighbors[loc_atoms_mask] += nloc + virtual_neighbors[ghost_atoms_mask] += nall + torch.testing.assert_close( + nlist_updated[:, :nloc, 1 + nnei :], virtual_neighbors + ) + + # 4. test forward_lower output process + model_ret = self.model.backbone_model.forward_common_lower( + extended_coord_updated, + extended_atype_updated, + nlist_updated, + mapping=mapping_updated, + do_atomic_virial=True, + ) + if self.model.do_grad_r("energy"): + force_all = model_ret["energy_derv_r"].squeeze(-2) + force_real, force_mag, _ = self.model.process_spin_output_lower( + extended_atype, force_all, nloc + ) + force_all_switched = torch.zeros_like(force_all) + force_all_switched[:, :nloc] = force_all[:, :nloc] + force_all_switched[:, nloc:nall] = force_all[:, nloc + nloc : nloc + nall] + force_all_switched[:, nall : nall + nloc] = force_all[:, nloc : nloc + nloc] + force_all_switched[:, nall + nloc :] = force_all[:, nloc + nall :] + torch.testing.assert_close( + force_real, force_all_switched[:, :nall] + force_all_switched[:, nall:] + ) + torch.testing.assert_close( + force_mag, force_all_switched[:, nall:] * virtual_scale.unsqueeze(-1) + ) + + def test_jit(self): + model = torch.jit.script(self.model) + self.assertEqual(model.get_rcut(), self.rcut) + self.assertEqual(model.get_nsel(), self.nsel) + self.assertEqual(model.get_type_map(), self.type_map) + + def test_self_consistency(self): + if hasattr(self, "serial_test") and not self.serial_test: + # not implement serialize and deserialize + return + model1 = SpinEnergyModel.deserialize(self.model.serialize()) + result = model1( + self.coord, + self.atype, + self.spin, + self.cell, + ) + expected_result = self.model( + self.coord, + self.atype, + self.spin, + self.cell, + ) + for key in result: + torch.testing.assert_close( + result[key], expected_result[key], rtol=self.prec, atol=self.prec + ) + model1 = torch.jit.script(model1) + + def test_dp_consistency(self): + if hasattr(self, "serial_test") and not self.serial_test: + # not implement serialize and deserialize + return + dp_model = DPSpinModel.deserialize(self.model.serialize()) + # test call + dp_ret = dp_model.call( + to_numpy_array(self.coord), + to_numpy_array(self.atype), + to_numpy_array(self.spin), + to_numpy_array(self.cell), + ) + result = self.model.forward_common( + self.coord, + self.atype, + self.spin, + self.cell, + ) + np.testing.assert_allclose( + to_numpy_array(result["energy"]), + dp_ret["energy"], + rtol=self.prec, + atol=self.prec, + ) + np.testing.assert_allclose( + to_numpy_array(result["energy_redu"]), + dp_ret["energy_redu"], + rtol=self.prec, + atol=self.prec, + ) + + # test call_lower + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + self.coord, + self.atype, + self.model.get_rcut(), + self.model.get_sel(), + mixed_types=self.model.mixed_types(), + box=self.cell, + ) + extended_spin = torch.gather( + self.spin, index=mapping.unsqueeze(-1).tile((1, 1, 3)), dim=1 + ) + dp_ret_lower = dp_model.call_lower( + to_numpy_array(extended_coord), + to_numpy_array(extended_atype), + to_numpy_array(extended_spin), + to_numpy_array(nlist), + to_numpy_array(mapping), + ) + result_lower = self.model.forward_common_lower( + extended_coord, + extended_atype, + extended_spin, + nlist, + mapping, + ) + np.testing.assert_allclose( + to_numpy_array(result_lower["energy"]), + dp_ret_lower["energy"], + rtol=self.prec, + atol=self.prec, + ) + np.testing.assert_allclose( + to_numpy_array(result_lower["energy_redu"]), + dp_ret_lower["energy_redu"], + rtol=self.prec, + atol=self.prec, + ) + + +class TestEnergyModelSpinSeA(unittest.TestCase, SpinTest): + def setUp(self): + SpinTest.setUp(self) + model_params = copy.deepcopy(model_spin) + model_params["descriptor"] = copy.deepcopy(model_se_e2_a["descriptor"]) + self.rcut = model_params["descriptor"]["rcut"] + self.nsel = sum(model_params["descriptor"]["sel"]) + self.type_map = model_params["type_map"] + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelSpinDPA1(unittest.TestCase, SpinTest): + def setUp(self): + SpinTest.setUp(self) + model_params = copy.deepcopy(model_spin) + model_params["descriptor"] = copy.deepcopy(model_dpa1["descriptor"]) + self.rcut = model_params["descriptor"]["rcut"] + self.nsel = model_params["descriptor"]["sel"] + self.type_map = model_params["type_map"] + # not implement serialize and deserialize + self.serial_test = False + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelSpinDPA2(unittest.TestCase, SpinTest): + def setUp(self): + SpinTest.setUp(self) + model_params = copy.deepcopy(model_spin) + model_params["descriptor"] = copy.deepcopy(model_dpa2["descriptor"]) + self.rcut = model_params["descriptor"]["repinit_rcut"] + self.nsel = model_params["descriptor"]["repinit_nsel"] + self.type_map = model_params["type_map"] + # not implement serialize and deserialize + self.serial_test = False + self.model = get_model(model_params).to(env.DEVICE) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/model/test_forward_lower.py b/source/tests/pt/model/test_forward_lower.py new file mode 100644 index 0000000000..32be3b62ad --- /dev/null +++ b/source/tests/pt/model/test_forward_lower.py @@ -0,0 +1,177 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +import copy +import unittest + +import torch + +from deepmd.pt.infer.deep_eval import ( + eval_model, +) +from deepmd.pt.model.model import ( + get_model, +) +from deepmd.pt.utils import ( + env, +) +from deepmd.pt.utils.nlist import ( + extend_input_and_build_neighbor_list, +) + +from .test_permutation import ( # model_dpau, + model_dpa1, + model_dpa2, + model_se_e2_a, + model_spin, + model_zbl, +) + +dtype = torch.float64 + + +def reduce_tensor(extended_tensor, mapping, nloc: int): + nframes, nall = extended_tensor.shape[:2] + ext_dims = extended_tensor.shape[2:] + reduced_tensor = torch.zeros( + [nframes, nloc, *ext_dims], + dtype=extended_tensor.dtype, + device=extended_tensor.device, + ) + mldims = list(mapping.shape) + mapping = mapping.view(mldims + [1] * len(ext_dims)).expand( + [-1] * len(mldims) + list(ext_dims) + ) + # nf x nloc x (*ext_dims) + reduced_tensor = torch.scatter_reduce( + reduced_tensor, + 1, + index=mapping, + src=extended_tensor, + reduce="sum", + ) + return reduced_tensor + + +class ForwardLowerTest: + def test( + self, + ): + prec = self.prec + natoms = 5 + cell = 4.0 * torch.eye(3, dtype=dtype, device=env.DEVICE) + coord = 3.0 * torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) + spin = 0.5 * torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) + atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int64, device=env.DEVICE) + test_spin = getattr(self, "test_spin", False) + if not test_spin: + test_keys = ["energy", "force", "virial"] + else: + test_keys = ["energy", "force", "force_mag"] + + result_forward = eval_model( + self.model, + coord.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), + ) + ( + extended_coord, + extended_atype, + mapping, + nlist, + ) = extend_input_and_build_neighbor_list( + coord.unsqueeze(0), + atype.unsqueeze(0), + self.model.get_rcut(), + self.model.get_sel(), + mixed_types=self.model.mixed_types(), + box=cell.unsqueeze(0), + ) + extended_spin = torch.gather( + spin.unsqueeze(0), index=mapping.unsqueeze(-1).tile((1, 1, 3)), dim=1 + ) + input_dict = { + "extended_coord": extended_coord, + "extended_atype": extended_atype, + "nlist": nlist, + "mapping": mapping, + "do_atomic_virial": False, + } + if test_spin: + input_dict["extended_spin"] = extended_spin + result_forward_lower = self.model.forward_lower(**input_dict) + for key in test_keys: + if key in ["energy"]: + torch.testing.assert_close( + result_forward_lower[key], result_forward[key], rtol=prec, atol=prec + ) + elif key in ["force", "force_mag"]: + reduced_vv = reduce_tensor( + result_forward_lower[f"extended_{key}"], mapping, natoms + ) + torch.testing.assert_close( + reduced_vv, result_forward[key], rtol=prec, atol=prec + ) + elif key == "virial": + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + result_forward_lower[key], + result_forward[key], + rtol=prec, + atol=prec, + ) + else: + raise RuntimeError(f"Unexpected test key {key}") + + +class TestEnergyModelSeA(unittest.TestCase, ForwardLowerTest): + def setUp(self): + self.prec = 1e-10 + model_params = copy.deepcopy(model_se_e2_a) + self.type_split = False + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelDPA1(unittest.TestCase, ForwardLowerTest): + def setUp(self): + self.prec = 1e-10 + model_params = copy.deepcopy(model_dpa1) + self.type_split = True + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelDPA2(unittest.TestCase, ForwardLowerTest): + def setUp(self): + self.prec = 1e-10 + model_params_sample = copy.deepcopy(model_dpa2) + model_params_sample["descriptor"]["rcut"] = model_params_sample["descriptor"][ + "repinit_rcut" + ] + model_params_sample["descriptor"]["sel"] = model_params_sample["descriptor"][ + "repinit_nsel" + ] + model_params = copy.deepcopy(model_dpa2) + self.type_split = True + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelZBL(unittest.TestCase, ForwardLowerTest): + def setUp(self): + self.prec = 1e-10 + model_params = copy.deepcopy(model_zbl) + self.type_split = False + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelSpinSeA(unittest.TestCase, ForwardLowerTest): + def setUp(self): + # still need to figure out why only 1e-5 rtol and atol + self.prec = 1e-5 + model_params = copy.deepcopy(model_spin) + self.type_split = False + self.test_spin = True + self.model = get_model(model_params).to(env.DEVICE) + + +if __name__ == "__main__": + unittest.main() diff --git a/source/tests/pt/model/test_null_input.py b/source/tests/pt/model/test_null_input.py index eb8ff714e8..c8f4307d52 100644 --- a/source/tests/pt/model/test_null_input.py +++ b/source/tests/pt/model/test_null_input.py @@ -41,14 +41,9 @@ def test_nloc_1( cell = (cell + cell.T) + 100.0 * torch.eye(3, device=env.DEVICE) coord = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) atype = torch.tensor([0], dtype=torch.int32, device=env.DEVICE) - e0, f0, v0 = eval_model( - self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype - ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } + test_keys = ["energy", "force", "virial"] + result = eval_model(self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype) + ret0 = {key: result[key].squeeze(0) for key in test_keys} prec = 1e-10 expect_e_shape = [1] expect_f = torch.zeros([natoms, 3], dtype=dtype, device=env.DEVICE) @@ -70,14 +65,9 @@ def test_nloc_2_far( # 2 far-away atoms coord = torch.cat([coord, coord + 100.0], dim=0) atype = torch.tensor([0, 2], dtype=torch.int32, device=env.DEVICE) - e0, f0, v0 = eval_model( - self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype - ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } + test_keys = ["energy", "force", "virial"] + result = eval_model(self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype) + ret0 = {key: result[key].squeeze(0) for key in test_keys} prec = 1e-10 expect_e_shape = [1] expect_f = torch.zeros([natoms, 3], dtype=dtype, device=env.DEVICE) diff --git a/source/tests/pt/model/test_permutation.py b/source/tests/pt/model/test_permutation.py index fa97281718..8ec5c375fd 100644 --- a/source/tests/pt/model/test_permutation.py +++ b/source/tests/pt/model/test_permutation.py @@ -9,7 +9,6 @@ ) from deepmd.pt.model.model import ( get_model, - get_zbl_model, ) from deepmd.pt.utils import ( env, @@ -23,7 +22,7 @@ "type": "se_e2_a", "sel": [46, 92, 4], "rcut_smth": 0.50, - "rcut": 6.00, + "rcut": 4.00, "neuron": [25, 50, 100], "resnet_dt": False, "axis_neuron": 16, @@ -61,6 +60,31 @@ "data_stat_nbatch": 20, } +model_spin = { + "type_map": ["O", "H", "B"], + "descriptor": { + "type": "se_e2_a", + "sel": [46, 92, 4], + "rcut_smth": 0.50, + "rcut": 4.00, + "neuron": [25, 50, 100], + "resnet_dt": False, + "axis_neuron": 16, + "seed": 1, + }, + "fitting_net": { + "neuron": [24, 24, 24], + "resnet_dt": True, + "seed": 1, + }, + "data_stat_nbatch": 20, + "spin": { + "use_spin": [True, False, False], + "virtual_scale": [0.3140], + "_comment": " that's all", + }, +} + model_dpa2 = { "type_map": ["O", "H", "B"], "descriptor": { @@ -205,34 +229,46 @@ def test( cell = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) cell = (cell + cell.T) + 5.0 * torch.eye(3, device=env.DEVICE) coord = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) + spin = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) coord = torch.matmul(coord, cell) atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE) idx_perm = [1, 0, 4, 3, 2] - e0, f0, v0 = eval_model( - self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype + test_spin = getattr(self, "test_spin", False) + if not test_spin: + test_keys = ["energy", "force", "virial"] + else: + test_keys = ["energy", "force", "force_mag", "virial"] + result_0 = eval_model( + self.model, + coord.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } - e1, f1, v1 = eval_model( - self.model, coord[idx_perm].unsqueeze(0), cell.unsqueeze(0), atype[idx_perm] + ret0 = {key: result_0[key].squeeze(0) for key in test_keys} + result_1 = eval_model( + self.model, + coord[idx_perm].unsqueeze(0), + cell.unsqueeze(0), + atype[idx_perm], + spins=spin[idx_perm].unsqueeze(0), ) - ret1 = { - "energy": e1.squeeze(0), - "force": f1.squeeze(0), - "virial": v1.squeeze(0), - } + ret1 = {key: result_1[key].squeeze(0) for key in test_keys} prec = 1e-10 - torch.testing.assert_close(ret0["energy"], ret1["energy"], rtol=prec, atol=prec) - torch.testing.assert_close( - ret0["force"][idx_perm], ret1["force"], rtol=prec, atol=prec - ) - if not hasattr(self, "test_virial") or self.test_virial: - torch.testing.assert_close( - ret0["virial"], ret1["virial"], rtol=prec, atol=prec - ) + for key in test_keys: + if key in ["energy"]: + torch.testing.assert_close(ret0[key], ret1[key], rtol=prec, atol=prec) + elif key in ["force", "force_mag"]: + torch.testing.assert_close( + ret0[key][idx_perm], ret1[key], rtol=prec, atol=prec + ) + elif key == "virial": + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + ret0[key], ret1[key], rtol=prec, atol=prec + ) + else: + raise RuntimeError(f"Unexpected test key {key}") class TestEnergyModelSeA(unittest.TestCase, PermutationTest): @@ -299,7 +335,15 @@ class TestEnergyModelZBL(unittest.TestCase, PermutationTest): def setUp(self): model_params = copy.deepcopy(model_zbl) self.type_split = False - self.model = get_zbl_model(model_params).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelSpinSeA(unittest.TestCase, PermutationTest): + def setUp(self): + model_params = copy.deepcopy(model_spin) + self.type_split = False + self.test_spin = True + self.model = get_model(model_params).to(env.DEVICE) # class TestEnergyFoo(unittest.TestCase): diff --git a/source/tests/pt/model/test_rot.py b/source/tests/pt/model/test_rot.py index 19f671e619..a12bd063b4 100644 --- a/source/tests/pt/model/test_rot.py +++ b/source/tests/pt/model/test_rot.py @@ -9,7 +9,6 @@ ) from deepmd.pt.model.model import ( get_model, - get_zbl_model, ) from deepmd.pt.utils import ( env, @@ -20,6 +19,7 @@ model_dpa2, model_hybrid, model_se_e2_a, + model_spin, model_zbl, ) @@ -34,80 +34,102 @@ def test( natoms = 5 cell = 10.0 * torch.eye(3, dtype=dtype, device=env.DEVICE) coord = 2 * torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) + spin = 2 * torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) shift = torch.tensor([4, 4, 4], dtype=dtype, device=env.DEVICE) atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE) from scipy.stats import ( special_ortho_group, ) + test_spin = getattr(self, "test_spin", False) + if not test_spin: + test_keys = ["energy", "force", "virial"] + else: + test_keys = ["energy", "force", "force_mag"] rmat = torch.tensor(special_ortho_group.rvs(3), dtype=dtype, device=env.DEVICE) # rotate only coord and shift to the center of cell coord_rot = torch.matmul(coord, rmat) - e0, f0, v0 = eval_model( - self.model, (coord + shift).unsqueeze(0), cell.unsqueeze(0), atype + spin_rot = torch.matmul(spin, rmat) + result_0 = eval_model( + self.model, + (coord + shift).unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } - e1, f1, v1 = eval_model( - self.model, (coord_rot + shift).unsqueeze(0), cell.unsqueeze(0), atype + ret0 = {key: result_0[key].squeeze(0) for key in test_keys} + result_1 = eval_model( + self.model, + (coord_rot + shift).unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin_rot.unsqueeze(0), ) - ret1 = { - "energy": e1.squeeze(0), - "force": f1.squeeze(0), - "virial": v1.squeeze(0), - } - torch.testing.assert_close(ret0["energy"], ret1["energy"], rtol=prec, atol=prec) - torch.testing.assert_close( - torch.matmul(ret0["force"], rmat), ret1["force"], rtol=prec, atol=prec - ) - if not hasattr(self, "test_virial") or self.test_virial: - torch.testing.assert_close( - torch.matmul(rmat.T, torch.matmul(ret0["virial"].view([3, 3]), rmat)), - ret1["virial"].view([3, 3]), - rtol=prec, - atol=prec, - ) - + ret1 = {key: result_1[key].squeeze(0) for key in test_keys} + for key in test_keys: + if key in ["energy"]: + torch.testing.assert_close(ret0[key], ret1[key], rtol=prec, atol=prec) + elif key in ["force", "force_mag"]: + torch.testing.assert_close( + torch.matmul(ret0[key], rmat), ret1[key], rtol=prec, atol=prec + ) + elif key == "virial": + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + torch.matmul( + rmat.T, torch.matmul(ret0[key].view([3, 3]), rmat) + ), + ret1[key].view([3, 3]), + rtol=prec, + atol=prec, + ) + else: + raise RuntimeError(f"Unexpected test key {key}") # rotate coord and cell torch.manual_seed(0) cell = torch.rand([3, 3], dtype=dtype, device=env.DEVICE) cell = (cell + cell.T) + 5.0 * torch.eye(3, device=env.DEVICE) coord = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) coord = torch.matmul(coord, cell) + spin = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE) coord_rot = torch.matmul(coord, rmat) + spin_rot = torch.matmul(spin, rmat) cell_rot = torch.matmul(cell, rmat) - e0, f0, v0 = eval_model( - self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype - ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } - e1, f1, v1 = eval_model( - self.model, coord_rot.unsqueeze(0), cell_rot.unsqueeze(0), atype + result_0 = eval_model( + self.model, + coord.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret1 = { - "energy": e1.squeeze(0), - "force": f1.squeeze(0), - "virial": v1.squeeze(0), - } - torch.testing.assert_close(ret0["energy"], ret1["energy"], rtol=prec, atol=prec) - torch.testing.assert_close( - torch.matmul(ret0["force"], rmat), ret1["force"], rtol=prec, atol=prec + ret0 = {key: result_0[key].squeeze(0) for key in test_keys} + result_1 = eval_model( + self.model, + coord_rot.unsqueeze(0), + cell_rot.unsqueeze(0), + atype, + spins=spin_rot.unsqueeze(0), ) - if not hasattr(self, "test_virial") or self.test_virial: - torch.testing.assert_close( - torch.matmul(rmat.T, torch.matmul(ret0["virial"].view([3, 3]), rmat)), - ret1["virial"].view([3, 3]), - rtol=prec, - atol=prec, - ) + ret1 = {key: result_1[key].squeeze(0) for key in test_keys} + for key in test_keys: + if key in ["energy"]: + torch.testing.assert_close(ret0[key], ret1[key], rtol=prec, atol=prec) + elif key in ["force", "force_mag"]: + torch.testing.assert_close( + torch.matmul(ret0[key], rmat), ret1[key], rtol=prec, atol=prec + ) + elif key == "virial": + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + torch.matmul( + rmat.T, torch.matmul(ret0[key].view([3, 3]), rmat) + ), + ret1[key].view([3, 3]), + rtol=prec, + atol=prec, + ) class TestEnergyModelSeA(unittest.TestCase, RotTest): @@ -174,7 +196,15 @@ class TestEnergyModelZBL(unittest.TestCase, RotTest): def setUp(self): model_params = copy.deepcopy(model_zbl) self.type_split = False - self.model = get_zbl_model(model_params).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelSpinSeA(unittest.TestCase, RotTest): + def setUp(self): + model_params = copy.deepcopy(model_spin) + self.type_split = False + self.test_spin = True + self.model = get_model(model_params).to(env.DEVICE) if __name__ == "__main__": diff --git a/source/tests/pt/model/test_smooth.py b/source/tests/pt/model/test_smooth.py index bc1d26bffa..86e9ed94d7 100644 --- a/source/tests/pt/model/test_smooth.py +++ b/source/tests/pt/model/test_smooth.py @@ -9,7 +9,6 @@ ) from deepmd.pt.model.model import ( get_model, - get_zbl_model, ) from deepmd.pt.utils import ( env, @@ -20,6 +19,7 @@ model_dpa2, model_hybrid, model_se_e2_a, + model_spin, model_zbl, ) @@ -59,7 +59,7 @@ def test( ) coord1 = torch.matmul(coord1, cell) coord = torch.concat([coord0, coord1], dim=0) - + spin = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) coord0 = torch.clone(coord) coord1 = torch.clone(coord) coord1[1][0] += epsilon @@ -68,52 +68,63 @@ def test( coord3 = torch.clone(coord) coord3[1][0] += epsilon coord3[2][1] += epsilon - - e0, f0, v0 = eval_model( - self.model, coord0.unsqueeze(0), cell.unsqueeze(0), atype + test_spin = getattr(self, "test_spin", False) + if not test_spin: + test_keys = ["energy", "force", "virial"] + else: + test_keys = ["energy", "force", "force_mag", "virial"] + + result_0 = eval_model( + self.model, + coord0.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } - e1, f1, v1 = eval_model( - self.model, coord1.unsqueeze(0), cell.unsqueeze(0), atype + ret0 = {key: result_0[key].squeeze(0) for key in test_keys} + result_1 = eval_model( + self.model, + coord1.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret1 = { - "energy": e1.squeeze(0), - "force": f1.squeeze(0), - "virial": v1.squeeze(0), - } - e2, f2, v2 = eval_model( - self.model, coord2.unsqueeze(0), cell.unsqueeze(0), atype + ret1 = {key: result_1[key].squeeze(0) for key in test_keys} + result_2 = eval_model( + self.model, + coord2.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret2 = { - "energy": e2.squeeze(0), - "force": f2.squeeze(0), - "virial": v2.squeeze(0), - } - e3, f3, v3 = eval_model( - self.model, coord3.unsqueeze(0), cell.unsqueeze(0), atype + ret2 = {key: result_2[key].squeeze(0) for key in test_keys} + result_3 = eval_model( + self.model, + coord3.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret3 = { - "energy": e3.squeeze(0), - "force": f3.squeeze(0), - "virial": v3.squeeze(0), - } + ret3 = {key: result_3[key].squeeze(0) for key in test_keys} def compare(ret0, ret1): - torch.testing.assert_close( - ret0["energy"], ret1["energy"], rtol=rprec, atol=aprec - ) - # plus 1. to avoid the divided-by-zero issue - torch.testing.assert_close( - 1.0 + ret0["force"], 1.0 + ret1["force"], rtol=rprec, atol=aprec - ) - if not hasattr(self, "test_virial") or self.test_virial: - torch.testing.assert_close( - 1.0 + ret0["virial"], 1.0 + ret1["virial"], rtol=rprec, atol=aprec - ) + for key in test_keys: + if key in ["energy"]: + torch.testing.assert_close( + ret0[key], ret1[key], rtol=rprec, atol=aprec + ) + elif key in ["force", "force_mag"]: + # plus 1. to avoid the divided-by-zero issue + torch.testing.assert_close( + 1.0 + ret0[key], 1.0 + ret1[key], rtol=rprec, atol=aprec + ) + elif key == "virial": + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + 1.0 + ret0[key], 1.0 + ret1[key], rtol=rprec, atol=aprec + ) + else: + raise RuntimeError(f"Unexpected test key {key}") compare(ret0, ret1) compare(ret1, ret2) @@ -207,7 +218,16 @@ class TestEnergyModelZBL(unittest.TestCase, SmoothTest): def setUp(self): model_params = copy.deepcopy(model_zbl) self.type_split = False - self.model = get_zbl_model(model_params).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) + self.epsilon, self.aprec = 1e-10, None + + +class TestEnergyModelSpinSeA(unittest.TestCase, SmoothTest): + def setUp(self): + model_params = copy.deepcopy(model_spin) + self.type_split = False + self.test_spin = True + self.model = get_model(model_params).to(env.DEVICE) self.epsilon, self.aprec = None, None diff --git a/source/tests/pt/model/test_trans.py b/source/tests/pt/model/test_trans.py index b9affac3aa..359e91d8c8 100644 --- a/source/tests/pt/model/test_trans.py +++ b/source/tests/pt/model/test_trans.py @@ -9,7 +9,6 @@ ) from deepmd.pt.model.model import ( get_model, - get_zbl_model, ) from deepmd.pt.utils import ( env, @@ -20,6 +19,7 @@ model_dpa2, model_hybrid, model_se_e2_a, + model_spin, model_zbl, ) @@ -35,35 +35,45 @@ def test( cell = (cell + cell.T) + 5.0 * torch.eye(3, device=env.DEVICE) coord = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) coord = torch.matmul(coord, cell) + spin = torch.rand([natoms, 3], dtype=dtype, device=env.DEVICE) atype = torch.tensor([0, 0, 0, 1, 1], dtype=torch.int32, device=env.DEVICE) shift = (torch.rand([3], dtype=dtype, device=env.DEVICE) - 0.5) * 2.0 coord_s = torch.matmul( torch.remainder(torch.matmul(coord + shift, torch.linalg.inv(cell)), 1.0), cell, ) - e0, f0, v0 = eval_model( - self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype + test_spin = getattr(self, "test_spin", False) + if not test_spin: + test_keys = ["energy", "force", "virial"] + else: + test_keys = ["energy", "force", "force_mag", "virial"] + result_0 = eval_model( + self.model, + coord.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } - e1, f1, v1 = eval_model( - self.model, coord_s.unsqueeze(0), cell.unsqueeze(0), atype + ret0 = {key: result_0[key].squeeze(0) for key in test_keys} + result_1 = eval_model( + self.model, + coord_s.unsqueeze(0), + cell.unsqueeze(0), + atype, + spins=spin.unsqueeze(0), ) - ret1 = { - "energy": e1.squeeze(0), - "force": f1.squeeze(0), - "virial": v1.squeeze(0), - } + ret1 = {key: result_1[key].squeeze(0) for key in test_keys} prec = 1e-10 - torch.testing.assert_close(ret0["energy"], ret1["energy"], rtol=prec, atol=prec) - torch.testing.assert_close(ret0["force"], ret1["force"], rtol=prec, atol=prec) - if not hasattr(self, "test_virial") or self.test_virial: - torch.testing.assert_close( - ret0["virial"], ret1["virial"], rtol=prec, atol=prec - ) + for key in test_keys: + if key in ["energy", "force", "force_mag"]: + torch.testing.assert_close(ret0[key], ret1[key], rtol=prec, atol=prec) + elif key == "virial": + if not hasattr(self, "test_virial") or self.test_virial: + torch.testing.assert_close( + ret0[key], ret1[key], rtol=prec, atol=prec + ) + else: + raise RuntimeError(f"Unexpected test key {key}") class TestEnergyModelSeA(unittest.TestCase, TransTest): @@ -130,7 +140,15 @@ class TestEnergyModelZBL(unittest.TestCase, TransTest): def setUp(self): model_params = copy.deepcopy(model_zbl) self.type_split = False - self.model = get_zbl_model(model_params).to(env.DEVICE) + self.model = get_model(model_params).to(env.DEVICE) + + +class TestEnergyModelSpinSeA(unittest.TestCase, TransTest): + def setUp(self): + model_params = copy.deepcopy(model_spin) + self.type_split = False + self.test_spin = True + self.model = get_model(model_params).to(env.DEVICE) if __name__ == "__main__": diff --git a/source/tests/pt/model/test_unused_params.py b/source/tests/pt/model/test_unused_params.py index 36080c2bbd..a3c93cbe68 100644 --- a/source/tests/pt/model/test_unused_params.py +++ b/source/tests/pt/model/test_unused_params.py @@ -64,14 +64,9 @@ def _test_unused(self, model_params): coord = torch.matmul(coord, cell) atype = torch.IntTensor([0, 0, 0, 1, 1]).to(env.DEVICE) idx_perm = [1, 0, 4, 3, 2] - e0, f0, v0 = eval_model( - self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype - ) - ret0 = { - "energy": e0.squeeze(0), - "force": f0.squeeze(0), - "virial": v0.squeeze(0), - } + result_0 = eval_model(self.model, coord.unsqueeze(0), cell.unsqueeze(0), atype) + test_keys = ["energy", "force", "virial"] + ret0 = {key: result_0[key].squeeze(0) for key in test_keys} # use computation graph to find all contributing tensors def get_contributing_params(y, top_level=True): diff --git a/source/tests/pt/test_dp_test.py b/source/tests/pt/test_dp_test.py index 095994f8ec..271b8f1082 100644 --- a/source/tests/pt/test_dp_test.py +++ b/source/tests/pt/test_dp_test.py @@ -2,6 +2,7 @@ import json import os import shutil +import tempfile import unittest from copy import ( deepcopy, @@ -13,59 +14,130 @@ import numpy as np import torch +from deepmd.entrypoints.test import test as dp_test from deepmd.pt.entrypoints.main import ( get_trainer, ) -from deepmd.pt.infer import ( - inference, +from deepmd.pt.utils.utils import ( + to_numpy_array, ) +from .model.test_permutation import ( + model_se_e2_a, + model_spin, +) -class TestDPTest(unittest.TestCase): - def setUp(self): - input_json = str(Path(__file__).parent / "water/se_atten.json") - with open(input_json) as f: - self.config = json.load(f) - self.config["training"]["numb_steps"] = 1 - self.config["training"]["save_freq"] = 1 - data_file = [str(Path(__file__).parent / "water/data/data_0")] - self.config["training"]["training_data"]["systems"] = data_file - self.config["training"]["validation_data"]["systems"] = [ - str(Path(__file__).parent / "water/data/single") - ] - self.input_json = "test_dp_test.json" - with open(self.input_json, "w") as fp: - json.dump(self.config, fp, indent=4) - def test_dp_test(self): +class DPTest: + def test_dp_test_1_frame(self): trainer = get_trainer(deepcopy(self.config)) - trainer.run() - with torch.device("cpu"): input_dict, label_dict, _ = trainer.get_data(is_train=False) - _, _, more_loss = trainer.wrapper(**input_dict, label=label_dict, cur_lr=1.0) - - tester = inference.Tester("model.pt", input_script=self.input_json) - try: - res = tester.run() - except StopIteration: - raise StopIteration("Unexpected stop iteration.(test step < total batch)") - for k, v in res.items(): - if k == "rmse" or "mae" in k or k not in more_loss: - continue - np.testing.assert_allclose( - v, more_loss[k].cpu().detach().numpy(), rtol=1e-04, atol=1e-07 + has_spin = getattr(trainer.model, "has_spin", False) + if callable(has_spin): + has_spin = has_spin() + if not has_spin: + input_dict.pop("spin", None) + input_dict["do_atomic_virial"] = True + result = trainer.model(**input_dict) + model = torch.jit.script(trainer.model) + tmp_model = tempfile.NamedTemporaryFile(delete=False, suffix=".pth") + torch.jit.save(model, tmp_model.name) + dp_test( + model=tmp_model.name, + system=self.config["training"]["validation_data"]["systems"][0], + datafile=None, + set_prefix="set", + numb_test=0, + rand_seed=None, + shuffle_test=False, + detail_file=self.detail_file, + atomic=False, + ) + os.unlink(tmp_model.name) + natom = input_dict["atype"].shape[1] + pred_e = np.loadtxt(self.detail_file + ".e.out", ndmin=2)[0, 1] + np.testing.assert_almost_equal( + pred_e, + to_numpy_array(result["energy"])[0][0], + ) + pred_e_peratom = np.loadtxt(self.detail_file + ".e_peratom.out", ndmin=2)[0, 1] + np.testing.assert_almost_equal(pred_e_peratom, pred_e / natom) + if not has_spin: + pred_f = np.loadtxt(self.detail_file + ".f.out", ndmin=2)[:, 3:6] + np.testing.assert_almost_equal( + pred_f, + to_numpy_array(result["force"]).reshape(-1, 3), + ) + pred_v = np.loadtxt(self.detail_file + ".v.out", ndmin=2)[:, 9:18] + np.testing.assert_almost_equal( + pred_v, + to_numpy_array(result["virial"]), + ) + pred_v_peratom = np.loadtxt(self.detail_file + ".v_peratom.out", ndmin=2)[ + :, 9:18 + ] + np.testing.assert_almost_equal(pred_v_peratom, pred_v / natom) + else: + pred_fr = np.loadtxt(self.detail_file + ".fr.out", ndmin=2)[:, 3:6] + np.testing.assert_almost_equal( + pred_fr, + to_numpy_array(result["force"]).reshape(-1, 3), + ) + pred_fm = np.loadtxt(self.detail_file + ".fm.out", ndmin=2)[:, 3:6] + np.testing.assert_almost_equal( + pred_fm, + to_numpy_array( + result["force_mag"][result["mask_mag"].bool().squeeze(-1)] + ).reshape(-1, 3), ) def tearDown(self): for f in os.listdir("."): if f.startswith("model") and f.endswith(".pt"): os.remove(f) + if f.startswith(self.detail_file): + os.remove(f) if f in ["lcurve.out", self.input_json]: os.remove(f) if f in ["stat_files"]: shutil.rmtree(f) +class TestDPTestSeA(DPTest, unittest.TestCase): + def setUp(self): + self.detail_file = "test_dp_test_ener_detail" + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + data_file = [str(Path(__file__).parent / "water/data/single")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_se_e2_a) + self.input_json = "test_dp_test.json" + with open(self.input_json, "w") as fp: + json.dump(self.config, fp, indent=4) + + +class TestDPTestSeASpin(DPTest, unittest.TestCase): + def setUp(self): + self.detail_file = "test_dp_test_ener_spin_detail" + input_json = str(Path(__file__).parent / "water/se_atten.json") + with open(input_json) as f: + self.config = json.load(f) + self.config["training"]["numb_steps"] = 1 + self.config["training"]["save_freq"] = 1 + data_file = [str(Path(__file__).parent / "NiO/data/single")] + self.config["training"]["training_data"]["systems"] = data_file + self.config["training"]["validation_data"]["systems"] = data_file + self.config["model"] = deepcopy(model_spin) + self.config["model"]["type_map"] = ["Ni", "O", "B"] + self.input_json = "test_dp_test.json" + with open(self.input_json, "w") as fp: + json.dump(self.config, fp, indent=4) + + if __name__ == "__main__": unittest.main() diff --git a/source/tests/pt/test_init_frz_model.py b/source/tests/pt/test_init_frz_model.py index d156eddc41..223b28515d 100644 --- a/source/tests/pt/test_init_frz_model.py +++ b/source/tests/pt/test_init_frz_model.py @@ -92,8 +92,10 @@ def test_dp_test(self): ).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) + ret1 = dp1.eval(coord, cell, atype, atomic=True) + e1, f1, v1, ae1, av1 = ret1[0], ret1[1], ret1[2], ret1[3], ret1[4] + ret2 = dp2.eval(coord, cell, atype, atomic=True) + e2, f2, v2, ae2, av2 = ret2[0], ret2[1], ret2[2], ret2[3], ret2[4] 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) diff --git a/source/tests/pt/test_loss.py b/source/tests/pt/test_loss.py index 484d62a3ad..dddc9af219 100644 --- a/source/tests/pt/test_loss.py +++ b/source/tests/pt/test_loss.py @@ -1,5 +1,4 @@ # SPDX-License-Identifier: LGPL-3.0-or-later -import json import os import unittest @@ -8,22 +7,27 @@ import torch tf.disable_eager_execution() +from copy import ( + deepcopy, +) from pathlib import ( Path, ) from deepmd.pt.loss import ( + EnergySpinLoss, EnergyStdLoss, ) from deepmd.pt.utils.dataset import ( DeepmdDataSetForLoader, ) -from deepmd.tf.common import ( - expand_sys_str, -) from deepmd.tf.loss.ener import ( + EnerSpinLoss, EnerStdLoss, ) +from deepmd.utils.data import ( + DataRequirementItem, +) from .model.test_embedding_net import ( get_single_batch, @@ -35,28 +39,17 @@ CUR_DIR = os.path.dirname(__file__) -def get_batch(): - with open(str(Path(__file__).parent / "water/se_e2_a.json")) as fin: - content = fin.read() - config = json.loads(content) - data_file = [str(Path(__file__).parent / "water/data/data_0")] - config["training"]["training_data"]["systems"] = data_file - config["training"]["validation_data"]["systems"] = data_file - model_config = config["model"] - rcut = model_config["descriptor"]["rcut"] - # self.rcut_smth = model_config['descriptor']['rcut_smth'] - sel = model_config["descriptor"]["sel"] - systems = config["training"]["validation_data"]["systems"] - if isinstance(systems, str): - systems = expand_sys_str(systems) - dataset = DeepmdDataSetForLoader(systems[0], model_config["type_map"]) - dataset.add_data_requirement(energy_data_requirement) +def get_batch(system, type_map, data_requirement): + dataset = DeepmdDataSetForLoader(system, type_map) + dataset.add_data_requirement(data_requirement) np_batch, pt_batch = get_single_batch(dataset) return np_batch, pt_batch -class TestLearningRate(unittest.TestCase): +class TestEnerStdLoss(unittest.TestCase): def setUp(self): + self.system = str(Path(__file__).parent / "water/data/data_0") + self.type_map = ["H", "O"] self.start_lr = 1.1 self.start_pref_e = 0.02 self.limit_pref_e = 1.0 @@ -66,7 +59,9 @@ def setUp(self): self.limit_pref_v = 1.0 self.cur_lr = 1.2 # data - np_batch, pt_batch = get_batch() + np_batch, pt_batch = get_batch( + self.system, self.type_map, energy_data_requirement + ) natoms = np_batch["natoms"] self.nloc = natoms[0] l_energy, l_force, l_virial = ( @@ -177,8 +172,8 @@ def test_consistency(self): self.limit_pref_v, ) my_loss, my_more_loss = mine( - self.label, self.model_pred, + self.label, self.nloc, self.cur_lr, ) @@ -192,5 +187,179 @@ def test_consistency(self): ) +class TestEnerSpinLoss(unittest.TestCase): + def setUp(self): + self.system = str(Path(__file__).parent / "NiO/data/data_0") + self.type_map = ["Ni", "O"] + self.start_lr = 1.1 + self.start_pref_e = 0.02 + self.limit_pref_e = 1.0 + self.start_pref_fr = 1000.0 + self.limit_pref_fr = 1.0 + self.start_pref_fm = 1000.0 + self.limit_pref_fm = 1.0 + self.cur_lr = 1.2 + self.use_spin = [1, 0] + # data + spin_data_requirement = deepcopy(energy_data_requirement) + spin_data_requirement.append( + DataRequirementItem( + "force_mag", + ndof=3, + atomic=True, + must=False, + high_prec=False, + ) + ) + np_batch, pt_batch = get_batch( + self.system, self.type_map, spin_data_requirement + ) + natoms = np_batch["natoms"] + self.nloc = natoms[0] + nframes = np_batch["energy"].shape[0] + l_energy, l_force_real, l_force_mag, l_virial = ( + np_batch["energy"], + np_batch["force"], + np_batch["force_mag"], + np_batch["virial"], + ) + # merged force for tf old implement + l_force_merge_tf = np.concatenate( + [ + l_force_real.reshape(nframes, self.nloc, 3), + l_force_mag.reshape(nframes, self.nloc, 3)[ + np_batch["atype"] == 0 + ].reshape(nframes, -1, 3), + ], + axis=1, + ).reshape(nframes, -1) + p_energy, p_force_real, p_force_mag, p_force_merge_tf, p_virial = ( + np.ones_like(l_energy), + np.ones_like(l_force_real), + np.ones_like(l_force_mag), + np.ones_like(l_force_merge_tf), + np.ones_like(l_virial), + ) + virt_nloc = (np_batch["atype"] == 0).sum(-1) + natoms_tf = np.concatenate([natoms, virt_nloc], axis=0) + natoms_tf[:2] += virt_nloc + nloc = natoms_tf[0] + batch_size = pt_batch["coord"].shape[0] + atom_energy = np.zeros(shape=[batch_size, nloc]) + atom_pref = np.zeros(shape=[batch_size, nloc * 3]) + self.nloc_tf = nloc + # tf + base = EnerSpinLoss( + self.start_lr, + self.start_pref_e, + self.limit_pref_e, + self.start_pref_fr, + self.limit_pref_fr, + self.start_pref_fm, + self.limit_pref_fm, + use_spin=self.use_spin, + ) + self.g = tf.Graph() + with self.g.as_default(): + t_cur_lr = tf.placeholder(shape=[], dtype=tf.float64) + t_natoms = tf.placeholder(shape=[None], dtype=tf.int32) + t_penergy = tf.placeholder(shape=[None, 1], dtype=tf.float64) + t_pforce = tf.placeholder(shape=[None, None], dtype=tf.float64) + t_pvirial = tf.placeholder(shape=[None, 9], dtype=tf.float64) + t_patom_energy = tf.placeholder(shape=[None, None], dtype=tf.float64) + t_lenergy = tf.placeholder(shape=[None, 1], dtype=tf.float64) + t_lforce = tf.placeholder(shape=[None, None], dtype=tf.float64) + t_lvirial = tf.placeholder(shape=[None, 9], dtype=tf.float64) + t_latom_energy = tf.placeholder(shape=[None, None], dtype=tf.float64) + t_atom_pref = tf.placeholder(shape=[None, None], dtype=tf.float64) + find_energy = tf.constant(1.0, dtype=tf.float64) + find_force = tf.constant(1.0, dtype=tf.float64) + find_virial = tf.constant(0.0, dtype=tf.float64) + find_atom_energy = tf.constant(0.0, dtype=tf.float64) + find_atom_pref = tf.constant(0.0, dtype=tf.float64) + model_dict = { + "energy": t_penergy, + "force": t_pforce, + "virial": t_pvirial, + "atom_ener": t_patom_energy, + } + label_dict = { + "energy": t_lenergy, + "force": t_lforce, + "virial": t_lvirial, + "atom_ener": t_latom_energy, + "atom_pref": t_atom_pref, + "find_energy": find_energy, + "find_force": find_force, + "find_virial": find_virial, + "find_atom_ener": find_atom_energy, + "find_atom_pref": find_atom_pref, + } + self.base_loss_sess = base.build( + t_cur_lr, t_natoms, model_dict, label_dict, "" + ) + # torch + self.feed_dict = { + t_cur_lr: self.cur_lr, + t_natoms: natoms_tf, + t_penergy: p_energy, + t_pforce: p_force_merge_tf, + t_pvirial: p_virial.reshape(-1, 9), + t_patom_energy: atom_energy, + t_lenergy: l_energy, + t_lforce: l_force_merge_tf, + t_lvirial: l_virial.reshape(-1, 9), + t_latom_energy: atom_energy, + t_atom_pref: atom_pref, + } + self.model_pred = { + "energy": torch.from_numpy(p_energy), + "force": torch.from_numpy(p_force_real).reshape(nframes, self.nloc, 3), + "force_mag": torch.from_numpy(p_force_mag).reshape(nframes, self.nloc, 3), + "mask_mag": torch.from_numpy(np_batch["atype"] == 0).reshape( + nframes, self.nloc, 1 + ), + } + self.label = { + "energy": torch.from_numpy(l_energy), + "force": torch.from_numpy(l_force_real).reshape(nframes, self.nloc, 3), + "force_mag": torch.from_numpy(l_force_mag).reshape(nframes, self.nloc, 3), + } + self.natoms = pt_batch["natoms"] + + def tearDown(self) -> None: + tf.reset_default_graph() + return super().tearDown() + + def test_consistency(self): + with tf.Session(graph=self.g) as sess: + base_loss, base_more_loss = sess.run( + self.base_loss_sess, feed_dict=self.feed_dict + ) + mine = EnergySpinLoss( + self.start_lr, + self.start_pref_e, + self.limit_pref_e, + self.start_pref_fr, + self.limit_pref_fr, + self.start_pref_fm, + self.limit_pref_fm, + ) + my_loss, my_more_loss = mine( + self.model_pred, + self.label, + self.nloc_tf, # use tf natoms pref + self.cur_lr, + ) + my_loss = my_loss.detach().cpu() + self.assertTrue(np.allclose(base_loss, my_loss.numpy())) + for key in ["ener", "force_r", "force_m"]: + self.assertTrue( + np.allclose( + base_more_loss["l2_%s_loss" % key], my_more_loss["l2_%s_loss" % key] + ) + ) + + if __name__ == "__main__": unittest.main() diff --git a/source/tests/pt/test_stat.py b/source/tests/pt/test_stat.py index 3a09f82baf..e69caad502 100644 --- a/source/tests/pt/test_stat.py +++ b/source/tests/pt/test_stat.py @@ -180,7 +180,9 @@ def my_merge(energy, natoms): .unsqueeze(0) .expand(energy[i][j].shape[0], -1) ) - return energy_lst, natoms_lst + energy_merge = torch.cat(energy_lst) + natoms_merge = torch.cat(natoms_lst) + return energy_merge, natoms_merge energy = self.dp_sampled["energy"] natoms = self.dp_sampled["natoms_vec"] diff --git a/source/tests/tf/test_deeppot_a.py b/source/tests/tf/test_deeppot_a.py index 9b4d64282f..f40b57c213 100644 --- a/source/tests/tf/test_deeppot_a.py +++ b/source/tests/tf/test_deeppot_a.py @@ -804,7 +804,7 @@ def test_convert_012(self): convert_pbtxt_to_pb(str(infer_path / "sea_012.pbtxt"), old_model) run_dp(f"dp convert-from 0.12 -i {old_model} -o {new_model}") dp = DeepPot(new_model) - _, _, _, _, _ = dp.eval(self.coords, self.box, self.atype, atomic=True) + _ = dp.eval(self.coords, self.box, self.atype, atomic=True) os.remove(old_model) os.remove(new_model) @@ -814,7 +814,7 @@ def test_convert(self): convert_pbtxt_to_pb(str(infer_path / "sea_012.pbtxt"), old_model) run_dp(f"dp convert-from -i {old_model} -o {new_model}") dp = DeepPot(new_model) - _, _, _, _, _ = dp.eval(self.coords, self.box, self.atype, atomic=True) + _ = dp.eval(self.coords, self.box, self.atype, atomic=True) os.remove(old_model) os.remove(new_model)

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 R@Da2HvBjU6Zf(kd{{sexSU&&& literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_1/set.000/force_mag.npy b/examples/spin/data_reformat/data_1/set.000/force_mag.npy new file mode 100644 index 0000000000000000000000000000000000000000..844df39b76e03bada75d4b57cba5c4f226c01596 GIT binary patch literal 46208 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 literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_1/set.000/spin.npy b/examples/spin/data_reformat/data_1/set.000/spin.npy new file mode 100644 index 0000000000000000000000000000000000000000..1444e35c5fd898f232eeeb76afd5e57cc3e5cd35 GIT binary patch literal 46208 zcmeF)XE>IB{5NnEDJr93l!Tr<7;PShwMEr z^D?4@`*Z&P_k;WK{ltTQzh}SK@jl+4&-p!1j*g4#@={qtN%aOLg*}DKVIyNZL)*hr zoQI`vpE%6VdHA-q?LAxlJGZQDjf@GusBdX!3~pztZ(|Jq&nF?w&v{bf2tTJQ=l|z_ z+vb8=E_qgsBNOE;_5(>n*lvj{W~gHnNhtQOhk$DZ3veC;zpeA+`3U$_y#&h}@LgH* zVMoCEBNMyZzzrM*@`AvxwRK+D2cG;uW~3YZ#@=UFV!;hX)Db(lO^2a+FSsLJX45Nh zT&60%7d-86t@uyyty3s@(02lje$024M;w{rL6C&|=7At1*?qqe(;PA){w9?2kFcv#q1R zb;OpUU>{VMVEAJSoSkK}B9X6HD~^YAKr@T>)nssqkEi+Hfu9~MKS}t~t5o3uaCJ5= z?-=msP9!&O@Z_$)(Qv)kJLdk2*WfWW?HQwr@m{=W3Mzs+OtVxc8 z=$nceOD*Id{rUtQ$ojzt4Hz+)}Uy@BcmjBUY{hDP4K&&r(cAX=_re#gJcE zP0hFn`Sgr;>U@yb_h=lg12=h6*DVV!94M|M2(IIucD)chL4S6yD!BW_SItDfLCJV) z4Y=wZN=)oY^FcL*8$97_+2=hWeJE=!?${B*Hf$0q-c*-P#tL_J6pCsGP|GAmtHJ~c zU)*fjB+NU86)jHXH-n$*^F1OAUOIf#9{O>t%yFt#@EpgR&T8Nx&Yx~bf=8D11;bnv zq?GSW^qfDhwJZj160Xg6J1Gnqo!mTm|JW2p)Tyr&?oDE~u}GU1$TQL3ehd5K;rXw9 zM83gn(7p-WDSz)eTrc+CT4}}tzM(h&zu^=%;+=`Q5#zY}=&L8=S`pYTBK34s`V0!r zWf&aSBVp1Za^{UKL->o?$wIy2QItiK|Mk1Z7C;&GPh8xgpunEMSG`dJ6MRJ6g>N~o0MG$L@` z{O-IgaJIO08zt~RJQUS2uuqAvY;_g5q!-&AZ}8cWsN}!bBd`h^h437>?(PFE9uec{ z>ak0QyKh7w8;iYXN$E3q7A5g-;T}K{9!ZCWo$FD8S=leQ%@;AGaR2eh)$M41ip5S#i zFK{5u2y}5!xv&rNpYEUnTX4sO#K~9M5s2&g%-tn$#g)8=0pNlvGb?YQN2V|C+Y0!9 zkKo3hz^)1bjv#?KB=cf_o8hT?WUakIEt=kka$}6zUV$^lC)b;UN4{ru7Xzp0{Z$_V zer|{Gd1G+axOz!RaK}%FSA)SB;O$;WVOWFy;Ig*b`5F^$^wvr@g}$ zN4WmF^I^DN9M=D2#T0y#2x%*v50=%CmUelTB{b`kmbf1yO5ak3*T)aD zKo5GIamE(*M466P3ipEFy{mpF20ZhSs+k$MFLUbg)#fss>)PHx_|vcLiq_zd_fROU zfoJF`9g7E7JW;0t=Yzz~gunR({@kG73id~b64^xVg4=WD4wArAnEme(Jq{z@zjVNj zxO^UOhq1{zPsu%jLtB#P0M{B#FD<+)4_jwQJNVxs{H}~KFPam zIs*B)>Iv3($j>V(%cw&>J?bUPAb9=mvs!WB>mzTs5zjx0y zaI78p)bqgpS#)W>WaYv6e%!Cw|3+@p0AAGg4TXLr$ZNrv0Qp$vYT0X$|KM@sE9`@W zMtpX|K3F_iPZRdU&+jDCkAW++H7$Jr?-TzkqzSIi+ucsg75C)(L(Ju4GHxRBDnfhD z54QFZM-7*hXTIylHfmVXnY9QeELmN7JPA!p2SJ;W`<8@ z;@}U+&s62XQ@#dmC;Zy;O_j&Nw;xtFR0fYcr(Gci&T;ue(wZma63o(8xWJKZ;xk|>(QrWK0gFc-(%(tLUjyeQ+3DD005ZiX$x^I&qSG^;GdiJq=^+X+9r z^Y-EM;0vu%`=!7i6eJ7@fuCY3H@L40}kEej*_2h1jCI5UgM z|2_X3{QRq3c8yr#El1Te)t|2%UqICvEc+JkM`OIZ^z%G;0wZ-P;SvulbmqZ7ap-o8 zf-@#lUr__M?dO>v0bgCBah(P)J9|5r@WeCPGJn9QA91>lgS%?hvJviV^sS}(e|omR z90T`JeR_lFe^c)h(+hqdS2Xp3bMTrTC;YzAms_pipV^gOlEG^LzD`J;VG z(W;P-QM~`61M)RN?Ury|c)+t~?0mMb_knh_naCHFud3V|+!fTV_E^f7tMx&kwRVKj8ZA~Z$m$onK zU4#4v-~NAoJSVB-gc55fPA>SceO!JP1#&$&xoSRyT9r-(T>)2~qUs+7XI)!hC;USD z?1BQg9K&=Nk^d0#tL88G_+#F)s^G?rmeeHhcuoIT6c$6shC-2A9o+n(P$=OsKlgnh zdbmeJBhmNb&%E>4+J%N*C2yTQJ%R<)9IhNcG=uqKyOcCoJ5kv?YFfx+jYsDsAdj`_sjeu4 z?=|HJB)svUAnh7>=z(vGXFSGG;9Bsfvztj+=6L{RP{<(Oar_ZKk(c-xstV_TixTy3 z5*X`^V z?KvDt$sL8G8vgtKzrkODVRa9Yj^-f#nf6B6MU8}>e@{+4sXK;Fadd<>UK_;ME|i?w zr%pn;ZnOfGx5iKg&7YP_;C;=rd74*As7kXXn+;s`+|Vw{SF!mGm~kZ0Q|#TW!m zN2ZB3hy2RS*~w(cM`znTF$ez>>vby^d{Y={Z5rGr|Au!uxMy%j_855NR3kg#o4>pr zW!D=+%^!54T)>5`JBQ1`>*Z{ZT!Os%!G|lQj-jL$=^l5oX!O4e7m#;`Zj#7)=-$$o6>`U-xYN``j^e98A{^ha-JF#@>w1^v`ml}WH!T+FnU589I%>@-wNOXlcQvZdfWG@;Z@+>n0(;EAc~Cu~iHn zro3CBC|!x^hg$e!vgfcw{o?y|Qa9Rs?4a0l$Uocj%W4|(R-tZliq=Su!z$Zg4Bu;_fju=oo z4?W)R&929_!Ow@A>pj3xmy^WF@jEf7lKJ_u<=9pf8=8GBLTDaUUM?tM;H^iU62!t%>0wc*^06?PK82c%Rv{ zfg@jG%5ZRA zug&qcTLqtdWp#o9oJ&*jWj=V?*5~R&O&wUco-|taz8}eJ_pqg~krCf>Rly0!i`hPT zxdZZ^nj@57A%AtjWquu;_FLz1X7B}9d?FA0ox*v6Z7(X2UWex9J>Vy`qN5AJ1DXf+ zZ3kzQ`OQn@cdBrRVsJ+TO8T|H3T)8k)3Gn76A#U&-zi?0!wjZHevjL1k;xSIhn2LO zcxvz_`wsmDeBu#bFU^YzJX^16%>j9S3h_oFpCQ+|Z3%iPs-+xYF5X?+Bmn(b)nSeF z2V9=_#^`SF%d*icun&IGx?kWgxc^zpAU5##Ji4w$;NP=Y^QfWUwI(PZ&JFiUR0|RA zbJHtk0rI0755BR2*9iSVW#Aip`~Ugz-J=iQ?bcerFD9)!z0_my_ew^^A&X?B{DC%C zHF6%;6xE%|+5H23$#6PQNck0+eB$ER1^KO2AFcQxZ|-&TZyMwiL^~<>fSdRu-W%Xu z*}<>rEt3%{QOsISo5M3Z+_&(DcA|+jze~;%jfl5(SAgvCdF;bIRGRy_4)4C|(DdnT zD;|vfe3wryjEamweM^PzU~X9EXFW4`!QYjMZb1 z!5S6cphYqs{#^Ko3A{un&qp8JZjOt27Q8x2T#x?Q97@=9LblAW4~dx9@z?X@;X_7G z{=ju1J%-on+K{IfyG~aL`EQ1H6^!7r`)r%dz{~2I_IHCH-b+2hzIPrqMJYZXe%OdU zljxeFeOhsIN{cixx2^J>uP@|(aN!Z;?F-IW#W+L( z`H>L4EDi8)m5#$N!2bm2n-fk|Uj5Pt+(m6$-(&DWJ*{u6(Bnq0%C7`Ixb^0bU~sKN zO#^G-y4p4I>fmCY%S8d;5tI+EZGj$qDEgTxIJKX33W)wDxUN|KhJ+O{P_S#-&J?_om-do9jS^eWkRL>@22zq#m6-&6FM^m=VeLwU( z*?%fiqKbqxjvR0$n~Y-Lu%im~!-GhWv-Hvg1DO*X{r|@KL)5tpHVO(}7N&bD?ESiqh*`yO+hb?HGM;oCh zu%>JuC-krxyGPML&-|*V2njqMQzmeOdz#s&af9>47jyT4Q;eL9-wXaMjb#3Zx(EBs zi$@QG-_sJIh5gakN!QmbkeAjvFFXeB9k6zh2YinKl`R|iKmP!3@ByHyvD^DX8qr!* z#+!ml5)w$dJpJ2x3e)I_{<_znfsB^rT`i{i(B}F^3sLtutoT*<(%Zky$g{51YtO4` zWVh$RbP)A8KBDiKbq{*}To5Wwg`R7g8N$oZQ|0?qBmi8e%ky?1_^D$b`+C6HpVfZu z4{1O@`rfg}Q#T_Q#(*CMdh=M>$9AhPD{A1X;W?6qzGsPz;&q@S$g3SKZCko*QbUd`BX9Nh8CfGgo| zS7NsluF}g%E|65M=yNX;Vn_TvT5Z-d8N-~8kic#95WdOtX?u3Pan zX(ybf(MXpaGlHKRYTt32n?>$|6{3NVzgMlqPVD1D$yNyaBk$C^2R*>2Ug+i!bBjl= z2o8d8@a_NS$8)=L&6<6L-xv6Dm$&ZaD571ByPthx97R2(@!erEgR|!@-hJHOhnmzF zuPap!qD{3{U5wy52X1N*9zJCvsSJKE#WS`hlZ2(;{Qmk-y$Wrb^-&M!o<8= zpA0QC^o0DJ;ho7#kbhZhCC3I%nXGB+3vP4PD)1_}RE=Z9FU1)wuQuP98!?DNVuj1d zC;QQ+$q)3z+=)=3^{0>*e>5qo2Ki@l)j%-3eemD9{U*D>gUVhV zC49^EoM;vBR!*+zz2L_rn`*qlhb}!$fa}8JOS5-(gTK$)=1k;!`TJ88!MVTIpJ4$% zU*i?v1^#fK`zBRzw-o+Ia9yY*nZF0F3zxI>=Meq5JC%Zoo}XtTJ)eLV<(W(o`}lcj z$uWcPxc%ra(eE1)6iMX&`3G==51{*s2(4d0ITq@WmdPI_BaS0?x2}I0z!S#a2W7W~ z;`+agnw9hm=;XaB9s|1X`1>F=iy`C>DM{6qK%VR4Ir}BZOUJcHyMg!g$^R+_FL4aJ z`x|_>*5M0=@4B$Ry27hVH^x!Z^}_a&r;}LganD?7T^Cm9I^XqVjf85Rq?(&dk?=G3 z7kBSK&ybR2HqlQ$M=L!IJz61|o<#qh8&rx#;0cd|p09!1ZgD>28&Hlyd3I>QT)Zc_ zRe#gx0W_wp^aJw9_s{!Z(1UX#sdg@Y8bD_^?FxOD+m7^>r=4%o&S9P_*34Z^U5Ho8 z@X~j;LG;D6UF%TpBre^@aW_M27+nr$dh-~1oSDaRF!b~uQIGlqJ^dYe%(uWrO#G9H zez~@Ve4?K%Te0ov?`*Uz`Xk?wJd3&OE}y^XFoJl+=8xGxK4{C=1fu6~>5$rH*mHw# z|35$eg2r_g8KYUuZ)X{ir9O(T?NavPU8zEXl;*3_;52bhPvwHUJ3Vzg4gT?>LZ__S zJZ{K)yE$)9BQo$ivAq9bGg@2jlsFA}G1c<*bjXtmqTk(sd=+zNF5ztZ#x7@r*EER> zUjW}@rCCMv|5bP&Pxy=30xSz|Kc!wsxTXx76tRy1?e-i5ZhlVOpYZ6VBd7Af)3qC0 zG{6TW-;qul&7y-6)-8qVqqso(ERD=c6;|-9TB50$KnC~U<|&p=;@9n;X`FTI@&BER z{5&NGp&vhZ;LHivg}+_3xN`=2{-g-{rGa0jdz5PcKDckvW6gUEMfD1?6;MpzNgapF zn#Tt4px~$RFDE9^TglSB<_#m5zvY#HQNRGc>~Rny=+W-n6A0IZgXRq_#GvPJ+KD|y zAv35Zk=o5SehjY_I9+1v`TpPc{|){MoaK1!dvvA?(@=?~hN{h?{TcoGItPtVI6 zO5o32jlv?px0V_;90qr5bQzZgU*(Z3AUxECUhycnpzQ$;eel-X9-^_}0fmWwg~996 z?+tsFmSB5riz&6EW2o(~wU&U)EV5xM-g_GIH9`}+h`G+bZi+`BZ?gYkEzuvo%jsPd zxaC#7g@fSLM>!niz}Z7?NrZ!MId{1782HNWT)C4oU5K%k^yszPEEXU){^m;<#Db2F zQS#v2o_=eD>;IG$;skFQEggiph`JUR7H|&>0v`-*`~!2b z`bgBp3*cd2oJT^y-#HGg5WbLnR3$aU5?5;7TAL)z;-<%vS+2PwSU4{@iP$ID*NTgn z8=Eszv=8!8RK|y(2+r$$jxrKlybRzRuzmm4`>;Q%T=6(81Aa2K>jgEqjghkGTW}`F`UolT z*BrNl2=7c%Tea{?9k4I7T8*Wh*+evit6-_Aa6ybXMVZ~s3({>_dd zyG@xr*iHX1{|!qrVi&w3duLZS&dbl@-U5F0r$y5h@O_`(r>KKF@5;_O0vcZ%&hJ+>_JNxzrQOv8XI=`5(*hr8KP+<*Jd`Vh z(-_=2&$_}HoZQnE&IpdUhCdMfiQ{LM^uZ1JFFg63(Sz%?s`=Hyb2+JcOu)}(xmoSW z>_Li?fmgtB;G*PiaCBzsd1{Mp8?u%BbIi_Z6z>_F;pP>aK{NGk(<+dEk+qy)2zj=o zjKl|!r?ppODOMOlS4C{CQ)|aDeQhP**pV^x#*JwsSU26>G%=O6FuAI9ZwDJ5$VQ$3*0NK=A|R} zKmP!3@Bwg|vYNkStVYgJXLJqANAaZFr}&+xW|8%T{m$dd11R2$OS88E-rv2Az1T`; z2;C}^rWiNvM|txH1db@o!Oyw2EIpViMw(Z)Xa`GB#~481i;}B`p%* z8HL;OCZLC(A?=|HI8E3z#Rc%}*H0h*2LF)y>bN&Jm5a|7CGfEGxPJ|t6sRxZ3Qqqr zwkotQ4hN>8&nne3Sf)+lqLJe`TBl&xC%rs?&+Hw&GFdT&X1DJVg*-M;f7WyaoFOOv za25DecIV4m;8l_4EF$1Wv<~5LU1%R`{YwY%J!^mb*udHD-zY2wAIa@9)(8K2z2+4k z`1>`0`r6m~lim0m@0+Ng4~@8A zw7SC{eC?5PXBha+<{yu;z{~IU{jdd}ucDlN1|D|*gi#IP(AlR{_e>zK#->{24|)EiR2HJ&_+D6zDfmFx zZYxvpk|RQ`f#8D!w~L6m)b3Y!1Hippyr&AmnR3rb=At>&`5-{UqM#p1kh3Ip?i8Z) zoLt9Ttmn{eby35IlijF!S8ztrheqV=n3QJ)uDi?B26|BH5p$+5;FrI*1v!EHN@r`n z0+Cz2o^&8sl+^Td z(x%AhLYJ1Hm;$`+evx!m**^ujCi*-}1t&l8a1#fQO1jn_5B@2QJBe_Fji!!+e-xBy z^8)8d=s%bO?&(so89e3@uO^oK?5 z59!*8!)0znn(6PWnq1qQ{|rFQplHZt^1q!iOy$jlgwb%5@Qn5b%L- zytfH2In7)^h&iplljo$qRt#9Zg_^i0X~ zP}W)`+oJYp@gn@bz)c@ELGU;cnYlyYF7p$Un;@_JNxzC8eBp6ygcNvY&?Yff@W&b_ z8BT(4Gw0}W1m}$3r!WIODt0|GL|*^mqsi;whgc3zGn;=a)9t$>NCont3m;3j9B9M=%eBFjrt|PQC~xP_GBlxk^JDKA<$BR~ zWf%1`uJC>R^rV&tYb}-yewYgTqk6Iiy9oHxQr@Ed;I0yhUi07 z?ep%k*X~DEBfqw^!1rktT1-Xj^%AS9zn5=^ym_0&US7z{YIDV1guJr6%9m|0x5H(; zMHKvopiXW%xT^P7aT;(^Q+u&EIN4B4wg{ZfGBR}tdaBwCmw3PpFYdmg0shZFfE#=O z%a^Bl`mCoheA)*^(XSS)HK4Lkx;}_*-WRvBbeU_l|cT}Zkok%@HRF>-x2W3t`Eiwz@I9$ND)1rMzx${-~!K- zvz)3M@WAk{(`3{`wd35+nh^9LMk66D zcNjTuQp;|YCL=e!kmpZ-v7c7GgRv zBm_MV?_GG@Q5}Mrv=+B=v~3g{tPK!byBR(^mPk*&LRP z3efm6QIBr9=?b{B^kV-f?nAuLGiUnT=P>jrdMu+I(DS|Z>y|&VGkDYQga9$=bgXNX zArY|azt=xE`0=Mm8fU`xjooODt|RtGOg>H(Y&+f2KZobaNB%m1M~HtZZ3l0^F{ecM z$YtJ-gd1FqC~E~j#J%oHc$JpC0paHcn(}{w+tJJQ`~>%RJmpU~^Pgv`o#3ZO?>;B; z-Ya~Z#N3ZUoOwk5=ZRZ%|J(nV%?X@u+VN#C_%6#dIie?yf6TxUT<_{`-!AZ&A)ZG> z{!ON%CiG+e>G}6Xrzuj)P}}*#)%>Ch zh?e(I!Yq9S{^{|=aD`t^(?iVN===Y#&wriu|a-bD|DtlCGx*QuA~$F!haGHh@Q#=-0ha&#h>Fs2f;~F zDsS7t8?vJWAAy_72_LCtnLwYe@7UkKKaMZTUhlHt?8h-h4LYztdiYYlp&oqr_+Mp1 z@WboVj?>_>S2{DGA2)QJ*=7bVXOpZs4&K2(6OajhyGdu4DL7(Q>YW7txaLw{3$Fg7 z&({r{(eKvfSjtIsE6}L0>3%;px~XbtF)@loe`oc>{wViV#5%E$cf{>BI3G;eMyb*X zeus6tWgB?VHQK{Y;Q#vbx6#+Xc=%a>z@-{o!>!-(if;zh(WJ=N+#1ITjl06Rzz?;k zRz!ib>GbXY2_B?l=eG}haYFlDGZ)@-Iap-VOB?)IeL(2uj~Gq~Gtjia~mrD3e+e&WPk zH*EUh-z%$J7YZ0w$I%!0h@-raH*awAj)nY-Vp2KmgKp0$W*-E<=<#Tj@P>1rXB)x$ z>%OH3zimNAu9Lsw45#t*c5RxTgX3sWxMB&;4gKh#lZ=PFj&$#@7RYyhGB`&#&HJRb zci@((@amL-8Kk)$RlZ0!W-`WOqG2dP9hXLTvo!D|uf`>&14XlH=cGws^123WU z-XjL?TCa2(UX#TRzFa{>&rWtB_2b}FJ9do1T#SEZ{epdP=9Y^QQbdp5&g(>v_zL9^ zk(b%@d>5PpqDiVaK=iENztchV>x=w7P3)uXE+tRw!%|JYLG=9V&)-I0f4RGTyS8l~ ze)`?kQc-sTbM+KTvTKZ^8TxWSn*p{g-H2K~H`unNoNN~#_R!JL9_F9?6SqVeo z?S?;4g0Ro?oACO~`8!mDmvtwQ%I(5bn2X&n6zHpihZj@GsDO)PF(q+=TL=a<68Q{A z=hLtcQhu6M40CbN&cdr7Qb~y6eCYU+^Ehf0Rh@pQ){k$rXeF9L-uRZO(M8D12$>r0 zhx}mR37#?ipUBzvgA}45Bf8PUf#%N!urYZjt1hvR^0!PlH+0fgxEsz5FMgc%f^$Y& z6rZ&b`>?ye?}l^!@A=>0FMvxTYfmIof8ZE>8uPV)IkbKH(dE_hpLp+`l}q<|;OF>X zvhSYmm_!%DokT^oM)2pj|d|&swOBxPS>A84C?mIrVhc1WwX%-LHTR2chHsg_KZW~^aVH~?ZbsJ+H z8H=xPlA>rH#8=rrWyL2U~~=oS5!HtcY{l5pD{iCAPsMeHj5SkCu=Hd6Z?o$S-7V`-hi|!O5}IQdox21 zGU{0)^|+*=d;O(VnsVP!Mhd4}RSg;a$(9=MQ*Xzni*^G?Zug=Ku6yn=Kz=~vT>5Ls zpa1Qua~<;Ur;;O~A8q|e$9xAoqSn-j@ROhDwTQe|ld^afcu!LAUM=ugfu=8v;KRpu zEq??Tq&l6g2VN$7HdttR7G+Z%2t?z}*k#L)XL_!?hq|okWTruwR^xTyuG*Gx(lVun_pWo29Z_XkgWemOJ(a-8e zhXoHsFoNscZ{D}lG9L{#9UnUnuIdr2J2Nzg`mT(-gb{grMj{d0H(%K0v2Zb_etKow4`GmO{hUai`cCF7D%Y1C1tmu9*s~casf7f$%XbyLkP(AFi$j1l0 z%rE@@+>K={#Cc&)d~lD@1U2L<(`&D8hWuk0?T5ReM`&LFRTJd9!g9?7!MS`H$P4Aip`D5F^W`g) z2JL6Z&{3n9H*{2O=-O~t1kv+++stPH$kztyxcr5DWHF;TIAW}N_(+I6iqC6QTCT*_ zVaGU0%lSSMGG7`qTuOw`hh~gWbc+6g3p_*(;vs)dMDO-7$TLliJY9!;MMJBHy$kF{IEo+#w7*JHR|EYd?e0dsM0ppiD51BxjYDJJ}w{-zJB z;C*@t^u#{&$=-qff(JLvM12D%*;m{ogR?a2bshxwd*yZNJNS#iG$z8s zIioX%FGu51)9agvo*MDhqB(Hu^KzC=fG-R- z=8b_@WO>q`1*dEA{?ZKo?Yd3&H2BYVM`dQdwxF)�Wtr4|Gv3!pSmg9t~VNGL@zn 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 literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_1/type.raw b/examples/spin/data_reformat/data_1/type.raw new file mode 100644 index 0000000000..d9664c7a22 --- /dev/null +++ b/examples/spin/data_reformat/data_1/type.raw @@ -0,0 +1,32 @@ +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +0 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 diff --git a/examples/spin/data_reformat/data_1/type_map.raw b/examples/spin/data_reformat/data_1/type_map.raw new file mode 100644 index 0000000000..7eca995c31 --- /dev/null +++ b/examples/spin/data_reformat/data_1/type_map.raw @@ -0,0 +1,2 @@ +Ni +O diff --git a/examples/spin/data_reformat/data_2/set.000/box.npy b/examples/spin/data_reformat/data_2/set.000/box.npy new file mode 100644 index 0000000000000000000000000000000000000000..4e817ccff59b0654c0a1d033d884d4a22b3ec4f7 GIT binary patch literal 2360 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%ohB^wCnmP)#3giN=-9J?f*y>aq#DCuDyv7@_Kj2RHyNH>F`%ka;_$(`= z=%8wS$4hpNn1ieKwtHtotnEbtckbc`iqG>{nYm#)i$mF|i4DcZA`YYGjE2u>`Wej^ TqvghEc{o~MjMk^LZYKZ$TE@=j literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_2/set.000/coord.npy b/examples/spin/data_reformat/data_2/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..aa515d0b6e8b86de9fe880a1b3ec9c5ccc8161b6 GIT binary patch literal 23936 zcmeI)dsK~C8vyW=OLS67N21iJ=tvjcL&N*z-u=|bMX@!lUvlZ{77inlZ@)W#%=brYP2U>dlG*3K^Q^u0df&C)wb$Oyex6^t zqpJgVl@ueC5y4sS&0iP7nak#Uvtc$zpUv427_vFUGr%J-WW6^XU*hS<_eOC(&ojsy z{hwiKtk0ffp{vi1V*ignCS#Ca`?!{p@e>rmUY*DLDN91FRg;S4n~nmuWaq1?D1Ok> z*KX=*Y1q1v(k)xAM=hJuT(3Mq6TD`Nh3_I1z`b;0Z7UkzCZ1v>9nFGKPuot`%7IYt z{?2%}98KYdfXLU(axM7&es#f_ObNA&nWbug#*g&6tvCV2ooAN­IB90k5jU8oW@ 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 literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_2/set.000/energy.npy b/examples/spin/data_reformat/data_2/set.000/energy.npy new file mode 100644 index 0000000000000000000000000000000000000000..cd4efe3b554213af05151778822ccce7d6db06c1 GIT binary patch literal 376 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I%ohB}%$3bhL41Fob5Rf`)ZQx3SkN)u@X(xEH3Sq`5_IiSXPRQU~%uh*3z z@*2ora`l#L6OcB#_I8mRkQOehD!v4iUtU=LJ{!oFs1Ltg59GT)l#l)mq&N4-aV`bY zxwBq>5(Uzyw4KUV0coAe1F6kGdiu9538p|gC~=+bE+B2SXG+BiAl>hGKv@GwfBRv$ zqzFjQUB3brf+()CYz#6kMECw~6g3Y7OLH=4{0q@=y~LP{x0#VgSus&kc=qKG0wC`3eq zBI?uo`zL(QZ|DByJnnPf*LaSr*u=`%)Q*zk1jQ*456^J-FcD>55mhfmkwd&9Ucq4z zVXi^W!C@Yr|6A8}4G8z#S`YVe4e{K17nPAZ#H%DPc8K>h@BimR|9!{w%wD1)76~heKx38@mWIQlq^d4 z9-}!~a|jP))qIQ_lfc|vZBzUwe-lS8Ub>>JHcbev{Wvk|A_2X1BkevKf5-wFc1J_` z_vE{;ISX7nY#{jlc8(9Rvd}oU5GmVz2xhELY-qKJqUPNp_3di{SQHYO7Zmx2X!@tx zrx7R!LW$4S#A}#=YDGPZ@u~pC{Sd7Ftni!=x3r$(3YsKNT}!$6=2A8Bo9FexvdRfE zC7a21HoulAty=o%_o$9!_!^cG?>9yo+R%ZxY_40lM!NZNpCi>YK+v%c*^;8?|pKyiZR#2 zqL~n>+S%tK!GqQ-?w@sBcSA&{f5Gk(jBxjk9Nhl*j5xokl@v-N0u;HmQ;ds;IF*d}!Liw35k=Jq6qD<=2a#KtO#Yra+ho2)`j{iqay@9*}U^fL;`t#YX%ibf2LxEX(^UlD@oIcpYf(>-Y0vQzQ( z4o0L1#iDZ&KS@uOc?QGZLYO^i5kwuOhl9egLc|qe{O?isww|C>a*?dOZ6+pmvo=qZ794*F_9(GZMTeOOh z8FXW}wv-s`OnN}|&Bg_(_Kw79DIdivXH0}5LTvGwU{FSSpdvPM&PrX>VM0FQ#tf=I zCg^Gz%O^P%@z!tUqoy>w;PNMq@Q0uO5F3X&KbJpK0UNh0pX*#v z)2K5&!VDJ2_mi6{vjkx0Wnu#d%2D0%#V^JRrCbfvMo&ofHGu+p6-p5;Rj z)tlDq{_+_);bZuh`;aV-tX?oIzd?ak;T&;;V=TD+#FRPFrhsdG-aMXPS>ZF?^9zAW zW2Dl%zelN7obdSlwnlDyGpy6x@f=zVakhxXgGTZ=JPkawr^Lhz^aOtgSsph8-da0x zwLS&-!>=YHe3BJ2Aa!^$_7hq6yusmLg)A^qjs}^|uMllt0(_SavcgN38KJco${2lh z@QQeyDy~`7avi_OhN72rOzoe_K+Qw{zN0%>p;su7F11|-iYxH4RSmvKS_6`1W(WAChzcaraF&4p=!#Pb(VyPXbWzOWA6E|$ais)#uB zg5C_wy67c;=hDGd&o3>GT$X4?9`}{j6~)NiT`3Pod6B+o@DmTcI=0t%2p^r1MPJa* zTyFeJZeDsY`#xI<3XLw}Yod-OXnoa3!-ij9Di&?ARWy3KquU-j zlYBo-e{{hO&f65qp>il9d2Hi#y)lj!aHq!bMWS{dL&2AjdbnRoU!oJ=2`-6=n&Oe} zAnWCH{4%#Uwj6Sro^XgopD1_Nqb#BDRp`2?*5wc=s`RQ!EjPp6pRO`pvk1dsn%^H% zWJ2NjTMaf9^oR2XYAll<rH%#_q3YG?^)pj}AqL#4uA?Uy+W%}j)3SExYhnb`OuK3m|7xg-!66b39$cG($a z#G|cL;M7^BBn*U$oW;^^;Fl#*4E@H)!zH=!R8$UyM8(W` z)?Lhw5adWVDNxD=|7nTlo@vp;49$&ef{M|2AX|4$Pbv((GvmMB$|}OOOkOu53VZyu zP?o3F6o}_N9cNB+Md81)6UqmAUBK|G^D?{M5m?Gyj*~d81VJ4eB}KAP;B-?i_pewa z9_XQEzNa3KU6)iCb<}KO~wFb5#%92qh zIa~~=vY7kqf`%0CmG&XE}qav;g~Cwx>>qd818h@e9QuW56a%= zp-_hKn2TcfzR3Ww;d$(c=K=i0tm`ePXoimYdKODZ04}(0aLU{l0-l7FZ4tM$aJ3`< zjd{W`G(GX!PS4~9`R#k{aW@Hl)V`=%UAkn3PQ<_R8N0nuts$#vGJio7NA{00cJAJRTqpZ>3~H?sNwZF&g)98vMqDYcyP|?qZ)X*G9k%|!sQSQLDofRJj zkl&a3=S?w+$k~e z-tnm@B6o>2-Y{&DY^8uP!;#v7-_)=$v(d0KlnyQmn$oR)lmdPAYt;|zgwA6^?$hTrd)v8C zeZKW*m)l`@c>Yvd$*u#?$YMBI_Lv1e(f-g+DinaM6~XI@uLN=J@BQP{Gsp1EN3nDP zYAJZdqR16U&xQ>pxm_b;Vz9#bj!A-F0nLjQV!a%c;gU(1D208+A87bjRQu37U1?Z;*Yz0u4^nrIV2qrq<;0^Q#Zny(zrFZedUBhG!UNFaM)jUxE>dvB9_ z9|CL>S*g47WFSkK^NaX(ADF)T-_xSWV~7TH?M{bcfy-mv^ze!VDpPqgcTK88@r*u0 zl2HWk%*c8FSU-Y^S(g%~nbhE9>(ikh7b4)6)PsuYJ>ig-9hP+7T@M#_$kN(0g`>wG z>NWDX4@OqCCCns+0-0rg;9{2_PXBw@sqozetV^lg&6T4djXrqb#nIE~)U}84pZjSL zwB2T%SQiDas&7kO*3yDR*ZlO3gd1=%@hzA(WSljMgWF#-QDh9iiD|n|Fm-k0dVciw(T6={?Hvg z>v)IOA71yIRi4)>0pDZ1_1E(vpjmPE_KTn0;lRD{Pa#LofGOJnU)x9m6VneI)YCl$ zflr*3< zfe-hyt{gDefF>W|Dzjs582j+B5}%p}*8P0C=SOl3K1nLzW||5E7o!I^Z2l!ecmBSA z4xdfI*UHHGJKY{kXAO$od(|7q`2IEAi;TwX0=8=9!B}WKqu`l15C=;Ry1lg5uL9M5 z#W!qA|DjR!-y5RZ{qR#Osz5Z!7X?9SpkYoGNul+;Kj#1`3Z5LE_A$rMT7E)D-w1cT znZ9|da6fe1rARO8DTP#>Qi;-gXR#j7GVq!YVkhxjC}Ttc!cX@{_m%3wm&P~07iJ8> zZZ780Mn(aAlgdgnIHmy{cK1zLqRt^pJy%4ESRp>2u=(~}{1Q|iyS6j8wiXOuk``1? z{m`7<S{6#Bn`e-b!w-fh=c3-xF_+DoxNsU zXBCZH63qAcZie8qA#M87Cugx-$Vs0n+!|LVe|0wBk;GSgJG^2vPvh9?Ptnx=1Pm!0 zqb}ypfY|n<=5eRovBI_fsXd)HBnMi!9|$vmGi8n?4V9&+VD{$OUjrGCE>HX0BO!@D z?7zFmFK0u>9@!JuyrOVu;LCh}N;oEmWzubPP5>>ntPgLu@_x%^NmBXmU~IS0uM@su z4Bod3<_ohOz_?*?uCIAN8t7lvf38u0lktIWylMJy=D@62w@L}VsA;2GjdO#|;DYt3 z%``CGGs5c;l?7?ad%CZ!S%d7Un@_Kso5AmhGTt6qFWBT+xm)v*$V7qHZQL`pF`(xl;hGqd1%+ju?_{Pj&{8V)9p7j;xKb{={GiQ-Ed)>_XX^^3 z1}A15F56&+;o$_kWn;9OJ@R$wV-QHKUyEc)_k?2#&)jlKJz=_IrYI^e7UP4Nra4Oe z;n`Dzcgnw$(6#5ly2I8!l#I{+3VmjR*ZElo$ZdB6P>51V<^?4qVg{b&Yyw9}54vrVGT*?jf2NOrfam?{XhGW5& zobjPBAojn)unQDCgtOWlRX}z)nN|}5!KKh@#8j>bUBm}(y%s12@99q&EUh;|p0ob5 zl3FE(eATM9?IiI<_AN)NfMBF8dGGax$rZP!db@n)wgIIR+}pDcT4IH#XJzi6Jh0c= zwaeE#1iwyEmlWQN#zj%DSH%Wi7`J9#Pe-i?Uz^@iG&u!f!Y1pj1bJ;BK4cp*oVCF> z#`J^HAM)|)i@H=gz9MMPU1E@|y$FMj!f{-j{P=I}BkRwD-uQRGU`3-XZi~;lFWAm< z8oJpp&<0%ag4=4JMgqg4fI%Vf-o$b!7zP?8Y^oaLH76|wskax9^}iOn$03aJ zWDUFIj*Zvtb%x^MoBCD^Tm9;L`V;Qj`+@9#VD4Bk ztr@m0NsP~QYQtt$)`G2X2>M;VdC)J-36%Fvq$S@rfCm~oY-aC;A#ZR*dp>0#Ru__+ zI%$dUSbFX9DPMJz%hhN*EX;;6!a-aw8G~PiHk4mdLAAMC6(8e=2?NvRB$_Q=#DG@1x@h<~ z2pUqV4=u)n4ZZ4E)zx@V4`)C1h)NA-(w1YoJf*NVZ%TFOx+pSGJ`!r3*1@i-zYfNG zq|ni`J1ruLcKMoT^knMb2 zc;3|rKg8+{3y24yO8QH>ds8YnF(T3H|I82N{c~UErFi4#%PZ#AN7Rs#)?sYmm^*r! z6`k>`Z~z^-Ro)8}dO$KX^A%TGL(xT#*EIgNFl${zZ+T7|_a zj>^v%V##V-nfsw+tZTJ14L9Dp*QwtZ&gUM4+Wgl4ro&_~ajkib^^*o1O&gTwEE7ke z;<7!*lGTvu>0sA_ggb8AeK@_!X)i9y5n0&gqRTC7HzzyMdHR7~$l zv`n=5IC5DNx*Gqq8#*cC@2+UUlP-c_D;AgdqQwrLE`L$IY;AI4XN!8Ab*d8yG zbx95Oi-e0ZuXYp1zQ|6^eLRXIvR(X%8KL-wC0Q+qIs=!-a~{rJPlmTEOdcvPLV=3i z*5GYL1|BtwyheYz7WevpKYP|W2BYRmjdc1?!s04#`t@s`z@B(d=67)*co(*ewQbJ? zLet&dUo;pvDa+#?Z;Jr?8{euiJ{N3F%#%(>$6!z2!xoE%NHlrX%ut?^1A=Xx4VraH zkQeVKzf=a7*9(PN@sJ# z$fXCN@XLv>v~3<>)wOnaW6cbHeLrM+H^&p3sR!+KV(-HCrhQ`5QJrwa*Oso%rWV_I z_OCJvN5kg)!RFDkZeUd>uEUn725+8JIgf>kL3v0LRof4HILv)#?EOzyczTLFX+d`v zy;zi4gvUB@Qk%)*)nqAXTgKJOZJa{)(1QtCGkW0tnyV~=${+lc6;yo=yJK23+ikDw z21w5)%In_jh}SMNF)^8#;fJ)2bl{LcKDoMeb31#ulu)R&>roy!tCs7XzibG13LK-4 zn7Tl~O;HYt-;TJmikkd%gaQ}TswUSq4IoLi%IKY0JQ!-!ww`n<0fEP9?hP&Z2!9tA zCwo1?dEL0|{a_J3?qoVEqFDr@IhhfRLG~zBzf<_Pum<{Rp0_hMc0?(q(@%Cj%EGrb z@o}D-ws_+h^=TW86HsrGc=FL!{S`m)==}G4KIpN>Zr@#H2b8{i$v zV7Wb_eI?QjI<2Ck+kFy1BeCuv1*02yO4H6KRD|PWhtJ}bHzL5z!eo$7#s&fl&&E|7dFh+I#6i&Hih^>d7fEvRD$4om1F!&@rbK#CH?)qXteJ3Oixs7f7N|toM;X!@n z-CAQ@y2<84v3M5#+YztFsaJ~aFZQ;vyJ>Cp1*i9#9XW{X%{wyRND1S+ihQNR!bk8! zRFdVtbK;n=U`YIn;*6>^CV?YFVOt-3&~x;7yNr$5D>|0a3} zI7*@xt7SFeT+@sXhjbLmS%!QP>GHsRZ!)!oV}hZ{>5g0QY7%yv`D(tIt-{@Y=W2vA zFX2FQa`O9w00~U#s}3|8@YEz_Kyuq#qF3L^uJ@!cxQ`e2N`A4%^11>Eh*E@!j_KCQ zumD)C`Re)af-fAml=o;l6^~Cn?(*asdtt70dQohn04i39Q)sRip{oCBUu@XoLzFN% zNAd`MYrJLM%i)OL8%Bi*A)df0HCp&za{%b%rOZ9cae*&a_pE%sX^r{HVT@@ks@RwO zWkB06La{^6y|xn(*{^ev(C*Igh5OV`zyb>cy#1xox5Ta zhFO%IMxQQvVkog|=yiPw+F86T$v&(PrAy*O2InWTJape@Q&wvnq27M7Mb8>WR>+?Q ze}kYxhhNvzz;cVvKESG;auiF&ylvkEi^Ge2FWuS}ebk_QX14HU50bgv9_G%bF!!f} zwwA&a{*^R)ne2}OqwmyKJ->|blB)lAvMvJGrGMCK!!kK1eo{J-mj)US-%@hBu zIHZ@zJ<6}FO&vyw-8XmR(?|D-Lpz;KqqZ*-RF4t@&C_eh&EA?RIi^0MgzapP-9%C(|VT3CU=D}Sc}3l znRiWLKYx)wiP|5V!G~~1zwF2UBODN^RU(+-%ZHsRB@R-&!nlvRlFj_52(kAsyN~PG=m#Rfiv+7By)gTqFMY_A?ZCR<5gG_CF~YzH`HL{1OvBiO^Wp zzrIBH9_H;YmfHc>FV3l+B}K6{;GNDZiG!F^`OZ&?cNbLj4X%oh{UH1}1vdK4B*8~b z_Q%TDw??igtzVrv_L#ibl|7X= z$cJh_zc9Ug$pk^t$Kxt&=&)C@6E*q-;X;1RkQfggn*14lJ(kE0z0@%y)$#1`NNaGd znU@7dPt(t3yY1W3!PH;etffHh=>L*+FY_YD-G+}$_G9FCx~ zM3>qGqop@CJh{8((qfOzzcNi{6Sct8s(9dzM*wiXZApyG4aT5`TOKuuJ|MktZS;Oa z6x{K5f3{067_(xEgH4{r!the*T~jA7{EzM<17(3N{##g^8noWZ6O>I8Qf-#lhg)LP z4Nn+r_PwX^PYMpWNDeW7(nOEzy;%y3r{H?}hX0p`8-%CfkCp$9bHbAdu9zRqYPj8D z^%;{@G`#ZfEbKFmz=_QDaP_D#9J*oooa#lk3P{iFV>j{bk> znt5;QgDYcgb9kjW(iGL^2lIL1_FD^MRa8l6esg1cpDqFpTM#rc_W`|* z`2G${D1e8DFW&W@kVlp3ol`Bz$`~Gh*~svl1*p(5vIvU4Bcxh1?!?kd0W;~|6k6?s zs^A$I8EgcCdirz+`INwvPJR1augTUqxvn;s;YO&Qe03{bI$?Jp;Pd5{YkN7M>M3; zoE$NKkOrsK6bDP(PQeXF&5W5gC#ZcESN)UcD3t36z8Gy%0%M=wte2vaQOCOM_RH!x z41Hh0)}?2J3L;lllUI_^;s^zcR9P~#Kea#kVJsT1v^|M1;BkX`=JTn`${}bNWoC29 zJqr3WFWffI34^Cxlg;a|e9$g#hl~7hH10}29U^__1jw0F@xrMP?BLWH-WRBYJkPGC zY@QFp{H%P*Z8vVhltS}E0C*n0_p0Tl8#GjyT+6%{i2p`->C5(~fSk(JJ@npwa9ilv zo_M(g8m>Eq)i_C!*srY zpZbHj(_@dga3B18kpj0px+jnHW(i7zGOq?7a_k@Fa6DO{f5-`bRd@{<`1wX|K zij7sBLC(&2ebaM!aDUL#{^Q4Fa0{#3`#4t*OC$9Ei(vD^AJon=ekRWNYVQYHyE1TCur_oeA&IC0q1@;7xT`s z!#}^hL#fIuq<(Uef0?ufsBW-zlf?{J-uFw@i=h(^bnx$TNuci6tI=?xd8eD3%z37D7C)I1NCSN4x zGgNkaf4fF{W?ssrW2qnnSqqdDOP>&C`34qieb))`XgzCI`P*dm(5ru67lsM;3z>3b zi?w7;U5tS_3=&eK%Qy8;cakpWU1HUY?~wd)Vl5KC?~!x=-1R8y8i*W$$;d9Ro5bG! z&K3d6F2Xw6er2fc33-`CJKOx#6+)-xE$uVcVKQWnS8aW0f~1lE`LX(5GpYEIESpTe zMFjM}u;WU-P408Epi+8ujYwerk<=Y`nKbHp%zB+c83PIHn5AMK?5DYIbMNCW%(;Gb zU;PhLn20FjGCwZ|&#XM&-sZ5yjA?_HiU+hn?Zqb(?FxU6NxRkUC0U$sraP$0 z>N5+3G0!tm4l2P1s_*t)Z@Hm&W?RE=`aK{MV{j{P)fA<>Nh94FIqci<`GnB24R9pf za+4O;LYWhtf?@6&@N4Z|VmrqH;3dxK^2l++lRM0xM@oc%-RN3Ro7PXF?(Kh`+2gF3 z9#GV5`A-G4w3|5RVrR)CzYYEhT1dkqVR56=TRQcrRGL+4?lm$oi`~z0LKW0!)C31@ zd7(m7efPp0DXi4hnbb;?#fwYg2N?P|F#6Lio{1OxFlKy6D4(4b%RcWV$ItFVo0k$Q zRrmJdj%}Ow(%-Pc6y5QhQzjc^msUnl{l`C~(ki*_5IqOXbL8j06s5y#^%tQ}GuB8m zz2NMAIaZW1d!M(vmjmr1#@;E4(x7+&fAnxJGZb!PXo|Du0MXUmlyH$6vhNw2bY*d2 zG-dj{r0aI@RAdtkWc^3%9=2P)`}+V=58ZjUdU%6ay5#b5t!Rai*)M8B=e!qef}haU ztL=q_&7a==*?Zx@zImYwdDh6GsSxIHMHFdG+?4VR*|GX+eVyFpge3u0cM@65QFwuniaaWW>JHnEef6M6$4;(WvMEwv z>5(l|$0Y(~llr=5&vpSr)j{?ZJ}La~NKVz|ITzS)D%ef#cLE{a$TzFI%z;U3@S@h5 zIexvSa(#6xzWew;w0!-lg*Qs&m~9)jaGc=KYFn=u-1lEOe(EPXFa=b3KBzUoV#O-2 zxJUz#K67TDq?j3EuYE6ZIFWnlnq zvQu~?Lm8PDzJ982Ge#QbfG2YY1wfCLCDO210e36$k1!|-!0@(A8_u4C;4au<&Hvm3 zB2#$dtD6b9s=FMZJ;MjX+#9|$tM2HOa$O|2-w=iD+$axA0S3-e8+5EN!gSFEh6~5F zak)L_jS#;&=6}CAG?{+{`rdge-!hg0XNiXztVtSZEV2N%=RIG^;el|Xw$NFkNJQt9YS2EMYZvivH z+=5$v&Tns(lusSTNUM7Wg-d!c|F<=I^?)u!jyBNvB`RazP~GmFQfp|Mj#=upjR0Y- z#|Z~cawBIwL!jZ@F%X+MeeJ_HNiY+>DE^j_eTyGHl}ThuBZIv7#@_8)`nM|!`G(dN zXkSEo+}(=nq(at!*j+SOoc+1&^;b1id~KKAdWaRj*XQb2P|<+>XMx!VTmGGyNsN|Lu9U!xe$Uu<_R~8o2v*P)dpjI2WVaGo}oQx6k{8aae z82-}BZ)Y)0%4cwZaH-st~|&PXjXbRAUspbm7T|@NEmbOkmWl;5@g-G2Gy) z`ji(hibk#BC!ajf!~nI7-nvz4*nG56Oj&9GG;eCfgFEyvq49EFYnUar-K(SLOdKV@ zLie7~5C?d+?db*8KufqY6?|T&gAVdy1^*r7;e-D?Li}8*89*qvT%avS9na{qIX2Wi zB`;iSr&Lgt0{VsnM0pz}#FsQHD2vL2LD7I*D6WHD$c?=~A+eSoom;&vlib>A)gYb7Md^f9_Hn{)3O7l>S2@f9b z=~$Z9Lq6esdxo~+{YN6@^3MuOxDXz5;Ncn_w(iii5)6$qYHFUQ8I6F>cyE`4F zRY1`bUN0l!si0kfiS6U`GT|pc-qD4(?KVK`1Dt@=WGFCdLEv7mvJKCP!4>G7>K*$>hE63wB&XxWT@(h2M=1RuE3nBIS8j3lf_o$2ddX(2VZ@!S7^+cg`;} zw3cY08vcu-ey)lvuZwJkqFLcZj_;wifIo!sOp^ER0VVX-xOINLp8+4pYpn<<3u9q_ z!%OQPS$N&T)jsbh3Dveu4U}t}#PhxK2X9^ALFJYk(soy-33~Vzr?lncTA$dNE^H_Z z6A$n97iWv$=f|A>ulQb&1Do#^o(77;ttnOIX$O9|%l62uW|$x25|Sm)21;T&rw6Aq z8yoKCaVpgx-j1h_a=WkXlY_G1%)Rqx_W{eZn`v(XcR}oOnBm&E3Vx52GHbK|OqH8{ zaw(x z7bDzVZ})HI(`w1aPgJPk5#|QLVtDjfIs^G{OLxr?{2aJxifock2g^IY6Wzvp2D58a zA=tG(t*#Q_)o7e=`__JiGgMwgaLHr7`N5=LF0820=E~N|wHqZu%0>0;rC`-{Qjv9z z4Icb?SMSib1GcrwGrnRxgc2zRO%nO~rSuIq|`{ln;Lr5+}$+Mk^t`ByGsUJ6A<->H_a;yG+0LR#1N6TKt>8Z;9$} z7ANV#o#7=;fA9|N94g#-x=K$ZII*H zPR?;JH#`(&7CjMag2Fa!IqwrS!TCvmJ2N%MR-Vupc55~Py|W)ooNlXw>kFZmKRDfy z!BDI;`@BA8kIwy*V-&{w=k5%h;_iWTOPdB9V-u1Tn zhz#`C^ED(1*yB4(V}?#oJ#5W)_^(3T2>&YlsM@cuhlY=ue-&7SU}r%|;W2hwm@Z#+ zme?%>s$Ry`t%0`SeaiHDv*H|iueupYFDX3MIUTvk=nN(-4^1;t6p&%c{|W6rJqUg! z;#tMvfD&D0d|w&lAem3MlHFDwyMhnzqmQ$ITPOF{l;1l7q1eSXMyC$DUnjjEkM=_Q z@!*ySE+YsH-O-_-;DM(+c`lWj_~0V@e~#uYTCgMHYY%U?CB~~ar+myegV9>Pmb3{4 z`1ktRPu+YekX{OxENMLk{hlqKj6G%G&1m1_&2Mu=7+c)7L4UU`{2+7oQnVUU7k6j7 z51Ao{^`E2rbi6RxuzI4;#RY2By>9$q3P2gIKYrd!S}?=u;2Y(x2sAF{KBW#aFk&$+ zy)V}qyFW$s1aI+3Ze<<84x;L4%>Q!eUgp+!J%sN1*R$hUj-l8DF;#FVv8_7Yq!0GT zjH!dm6ye>3)wi*IyvS&hceiNE2bdmZAS?aK3`(dvRvvGi^P-5}8F8Bs=>7LIiD=u> z0nY_>vtB4fi?4io$A`p0>-O}A=M38Tf+6{IqX;kX?33mlzN?O7JZHMwXC-kpjpRIW zRujESiq06mD8{%7ei7F1SKuk<=bC!GZs73z<$k;;67#{l?H0QTsum1h*rZheUg{qs zcenhTF+5i-DNg1gZC}`>{X7xSf4X~*1bqQ~X(~JKJyL{k-cwLrsjtF2A!p{?_DaC@ zD?)OL%0^K0X)aOZ%|7rjzQ=IF>@+gvc{aaiO2t;Kj4ADXI_Q&F+-`6r9FrOb3M(H+ z!YfJX;?}2;P?Vl*S5K7$OR`asnT0WMN^=*Fk$@S(-s|Y(8U`(GR81+>)+kpb`RJN* z9KNO6Uue$g0hb@vwl6dIA7*QbmSg0Wwb|3P}nU?ee?tk4s#J2cjlYde| ziQ@6-Uw^JCS|tFaH-$Ehz63+S;?;zyJqZ|j&etY>R|YII(Pu`rx=i3KPKKjoJu9ntngtHKM&a}Zd%h(?MH8`t3*}Gz;y{?kVQ42V6k|NC6 z-V2;%T~j(v?8S2R?mxeIG-3Sb=keuLZM@TS(>qO97K(e-lWCc);mfvZxl84?AaTX# zg`p=I1Zh!i)9oM^ zRrscR%>s5BY}}7ww16GMUt{RlWwC27^G|6BCA|Im-0zP8I#^|USnTV8LwLr;ch9gN z4{|z>JPFjdf;;LbMdTI@q4HFuq+Yuax-(6(1$SBE=rN5MSKA2uIDC*bb0`>(aAmVf zEEnU&awWP{T~j!;o6r}yyp`v6)(0Ew#A4`Cp7b?EQ&2lNeZBR$6JB>rW&X2Y9*uZ* z?wXOvBxwWw{^r)sBbpBHi2Cs+h0NsXNIYG5iU?j|S2EomOZtTgQT|r4CM8uhv%8}F zNSE+;Pu=bxBNweD&V~1;5PS`mvU%Go$u^bQ>Wmv!qB1!(Jvah%U5m8wep*xm93XHvGFxO5YkA7ZvUo$Es zPP_j%`E106IKHQNw!24wEN?#h-KQmj>PG5M zb5^VpY$utwjcbL#I}wvG??z{ss(LFy&utGcwpFKBJk>%${q}Rk#hOSv@oE3;uN@e6 zcy#^=!Hk(}Hx5XO^5H#-*Fpd8Yd~V;PxBmIX5fBWp!8Ch4_XHHJ`cLDf$K4z*Fz_? z;Kz6zBSo<$P`Czh{8-%qF0T1fl-eOk?ZDTU-sp_#y>b_fx$UveQ0Gs%yfK#SH0T-| z86!RyJ>iDc$nKyxs4)>SY zHE!PoRGw9J8eZBCC+rJ$Zr?42J=8SjzAukrELWV%aIFS}M;zz1zA1@|>J#*Ij6Vp? z>Ub$_Rx4bq(Dm%uVFrxECr#;LR>1v^RHK{+Vf9XKUbrMTxYP3Y7=Ani8^Y9$KDt{t zd4_{i>8LPlkMz5E?;p;KoV&mTiSQ>m&S?N*R}f$_*_lO+W5^V{)V zI0h5Foe|h&g*&<&r;i(J;_Uq#rIdDI49WC3w&#mD(tLS7W-NRVw>zr_onGR`{(s8- zF8>bUjQrKsu3c7;vvBI9;+iRXe7LhB{FfCc=!G*qiZt*xucw)2xFk%-MRPRYSs-T} zA$zEQJF@fKmkr(GF*|%Kq-Um&0@d}y+(hO8X0-da;)Ek+KHP z_e}2Yos`8#18h^f)1|$}%$KKLgrZ1$E?TJE`$BqmP)pFD^3_ zRz*Cl%`wTcxl50cXVw7aak8c4qV z=ree-npDbBI{A09hxoJF&z4|SLp+xB*!QEnhWK7dbNp247H<7}M!(apj+`Td8EK7b z$xLgvh1+?vHsLf_V1Ts!eS_cb3>_?_%UMb zn>pD`2p+UA?amJ%a^ALbHAGz}lA}HTGTt2{Z+@rP%h`XM?6bMq$ryBlO#JoOZ$0!r z`CC6kqvONYeD43-UjY&DWq5p_@iXFz`0gpM^llGLA_>in zD4W~|w+US?t}4yKn`C7a`mlGqkaCBsR*l;)lj&NL>p>3fq+t3|{IcaRd3t+VZ;#<5 z!67xwf0ywoncaVANGAO*@kP*i@x9mtSs53%{ijO@nMqx{DKz_#kf*fc5R+*k7C2Yk zryB1P%I$CexW6AI?7KXbVe!g-YszFo z{#(9B!OR^(H`vH^DQ=vwdlvMX+W!gJdDdjP?#DP8DdeUXpqWd4^LqbXsiKc8OSSbn zz}rDyePNbrs_0I-iSK!S$H(^F7t1udhXAf7~TP{aWvaPr_N^ z$y~O{+&~LaXJf?2-PlT|9qeyt%y~)jsg`7{4-Av*uVf`EXljTT@6H~I`Aib|mt@Qg zI;IJp`bp{Z?GMR}UAu)FI_iiCU7<&3KlKoti+WveR(lEGdc9}li(AC#g?cnO`_!#l#-KO2Ggy-b7rP?m9 zeF0nk!9C@2ypgCi98J^qF$J^IUOyjvc^X(T*sZc0w%NPmS+=i$sjPgxL*Vk?TR(OCq`_|6XiPqWeRfV zy}y{T91L?*uMfZHi@}RSjnW)9d~lxajBCFAMFUVSsOe?-w{D8YzV>)A=jN%jo{Mu-nU}+L@+qWi-}v_20PzJSRG|O0VgSS z*y9!pz_D?THJz^*+anV9T>0*TNx8#gJEL5XNBW6M@HS7Rp6`1TNtq3$>68UOQZlhJ zXTs{^Xes_*3FrNe<@?6*WbaT2Wy{Q_jOR46_k8S`UA71zA)*LHG8;(BNTTwbMMz0$ zY1p#Mh>w~2KF9I>>HY)m`?#<3dY`ZNt4=a@@`JG+Y`(Y897~VGt{&CLeRa{G%rMNs z=5rPU#`&Izba>;N!=bLkIRgA#2$_gAc{w+acQL4#Z=)Ceg`rD1^+rPJR#sC4D>{syTJEI^2J8S`JA-uRMc6vJEL+g4%K};#9kAwqNeFeRPzV&`6$bz zdXN16T+eVnyOkb{U5qm2rs2+T(qu1nfD{)lzSDZ~=tVe=$?IfBC|bZuVQ@QFcO+`o zl<2Ez$D!l~iF?noD>#pJ?2)!{h6^XUovD90f@|MA|L=GY&{6iED4{(L%hYP-UaQuq z|HVaEIm#D9R*DxS6x6{@_~d6%GT%%^a;iDdLxZQb>@L)hpNu6$B~I8N=q(Fu9%5sI zv3H#TaYkPWfgT~OkElKq;MA_H2X36O^W-5rezh_u=(dpGyc>;5;2q3vh>Vn(yC9BqZl_0JloB_S-wRBbb_)MLSTcG zNeJ*LZ*CyD=#wl~pOq8BoUa-(S5%VR=`_#fQ@tnnis^)BnHLf$6voounqDJq+fH0P z6!VU7?zy5&jqFuIY=sDGk7NhoPLl_NL+A^Ve)KizhQU_SBHzNBljfzQog+_XRvCK; zDxy#Qf$)ehoTOS4lUPG&n>c1aqSH&NE@OXc+>u4lz4cqEL7;;~I65eB)b|~U^M~i3 zFI~-~_yo<4)&muUE2Yb3ixH0qm5*yRu7_VEgtAs#>|84&tVueG%?y4e4Xt`gX(gjq=VG@WAUQ?Ok_noJjFhi5F)A!&%jNhL+vHGZFG@pq3uZ zf}@t6N(y3t#>3+Fmz-dA!(wju@+g6LrKsP5f&nk>dy=kVHB0ERzOr-CS`3+I^Ipsr zD8t5&JP}nPcId6Ds5Jg8fNb^07)Gl2@L4v!cIkNuNU0Yrh?}E>|IY*KgwH$Op*##k{hrPeVd0B!ND2y;Z4MlGm)4NUp#hXnyhdZTR(R0=rI2wRAc)c7*ai zydud3EqTLBGl494%=^~T!Z4aB_8oKua! z#iybbTeZHx9~m2*?5Bg_pZt|m`@G=B4EtOGtL7P@4BQqTH1ai4! z(-UBAwVumnJq@P*cy=&eBm7tt<9!lx^5=q&--`8T9hI~ zyU_ug&KLOSJ;gy*4M!NI|CQtvrUTz|A8Xj!>EfB#KR8|<3is$+H`J6;p96Fn>VsBd>2UC^FP!r?z<@Y1b_E{(IDr!Y!ad4EBOAnp2urVf&=Nb zC1<8KkK=;2+e^!%N3kjMEmRHzO62fepC2@bp5zx5(XLiFH(Yf-hB*MGV(xxrYH&x} zd@q&Yukje(7dWT)EE=jaD=Z%VJP+&Z4e>4Mp7<0--o*u50I%V_lLgfdnEiT_$jah? zq;4uXc@BLLY7Y1@-kSnvo#dYhKaT@Gro~UcV?)8X-B~hF?;1I`{2WdjCV`s(>Y4{-l^!SVo|IBIfcztMc*kAG`}|FQVX;l`z7!PmZNBe9_& z=F^%9oaBhls2?^)1*NQDJxNa-2{KBJX?DSmwk2yyxg?BE*)Zh<*-423b<1<_Ovn}97*;Wl+BT0$iNVI zpKt&-Mzil13nAxmX_d`Gmuel*XsP&a?CG;mXX@#*DVPVhAM9S9&9%nZ{i8SR4jjdw z6w+LmsHKrA&A2haS`YEaa#jcv0q=hvI_u%BjF)dQ8}RM-fbzpCeA}_v&@}JMGW^XQ zBT`6=#sqT=7-G9p@ai~JH-3Ay5*P)iek3H={T78Bp`M?9#`<9XIZNT_V;9K35cohm z@E8odTZ|3fQUiUR1hKC{rno@qHNWx452+3BewkPwC!8?M_H$?P1@5Z@+Z9eW5V=|~ zG+3z))3<+~9y_Ojxx6E(tRpG-`hhe}*q0cni)vqQjf(~*(;exDQ>y5z6Ejpr?iWP0LbBGR2Pa>+PZGq11ASF*Y8%nA#EI*(XFGNNIJZ!IK_S%GmOo8LeP+lAk7(N*{8(Tf#K{ zoIq>!si@fw2N>;K&o@}~10f!Zf{%+f;LiSz%7XlVcdO``G*&mn4P_rSwX{^MopLQx ztvC%SLQ>0L^djN3g?tv%dj(`Vr+(S)l@)GrC|mOv*=&e?Os46P|X z$^`A`K`dLFLSGP>o4vOdp53d1=_}rXODF_SINRnABxj@fM}db$bop>2+C%GsNjc;U z&W7z_4~5!-56A89Mu4Ll5MK43M*lB)tAD24$sErju^>kpTDR^7m2qgGjN~?(VrxER z1Q~Vbr=LROIfD4(t#xvK=$uV=C5-4B2=?beyI-;#jR>9VP_ zM>q!Ci5B)eNCI9tq$6I9!Wb`OsgG~NuvV~BZ?At6CcNI$*Y?yNK{#8=?{x@tJgWR- z80d+u_vddMlC{R%(^8X{O;bQkkVSR-n;QzHR>URD+k;CS;Z?t6GW5<>x=+TY{THho2?N3nfCfc*3w~&N(=9 zJT>I2qXnj$Ke=@GP(0f3_%4wCg+PMmzTBr)X`uP$ZKJV)7rv+nzs2?48ihtiUiM8N z!@P5Pomy^YD5sd*F7{R)&qwYLf9h@qZil6lq{HoDuw!wfyGRS_=vywVar)qmqAR?L zHSS32SU#?I)EW<3KHI)138D1s@#lS_@ilzpVH|U>?Fc8fv z1f4S(NiKT25H)J(tm0z{90KeE`Cn|{%{VX9?7A?7HEJw{TM)1+wEdIkYcJILzW1}o znmd&I^APk;Jp#tMdWK_OHn6UcP*7y(25EhX?>3`dF*MR~^BdXg#O2@9WJh-vXSDN% zONe10-o^CGGCKuw1?l=m&+6m!>&vrl`GUB$Oe>VDR5wn7+#3q|!T8u4-UVNgQH`X)P=PsjmfRRkeVPK^~H$F)pQYRJ#t_5o%fQSK16e<8a{Yp1t$jBmamPtp@O@yP4u=Soaryt zGJa_W161NM@5p^f{#;k{*-{@|1c~##Lps2G>1pG^PJi55y&5Yk=#3-J{etwqmZ%&= zxku`tFCJ4Lm$0z42GmaHU$wM?*qe2hG$Dd`zSlTAiq8N z_r=U_>}TeW#3AR`v42W?ZQz2Iw5djj9l4LkD0E_&1BDzYR7Yt?fl2O+G*9KXglGy6B-xeo=%RzXvkg zWlHqEChx;6m4DTl9Rq5nLZ7=53P@SJoYL02A9D6*saj6zqx!uC_ZG7M>!qPlQGyLO zn(pdKT$3>X5jv_m;w3rgv!cFBc}^Uxv#hzM4=Vx3clI@1QFZ8UeQcX1K=$5*$vba} z=tAoxgRVpp5mUDHr0E6Juu;ZNEt>43uzdFD5NnAp6kDAio*~oQR+DdzrB*xP=ZrCb zXEQPE2)TIv!AU-x%Du~Os^x@(`reAfCP0tMuW{jB0(fvGkFIFL7vHE&>d8zvqyLJs zEs??jo!F|x-s$@R^Mf6WH(rn{)gtc_d&zodxz_-6yWq^y}D-r5u?wqkb&^XuKbW>uGKwR6`Cw0>l(*rDKb}ZD8$@-v~tD!DN zUw?l5qAV+JM13Ah*kQ(OxvjQOyLX|B!8BceiVf`jE|N2QYm?_9ee&`sjDGo@%OFsbDCiCB`IaYIxUoy4Sv7xo)bhcii%SZ_rrzT zCZ{UatZ<$w_#;Pv9Z0lBQJ?$I7MaJ;itDox^miKNrV<>m<6cepYVQVVEYkQ-ahegx zJNGJAMN8t-w%LgIY9xn`0s zaQMKdn+BGuU>BMvE=xlR9rm*M471LdXURz6&Z9#119AB_xrky)% zI|jcTMSMD_4?t&}?m@0gt~h#nut#%31@gAf=$Vs!LgTu$)#EoTu^?RDCS%Y9(lbAu zZ!}j%b*<6wSNe~Do1E-_attQ$Mv(V=>ki> zf*-AqaG7)(X&ki7>PD&+-Vcw)+%_ zxT?Z z98395jPOL9zP{Y<1L;}cHHk;^68K}^1d;Z-DEgYtN0lZr;9{XyVIi9^))r(2-fCh- zkvu>D9;cH=Un(a+(Wv3`KIQMA187p4O6=-kc_ajdLz{B5M4io5;_M|Y^JVL5X6Xu6(5nz=-R)PNZjZqR&Ka3}jl z0+inWZ71g;w-To3*aE?IJm6&D&Ka=t=6Zag=^SXkVOu3JoPei1e0mlUNx(i;d@nXJ z8P8EETh~8OgR?EpKMOh%P=!NfQ$ZsZ3LWXJ>kq_W#8jfXd1V8^RqF1kM^R4D*Cjcz z_$UPwhpfF>noUr0x$x;kmo3Vu)jhaUYJt>I3BAn`QlP^0nyqu?0^Tne*Z0o84X!RR zg`aRQaC0*43Ym7riy(fpRL2K4t5+@M&*y_;w56P4zdKS)cDV~k9L7p6?xAIN9c<3} zsKD57j`|bMWgW%|@P0Q9Po~c`-1qsn(upgc(A8C+;Vf+iBXS))J1)9#CU>Fb(FYgM z_*4+!UZoEYQ?z$cPl!O^iHY}q%GGdVWqa-R_;XC1k4}8PCxhH83~A9T1hm=bqjvj? z5pGk;>vdFG!QF+a>e)Oi_{e`Qe@K*^S9Oo3vV=*)`MP&%5pHC^gkr+4A_^jyy)z$k zmKH+Uw3D+p`LgigF^lZXmTah*KSx*?3kMpG%lFwfC7^k*iuIC@Gxnd(<#d^Fh9-(j z8q9N{;K*kFs46uV8VzD4(_*4Az->~UG#r4yb#Op*B^neieR*EOeG&s+t~yu#qy}GV zX-}&(wbSOyIM^zqfXY`Au6uZ2WV=3|isO zx|=0ela8RJuXpXmldd2q)wO4_(gtW+l+ugE$+^!Xx8DYW+2}+p=8j3v0-;_@tqWql z5Vd&aNrI;?o;cgf+8>Kj=hf=L{(Jo4KAhHImD+TEr9Ky;7{0#grziKvx-V3v zUk$;sv_XBTjCisyHR!P}IUljT{Wg^~)EMZmNIi4^<^mV5xxMV%u>=aL1Dfu4kAWe* z@TTmNB$WCUnpQ6|!V2Ht%(EhTsM8n~u3K>kAF5xBuSw*=Woe4YmbbcO?z)aex>W|F zcVCtAs}e%b?U}*04PDUdVm|6sCJgR;8D_NwBGBzT*i2MDhV$JIUz#5m##YY#F00)l z_@rX9rTe5V)Ys2c&K{G2;|oy|5-CDZE;t>%bMF}J_$isQ5)Oe}ZO}`j1s?FudZK4I ze++y4DIWbzl)@W-Cf-RLV#W^#i`U;*T_>@~`4-aGuaiiVL|zrwhlCBG?}2qP>x2@v z?xnZMtE3=elyx+5ne;1ZvQ|y*6~V3RvZQ?3GCB47z?RTZPH3gBFQ2w5Ca{m1mC{)K zBK>3kPcUTn7O7{Md+>_?DB+6)d(~~4--MKVI(*a%+l0VjV&a|0!-SK}<*P>57YVN| zpB7c*JRykmbSik)&Xc$W*ap~GRtfiugu6qe|08USc)LhXjT7ic53V-^{w1_GK$K%l z17S}w>!613arh){N2%^X_Sn$I3U-ls0@2XTw$4T~WMoTh_MfOYng4T0h@H>lRQf$Kp4>iAW@Y!~N7`dM(jxQ;|*5ky}>&x!o zc_W#^M9T^byVPSW^{wDl+a-ymW(RV=)Nj1bPY-(AnwQ)Dcw^EQ@!(NATR44WwpT*S z4q3Opgu45hfN6B4f(n%b-Yi%*H~wS~7cAOeJJsmn&d54_XBZJoJ;v{@@7Uu!Ygq5N z$01a%{f{!F+6@C0ss1@QS-{f~->z{gTL`JW82sLsypQYX`zhY82mB@jxY+HD^0UHE zAFo=$;VQp{kr$3gJ;ST;NkayG)0-DWiJs`XtQpniY5_c7vy60?txzU7S>rdE+h}`z K#9DMp7yk#F+7>zh literal 0 HcmV?d00001 diff --git a/examples/spin/data_reformat/data_2/set.000/force_mag.npy b/examples/spin/data_reformat/data_2/set.000/force_mag.npy new file mode 100644 index 0000000000000000000000000000000000000000..14b73ffb54c66edbd1b4a179dd7b885bfd5fe566 GIT binary patch literal 23936 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 dict: type_one_side, excluded_types, precision, + env_protection, ) = self.param return { "sel": [9, 10], @@ -59,6 +61,7 @@ def data(self) -> dict: "resnet_dt": resnet_dt, "type_one_side": type_one_side, "exclude_types": excluded_types, + "env_protection": env_protection, "precision": precision, "seed": 1145141919810, } @@ -70,6 +73,7 @@ def skip_pt(self) -> bool: type_one_side, excluded_types, precision, + env_protection, ) = self.param return CommonTest.skip_pt @@ -80,9 +84,21 @@ def skip_dp(self) -> bool: type_one_side, excluded_types, precision, + env_protection, ) = self.param return CommonTest.skip_dp + @property + def skip_tf(self) -> bool: + ( + resnet_dt, + type_one_side, + excluded_types, + precision, + env_protection, + ) = self.param + return env_protection != 0.0 + tf_class = DescrptSeATF dp_class = DescrptSeADP pt_class = DescrptSeAPT @@ -127,6 +143,7 @@ def setUp(self): type_one_side, excluded_types, precision, + env_protection, ) = self.param if not type_one_side: idx = np.argsort(self.atype) @@ -172,6 +189,7 @@ def rtol(self) -> float: type_one_side, excluded_types, precision, + env_protection, ) = self.param if precision == "float64": return 1e-10 @@ -188,6 +206,7 @@ def atol(self) -> float: type_one_side, excluded_types, precision, + env_protection, ) = self.param if precision == "float64": return 1e-10 diff --git a/source/tests/pt/NiO/data/data_0/set.000/box.npy b/source/tests/pt/NiO/data/data_0/set.000/box.npy new file mode 100644 index 0000000000000000000000000000000000000000..1f72eb7185497167688c573cd800c4962932eca2 GIT binary patch literal 4448 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$d2099snmP)#3giN=-9J?f*y>aq#DCuDyv7@_Kj2RHyNH>F`%ka;_$(`= z=%8wS$4hpNn1ieKwtHtotnEbtckbc`iqG>{nYm#)i$mF|i4DcZA`YYGjE2u>`Wej^ qqvghEc{o~MjMk^4?S#>G)M)!~wEaBVFBt6?jP?sg`vn8lF8}}r5Jfux literal 0 HcmV?d00001 diff --git a/source/tests/pt/NiO/data/data_0/set.000/coord.npy b/source/tests/pt/NiO/data/data_0/set.000/coord.npy new file mode 100644 index 0000000000000000000000000000000000000000..4b60ae0e0bb627d0fd55de43e9e6fb0ff0d79ad3 GIT binary patch literal 46208 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 literal 0 HcmV?d00001 diff --git a/source/tests/pt/NiO/data/data_0/set.000/energy.npy b/source/tests/pt/NiO/data/data_0/set.000/energy.npy new file mode 100644 index 0000000000000000000000000000000000000000..8754b6dad25e9c00588bb4fb0f1eec06cf10e043 GIT binary patch literal 608 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlV+i=qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$d20EHL3bhL411@nt-EA%>Qx2SwuTW$H(sxe0TxkoW8L#;-VK|j?U|!$4 zd|@CzW1{}di$J=!G@|aziIf9vPuH!C0rEfkRlJY~(kA~(L-zv3?JvwPCHiB3Z#x2{s}pjL@d4=_-~1av_86ANKYs~SKcmLJyb8#Fc>U?~6F~mL zujj*O0{QHcAB|#xbWF6U$$Fr;*CD}YeL()J+u~g2Ksv2+t%?(np3O9K?o%LbC;8e0 zWPg*7ie~pSO1d`KOkoYM2A*1O<)>3xVPq z;Vh0bfV6LbT%j{a-SF^mycS~t+Z3l`Unju?$1jtWjZB1GZ zcLBv+cM2Em0Mbj+9qv5`($=?EuV4Yn*91;_U<{;h+o&tt0*W`! z_>t%X`?!h8Ax}kN93CU#bq+SL~a4npLF)}g2LZFufYx^ a-up=z4zXGXC+BgMx?0MK}A+?nWdyc%4o<) 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