diff --git a/deepmd/pt/cxx_op.py b/deepmd/pt/cxx_op.py index 1106a8887f..fe0e78a0ce 100644 --- a/deepmd/pt/cxx_op.py +++ b/deepmd/pt/cxx_op.py @@ -42,6 +42,16 @@ def load_library(module_name: str) -> bool: module_file = (SHARED_LIB_DIR / (prefix + module_name)).with_suffix(ext).resolve() if module_file.is_file(): + # Skip if this library was already loaded by torch.ops.load_library. + if str(module_file) in torch.ops.loaded_libraries: + return True + # Skip if ops were already registered via C++ shared-library linkage + # (e.g. LAMMPS plugin links libdeepmd_op_pt.so at the C++ level). + # TORCH_LIBRARY(deepmd, m) in print_summary.cc registers "enable_mpi" + # as the first op; if it's accessible, the library is already loaded. + # Calling torch.ops.load_library again would abort() the process. + if hasattr(torch.ops, "deepmd") and hasattr(torch.ops.deepmd, "enable_mpi"): + return True try: torch.ops.load_library(module_file) except OSError as e: diff --git a/deepmd/pt_expt/infer/deep_eval.py b/deepmd/pt_expt/infer/deep_eval.py index 4eedb723d0..290f2ec923 100644 --- a/deepmd/pt_expt/infer/deep_eval.py +++ b/deepmd/pt_expt/infer/deep_eval.py @@ -131,12 +131,18 @@ def _init_from_model_json(self, model_json_str: str) -> None: def _load_pte(self, model_file: str) -> None: """Load a .pte (torch.export) model file.""" - extra_files = {"model.json": "", "model_def_script.json": ""} + extra_files = { + "model.json": "", + "model_def_script.json": "", + "metadata.json": "", + } exported = torch.export.load(model_file, extra_files=extra_files) self.exported_module = exported.module() self._init_from_model_json(extra_files["model.json"]) mds = extra_files["model_def_script.json"] self._model_def_script = json.loads(mds) if mds else {} + md = extra_files["metadata.json"] + self.metadata = json.loads(md) if md else {} def _load_pt2(self, model_file: str) -> None: """Load a .pt2 (AOTInductor) model file.""" @@ -157,9 +163,13 @@ def _load_pt2(self, model_file: str) -> None: mds = "" if "extra/model_def_script.json" in names: mds = zf.read("extra/model_def_script.json").decode("utf-8") + md = "" + if "extra/metadata.json" in names: + md = zf.read("extra/metadata.json").decode("utf-8") self._init_from_model_json(model_json_str) self._model_def_script = json.loads(mds) if mds else {} + self.metadata = json.loads(md) if md else {} # Load the AOTInductor model package (.pt2 ZIP archive). # Uses torch._inductor.aoti_load_package (private API, stable since PyTorch 2.6). @@ -612,6 +622,22 @@ def _eval_model( dtype=torch.float64, device=DEVICE, ) + elif self._is_pt2 and self.get_dim_fparam() > 0: + # .pt2 models are compiled with fparam as a required input. + # When the user omits fparam, fill with default values from metadata. + default_fp = self.metadata.get("default_fparam") + if default_fp is not None: + fparam_t = ( + torch.tensor(default_fp, dtype=torch.float64, device=DEVICE) + .unsqueeze(0) + .expand(nframes, -1) + .contiguous() + ) + else: + raise ValueError( + f"fparam is required for this model (dim_fparam={self.get_dim_fparam()}) " + "but was not provided, and no default_fparam is stored in the model." + ) else: fparam_t = None diff --git a/source/api_c/tests/test_deeppot_a_fparam_aparam_ptexpt.cc b/source/api_c/tests/test_deeppot_a_fparam_aparam_ptexpt.cc index 6db015d68c..9b5b086092 100644 --- a/source/api_c/tests/test_deeppot_a_fparam_aparam_ptexpt.cc +++ b/source/api_c/tests/test_deeppot_a_fparam_aparam_ptexpt.cc @@ -23,48 +23,49 @@ class TestInferDeepPotAFParamAParamPtExptC : public ::testing::Test { float aparamf[6] = {0.25852028, 0.25852028, 0.25852028, 0.25852028, 0.25852028, 0.25852028}; // Same reference values as test_deeppot_a_fparam_aparam_ptexpt.cc (C++ API) + // Generated from pre-committed fparam_aparam_default.pth std::vector expected_e = { - 1.596836265688982293e-01, 1.596933624175455035e-01, - 1.596859462832928844e-01, 1.596779837732069107e-01, - 1.596776702807257142e-01, 1.596869048501883825e-01}; + -1.038271223729636539e-01, -7.285433579124989123e-02, + -9.467600492266425860e-02, -1.467050207422957442e-01, + -7.660561676973243195e-02, -7.277296000253175023e-02}; std::vector expected_f = { - 1.112134318320094352e-05, 1.085230789880272100e-04, - 9.298442641358874074e-07, 1.491517597320257530e-04, - -1.250419527572717225e-05, -9.768265174690741028e-05, - -5.021052645725073397e-05, -9.741678916887848341e-05, - 9.375317764392495572e-05, 8.664103999852425394e-05, - -4.538513400016661465e-05, 8.561605116672722879e-05, - -2.454811055475981441e-05, 1.079491454988312104e-04, - -1.656974003674590440e-04, -1.721555059017403750e-04, - -6.116610604208616705e-05, 8.308097903957835525e-05}; + 6.622266941151369601e-02, 5.278739714221529489e-02, + 2.265728009692277028e-02, -2.606048291367509331e-02, + -4.538812303131847109e-02, 1.058247419681241676e-02, + 1.679392617013223121e-01, -2.257826240741929533e-03, + -4.490146347357203138e-02, -1.148364179422036724e-01, + -1.169790528013799069e-02, 6.140403441496700837e-02, + -8.078778123309421355e-02, -5.838879041789352825e-02, + 6.773641084621376263e-02, -1.247724902386305318e-02, + 6.494524782787665373e-02, -1.174787360813439457e-01}; std::vector expected_v = { - -1.264062189119726106e-04, -1.636544077308309524e-05, - 4.453224130911556590e-05, -7.947403699519458518e-06, - -4.603504987332719071e-05, 9.491045850088816000e-06, - 4.131028921467394082e-05, 9.691472468201876704e-06, - -3.323572704427471520e-05, -1.024556912293147473e-04, - 5.530809120954559223e-06, 5.211030391191971272e-05, - -3.851138686809524851e-06, 2.101414374153427902e-07, - 3.247573516972862344e-06, 4.561253716254950400e-05, - -3.865680092083280590e-06, -3.262252150841829144e-05, - -1.166788692566841262e-04, -1.814499890570940753e-05, - 2.155064011880968559e-05, -1.629918981392229854e-05, - -3.245631268444005592e-05, 2.968538417601228937e-05, - 2.463149007223104176e-05, 3.660689861518736949e-05, - -3.586518711234946879e-05, -1.424206401855391917e-04, - -1.017840928263488617e-05, 1.421307534994552908e-05, - -8.618294024757048886e-06, -2.192409332705383732e-05, - 3.461715847634955364e-05, 1.277625693457703254e-05, - 3.486479415793123331e-05, -5.604161168847292539e-05, - -8.612844407008925294e-05, 2.508361660152536905e-06, - -1.633895954532816838e-07, 1.903591783622687189e-06, - -3.028341203071209831e-05, 4.685511271783763848e-05, - 2.876824509984768127e-06, 4.576515617130315025e-05, - -7.108738780331672730e-05, -1.062354815105974551e-04, - -2.954644717832223544e-05, 4.075640001084848793e-05, - -3.138369091725678469e-05, -1.316088004849699161e-05, - 1.786389692843508954e-05, 4.579187321116929540e-05, - 1.869753034515593833e-05, -2.550749273395911822e-05}; + -1.589185601903579381e-01, 2.586167090689088510e-03, + -1.575150812458056548e-04, -1.855360549216640564e-02, + 1.949822308966445150e-02, -1.006552178977542650e-02, + 3.177030388421490936e-02, 1.714350280402104215e-03, + -1.290389705296313833e-03, -8.553511587973079699e-02, + -5.654638208496251539e-03, -1.286955066237439882e-02, + 2.464156699303176462e-02, -2.398203243424212178e-02, + -1.957110698882909630e-02, 2.233493653505165544e-02, + 6.107843889444162372e-03, 1.707076397717688723e-03, + -1.653994136896924094e-01, 3.894358809712639147e-02, + -2.169596032233910010e-02, 6.819702786556020371e-03, + -5.018240707559744503e-03, 2.640663592968431426e-03, + -1.985295554050418160e-03, -3.638422207618969423e-02, + 2.342932709960221863e-02, -8.501331666888653493e-02, + -2.181253119706856591e-03, 4.311299629418858387e-03, + -1.910329576491436726e-03, -1.808810428459609043e-03, + -1.540075460017477360e-03, -1.173703527688202929e-02, + -2.596307050960845741e-03, 6.705026635782097323e-03, + -9.038454847872562370e-02, 3.011717694088476838e-02, + -5.083053967307901710e-02, -2.951212926932282599e-03, + 2.342446057919112673e-02, -4.091208178777860222e-02, + -1.648470670751139844e-02, -2.872262362355524484e-02, + 4.763925761561256522e-02, -8.300037376164930147e-02, + 1.020429200603871836e-03, -1.026734257188876599e-03, + 5.678534821710372327e-02, 1.273635858276599142e-02, + -1.530143401888291177e-02, -1.061672032476311256e-01, + -2.486859787145567074e-02, 2.875323543588798395e-02}; int natoms; double expected_tot_e; std::vector expected_tot_v; diff --git a/source/api_cc/src/DeepPotPTExpt.cc b/source/api_cc/src/DeepPotPTExpt.cc index 31f8b41453..b30405ba55 100644 --- a/source/api_cc/src/DeepPotPTExpt.cc +++ b/source/api_cc/src/DeepPotPTExpt.cc @@ -1008,6 +1008,15 @@ void DeepPotPTExpt::compute(ENERGYVTYPE& ener, min_z = std::min(min_z, coord_d[ii * 3 + 2]); max_z = std::max(max_z, coord_d[ii * 3 + 2]); } + // Shift coords so minimum is at rcut (ensures all atoms are in [0, L)) + double shift_x = rcut - min_x; + double shift_y = rcut - min_y; + double shift_z = rcut - min_z; + for (int ii = 0; ii < natoms; ++ii) { + coord_d[ii * 3 + 0] += shift_x; + coord_d[ii * 3 + 1] += shift_y; + coord_d[ii * 3 + 2] += shift_z; + } box_d.resize(9, 0.0); box_d[0] = (max_x - min_x) + 2.0 * rcut; box_d[4] = (max_y - min_y) + 2.0 * rcut; diff --git a/source/api_cc/tests/test_deeppot_a_fparam_aparam_nframes_ptexpt.cc b/source/api_cc/tests/test_deeppot_a_fparam_aparam_nframes_ptexpt.cc index 9b6e26d637..b6da8a37a7 100644 --- a/source/api_cc/tests/test_deeppot_a_fparam_aparam_nframes_ptexpt.cc +++ b/source/api_cc/tests/test_deeppot_a_fparam_aparam_nframes_ptexpt.cc @@ -28,86 +28,86 @@ class TestInferDeepPotAFparamAparamNFramesPtExpt : public ::testing::Test { 0.25852028, 0.25852028, 0.25852028, 0.25852028, 0.25852028, 0.25852028}; // Same reference values as single-frame, duplicated for 2 frames std::vector expected_e = { - 1.596836265688982293e-01, 1.596933624175455035e-01, - 1.596859462832928844e-01, 1.596779837732069107e-01, - 1.596776702807257142e-01, 1.596869048501883825e-01, - 1.596836265688982293e-01, 1.596933624175455035e-01, - 1.596859462832928844e-01, 1.596779837732069107e-01, - 1.596776702807257142e-01, 1.596869048501883825e-01}; + -1.038271223729636539e-01, -7.285433579124989123e-02, + -9.467600492266425860e-02, -1.467050207422957442e-01, + -7.660561676973243195e-02, -7.277296000253175023e-02, + -1.038271223729636539e-01, -7.285433579124989123e-02, + -9.467600492266425860e-02, -1.467050207422957442e-01, + -7.660561676973243195e-02, -7.277296000253175023e-02}; std::vector expected_f = { - 1.112134318320094352e-05, 1.085230789880272100e-04, - 9.298442641358874074e-07, 1.491517597320257530e-04, - -1.250419527572717225e-05, -9.768265174690741028e-05, - -5.021052645725073397e-05, -9.741678916887848341e-05, - 9.375317764392495572e-05, 8.664103999852425394e-05, - -4.538513400016661465e-05, 8.561605116672722879e-05, - -2.454811055475981441e-05, 1.079491454988312104e-04, - -1.656974003674590440e-04, -1.721555059017403750e-04, - -6.116610604208616705e-05, 8.308097903957835525e-05, - 1.112134318320094352e-05, 1.085230789880272100e-04, - 9.298442641358874074e-07, 1.491517597320257530e-04, - -1.250419527572717225e-05, -9.768265174690741028e-05, - -5.021052645725073397e-05, -9.741678916887848341e-05, - 9.375317764392495572e-05, 8.664103999852425394e-05, - -4.538513400016661465e-05, 8.561605116672722879e-05, - -2.454811055475981441e-05, 1.079491454988312104e-04, - -1.656974003674590440e-04, -1.721555059017403750e-04, - -6.116610604208616705e-05, 8.308097903957835525e-05}; + 6.622266941151369601e-02, 5.278739714221529489e-02, + 2.265728009692277028e-02, -2.606048291367509331e-02, + -4.538812303131847109e-02, 1.058247419681241676e-02, + 1.679392617013223121e-01, -2.257826240741929533e-03, + -4.490146347357203138e-02, -1.148364179422036724e-01, + -1.169790528013799069e-02, 6.140403441496700837e-02, + -8.078778123309421355e-02, -5.838879041789352825e-02, + 6.773641084621376263e-02, -1.247724902386305318e-02, + 6.494524782787665373e-02, -1.174787360813439457e-01, + 6.622266941151369601e-02, 5.278739714221529489e-02, + 2.265728009692277028e-02, -2.606048291367509331e-02, + -4.538812303131847109e-02, 1.058247419681241676e-02, + 1.679392617013223121e-01, -2.257826240741929533e-03, + -4.490146347357203138e-02, -1.148364179422036724e-01, + -1.169790528013799069e-02, 6.140403441496700837e-02, + -8.078778123309421355e-02, -5.838879041789352825e-02, + 6.773641084621376263e-02, -1.247724902386305318e-02, + 6.494524782787665373e-02, -1.174787360813439457e-01}; std::vector expected_v = { - -1.264062189119726106e-04, -1.636544077308309524e-05, - 4.453224130911556590e-05, -7.947403699519458518e-06, - -4.603504987332719071e-05, 9.491045850088816000e-06, - 4.131028921467394082e-05, 9.691472468201876704e-06, - -3.323572704427471520e-05, -1.024556912293147473e-04, - 5.530809120954559223e-06, 5.211030391191971272e-05, - -3.851138686809524851e-06, 2.101414374153427902e-07, - 3.247573516972862344e-06, 4.561253716254950400e-05, - -3.865680092083280590e-06, -3.262252150841829144e-05, - -1.166788692566841262e-04, -1.814499890570940753e-05, - 2.155064011880968559e-05, -1.629918981392229854e-05, - -3.245631268444005592e-05, 2.968538417601228937e-05, - 2.463149007223104176e-05, 3.660689861518736949e-05, - -3.586518711234946879e-05, -1.424206401855391917e-04, - -1.017840928263488617e-05, 1.421307534994552908e-05, - -8.618294024757048886e-06, -2.192409332705383732e-05, - 3.461715847634955364e-05, 1.277625693457703254e-05, - 3.486479415793123331e-05, -5.604161168847292539e-05, - -8.612844407008925294e-05, 2.508361660152536905e-06, - -1.633895954532816838e-07, 1.903591783622687189e-06, - -3.028341203071209831e-05, 4.685511271783763848e-05, - 2.876824509984768127e-06, 4.576515617130315025e-05, - -7.108738780331672730e-05, -1.062354815105974551e-04, - -2.954644717832223544e-05, 4.075640001084848793e-05, - -3.138369091725678469e-05, -1.316088004849699161e-05, - 1.786389692843508954e-05, 4.579187321116929540e-05, - 1.869753034515593833e-05, -2.550749273395911822e-05, - -1.264062189119726106e-04, -1.636544077308309524e-05, - 4.453224130911556590e-05, -7.947403699519458518e-06, - -4.603504987332719071e-05, 9.491045850088816000e-06, - 4.131028921467394082e-05, 9.691472468201876704e-06, - -3.323572704427471520e-05, -1.024556912293147473e-04, - 5.530809120954559223e-06, 5.211030391191971272e-05, - -3.851138686809524851e-06, 2.101414374153427902e-07, - 3.247573516972862344e-06, 4.561253716254950400e-05, - -3.865680092083280590e-06, -3.262252150841829144e-05, - -1.166788692566841262e-04, -1.814499890570940753e-05, - 2.155064011880968559e-05, -1.629918981392229854e-05, - -3.245631268444005592e-05, 2.968538417601228937e-05, - 2.463149007223104176e-05, 3.660689861518736949e-05, - -3.586518711234946879e-05, -1.424206401855391917e-04, - -1.017840928263488617e-05, 1.421307534994552908e-05, - -8.618294024757048886e-06, -2.192409332705383732e-05, - 3.461715847634955364e-05, 1.277625693457703254e-05, - 3.486479415793123331e-05, -5.604161168847292539e-05, - -8.612844407008925294e-05, 2.508361660152536905e-06, - -1.633895954532816838e-07, 1.903591783622687189e-06, - -3.028341203071209831e-05, 4.685511271783763848e-05, - 2.876824509984768127e-06, 4.576515617130315025e-05, - -7.108738780331672730e-05, -1.062354815105974551e-04, - -2.954644717832223544e-05, 4.075640001084848793e-05, - -3.138369091725678469e-05, -1.316088004849699161e-05, - 1.786389692843508954e-05, 4.579187321116929540e-05, - 1.869753034515593833e-05, -2.550749273395911822e-05}; + -1.589185601903579381e-01, 2.586167090689088510e-03, + -1.575150812458056548e-04, -1.855360549216640564e-02, + 1.949822308966445150e-02, -1.006552178977542650e-02, + 3.177030388421490936e-02, 1.714350280402104215e-03, + -1.290389705296313833e-03, -8.553511587973079699e-02, + -5.654638208496251539e-03, -1.286955066237439882e-02, + 2.464156699303176462e-02, -2.398203243424212178e-02, + -1.957110698882909630e-02, 2.233493653505165544e-02, + 6.107843889444162372e-03, 1.707076397717688723e-03, + -1.653994136896924094e-01, 3.894358809712639147e-02, + -2.169596032233910010e-02, 6.819702786556020371e-03, + -5.018240707559744503e-03, 2.640663592968431426e-03, + -1.985295554050418160e-03, -3.638422207618969423e-02, + 2.342932709960221863e-02, -8.501331666888653493e-02, + -2.181253119706856591e-03, 4.311299629418858387e-03, + -1.910329576491436726e-03, -1.808810428459609043e-03, + -1.540075460017477360e-03, -1.173703527688202929e-02, + -2.596307050960845741e-03, 6.705026635782097323e-03, + -9.038454847872562370e-02, 3.011717694088476838e-02, + -5.083053967307901710e-02, -2.951212926932282599e-03, + 2.342446057919112673e-02, -4.091208178777860222e-02, + -1.648470670751139844e-02, -2.872262362355524484e-02, + 4.763925761561256522e-02, -8.300037376164930147e-02, + 1.020429200603871836e-03, -1.026734257188876599e-03, + 5.678534821710372327e-02, 1.273635858276599142e-02, + -1.530143401888291177e-02, -1.061672032476311256e-01, + -2.486859787145567074e-02, 2.875323543588798395e-02, + -1.589185601903579381e-01, 2.586167090689088510e-03, + -1.575150812458056548e-04, -1.855360549216640564e-02, + 1.949822308966445150e-02, -1.006552178977542650e-02, + 3.177030388421490936e-02, 1.714350280402104215e-03, + -1.290389705296313833e-03, -8.553511587973079699e-02, + -5.654638208496251539e-03, -1.286955066237439882e-02, + 2.464156699303176462e-02, -2.398203243424212178e-02, + -1.957110698882909630e-02, 2.233493653505165544e-02, + 6.107843889444162372e-03, 1.707076397717688723e-03, + -1.653994136896924094e-01, 3.894358809712639147e-02, + -2.169596032233910010e-02, 6.819702786556020371e-03, + -5.018240707559744503e-03, 2.640663592968431426e-03, + -1.985295554050418160e-03, -3.638422207618969423e-02, + 2.342932709960221863e-02, -8.501331666888653493e-02, + -2.181253119706856591e-03, 4.311299629418858387e-03, + -1.910329576491436726e-03, -1.808810428459609043e-03, + -1.540075460017477360e-03, -1.173703527688202929e-02, + -2.596307050960845741e-03, 6.705026635782097323e-03, + -9.038454847872562370e-02, 3.011717694088476838e-02, + -5.083053967307901710e-02, -2.951212926932282599e-03, + 2.342446057919112673e-02, -4.091208178777860222e-02, + -1.648470670751139844e-02, -2.872262362355524484e-02, + 4.763925761561256522e-02, -8.300037376164930147e-02, + 1.020429200603871836e-03, -1.026734257188876599e-03, + 5.678534821710372327e-02, 1.273635858276599142e-02, + -1.530143401888291177e-02, -1.061672032476311256e-01, + -2.486859787145567074e-02, 2.875323543588798395e-02}; int natoms; int nframes = 2; std::vector expected_tot_e; diff --git a/source/api_cc/tests/test_deeppot_a_fparam_aparam_pt.cc b/source/api_cc/tests/test_deeppot_a_fparam_aparam_pt.cc index b84d45a9bb..c8569ec212 100644 --- a/source/api_cc/tests/test_deeppot_a_fparam_aparam_pt.cc +++ b/source/api_cc/tests/test_deeppot_a_fparam_aparam_pt.cc @@ -28,49 +28,50 @@ class TestInferDeepPotAFParamAParamPt : public ::testing::Test { std::vector fparam = {0.25852028}; std::vector aparam = {0.25852028, 0.25852028, 0.25852028, 0.25852028, 0.25852028, 0.25852028}; - // Generated by source/tests/infer/gen_fparam_aparam.py (type_one_side=True) + // Generated by source/tests/infer/gen_fparam_aparam.py + // (from pre-committed fparam_aparam_default.pth, type_one_side=True) std::vector expected_e = { - 1.596836265688982293e-01, 1.596933624175455035e-01, - 1.596859462832928844e-01, 1.596779837732069107e-01, - 1.596776702807257142e-01, 1.596869048501883825e-01}; + -1.038271223729636539e-01, -7.285433579124989123e-02, + -9.467600492266425860e-02, -1.467050207422957442e-01, + -7.660561676973243195e-02, -7.277296000253175023e-02}; std::vector expected_f = { - 1.112134318320098417e-05, 1.085230789880272913e-04, - 9.298442641358670786e-07, 1.491517597320257801e-04, - -1.250419527572718750e-05, -9.768265174690742383e-05, - -5.021052645725076108e-05, -9.741678916887853762e-05, - 9.375317764392499637e-05, 8.664103999852429459e-05, - -4.538513400016661465e-05, 8.561605116672728300e-05, - -2.454811055475983474e-05, 1.079491454988312375e-04, - -1.656974003674590982e-04, -1.721555059017404292e-04, - -6.116610604208619416e-05, 8.308097903957838235e-05}; + 6.622266941151369601e-02, 5.278739714221529489e-02, + 2.265728009692277028e-02, -2.606048291367509331e-02, + -4.538812303131847109e-02, 1.058247419681241676e-02, + 1.679392617013223121e-01, -2.257826240741929533e-03, + -4.490146347357203138e-02, -1.148364179422036724e-01, + -1.169790528013799069e-02, 6.140403441496700837e-02, + -8.078778123309421355e-02, -5.838879041789352825e-02, + 6.773641084621376263e-02, -1.247724902386305318e-02, + 6.494524782787665373e-02, -1.174787360813439457e-01}; std::vector expected_v = { - -1.264062189119718516e-04, -1.636544077308298682e-05, - 4.453224130911559301e-05, -7.947403699518174416e-06, - -4.603504987332694676e-05, 9.491045850088937973e-06, - 4.131028921467351392e-05, 9.691472468201808941e-06, - -3.323572704427467454e-05, -1.024556912293136224e-04, - 5.530809120954762511e-06, 5.211030391191995666e-05, - -3.851138686809712045e-06, 2.101414374152978974e-07, - 3.247573516972806439e-06, 4.561253716254927361e-05, - -3.865680092083368681e-06, -3.262252150841838630e-05, - -1.166788692566848309e-04, -1.814499890570951256e-05, - 2.155064011880963138e-05, -1.629918981392338952e-05, - -3.245631268444034052e-05, 2.968538417601219450e-05, - 2.463149007223181425e-05, 3.660689861518750502e-05, - -3.586518711234942813e-05, -1.424206401855391917e-04, - -1.017840928263479808e-05, 1.421307534994556974e-05, - -8.618294024757196269e-06, -2.192409332705388475e-05, - 3.461715847634955364e-05, 1.277625693457723244e-05, - 3.486479415793142304e-05, -5.604161168847289151e-05, - -8.612844407008964597e-05, 2.508361660152530129e-06, - -1.633895954533155651e-07, 1.903591783622965016e-06, - -3.028341203071198989e-05, 4.685511271783774690e-05, - 2.876824509984395433e-06, 4.576515617130287920e-05, - -7.108738780331674085e-05, -1.062354815105980244e-04, - -2.954644717832236758e-05, 4.075640001084843372e-05, - -3.138369091725702186e-05, -1.316088004849702041e-05, - 1.786389692843502177e-05, 4.579187321116953935e-05, - 1.869753034515599593e-05, -2.550749273395904029e-05}; + -1.589185601903579381e-01, 2.586167090689088510e-03, + -1.575150812458056548e-04, -1.855360549216640564e-02, + 1.949822308966445150e-02, -1.006552178977542650e-02, + 3.177030388421490936e-02, 1.714350280402104215e-03, + -1.290389705296313833e-03, -8.553511587973079699e-02, + -5.654638208496251539e-03, -1.286955066237439882e-02, + 2.464156699303176462e-02, -2.398203243424212178e-02, + -1.957110698882909630e-02, 2.233493653505165544e-02, + 6.107843889444162372e-03, 1.707076397717688723e-03, + -1.653994136896924094e-01, 3.894358809712639147e-02, + -2.169596032233910010e-02, 6.819702786556020371e-03, + -5.018240707559744503e-03, 2.640663592968431426e-03, + -1.985295554050418160e-03, -3.638422207618969423e-02, + 2.342932709960221863e-02, -8.501331666888653493e-02, + -2.181253119706856591e-03, 4.311299629418858387e-03, + -1.910329576491436726e-03, -1.808810428459609043e-03, + -1.540075460017477360e-03, -1.173703527688202929e-02, + -2.596307050960845741e-03, 6.705026635782097323e-03, + -9.038454847872562370e-02, 3.011717694088476838e-02, + -5.083053967307901710e-02, -2.951212926932282599e-03, + 2.342446057919112673e-02, -4.091208178777860222e-02, + -1.648470670751139844e-02, -2.872262362355524484e-02, + 4.763925761561256522e-02, -8.300037376164930147e-02, + 1.020429200603871836e-03, -1.026734257188876599e-03, + 5.678534821710372327e-02, 1.273635858276599142e-02, + -1.530143401888291177e-02, -1.061672032476311256e-01, + -2.486859787145567074e-02, 2.875323543588798395e-02}; int natoms; double expected_tot_e; std::vector expected_tot_v; diff --git a/source/api_cc/tests/test_deeppot_a_fparam_aparam_ptexpt.cc b/source/api_cc/tests/test_deeppot_a_fparam_aparam_ptexpt.cc index 86a039c464..0ef30df29e 100644 --- a/source/api_cc/tests/test_deeppot_a_fparam_aparam_ptexpt.cc +++ b/source/api_cc/tests/test_deeppot_a_fparam_aparam_ptexpt.cc @@ -28,47 +28,47 @@ class TestInferDeepPotAFParamAParamPtExpt : public ::testing::Test { std::vector aparam = {0.25852028, 0.25852028, 0.25852028, 0.25852028, 0.25852028, 0.25852028}; std::vector expected_e = { - 1.596836265688982293e-01, 1.596933624175455035e-01, - 1.596859462832928844e-01, 1.596779837732069107e-01, - 1.596776702807257142e-01, 1.596869048501883825e-01}; + -1.038271223729636539e-01, -7.285433579124989123e-02, + -9.467600492266425860e-02, -1.467050207422957442e-01, + -7.660561676973243195e-02, -7.277296000253175023e-02}; std::vector expected_f = { - 1.112134318320094352e-05, 1.085230789880272100e-04, - 9.298442641358874074e-07, 1.491517597320257530e-04, - -1.250419527572717225e-05, -9.768265174690741028e-05, - -5.021052645725073397e-05, -9.741678916887848341e-05, - 9.375317764392495572e-05, 8.664103999852425394e-05, - -4.538513400016661465e-05, 8.561605116672722879e-05, - -2.454811055475981441e-05, 1.079491454988312104e-04, - -1.656974003674590440e-04, -1.721555059017403750e-04, - -6.116610604208616705e-05, 8.308097903957835525e-05}; + 6.622266941151369601e-02, 5.278739714221529489e-02, + 2.265728009692277028e-02, -2.606048291367509331e-02, + -4.538812303131847109e-02, 1.058247419681241676e-02, + 1.679392617013223121e-01, -2.257826240741929533e-03, + -4.490146347357203138e-02, -1.148364179422036724e-01, + -1.169790528013799069e-02, 6.140403441496700837e-02, + -8.078778123309421355e-02, -5.838879041789352825e-02, + 6.773641084621376263e-02, -1.247724902386305318e-02, + 6.494524782787665373e-02, -1.174787360813439457e-01}; std::vector expected_v = { - -1.264062189119726106e-04, -1.636544077308309524e-05, - 4.453224130911556590e-05, -7.947403699519458518e-06, - -4.603504987332719071e-05, 9.491045850088816000e-06, - 4.131028921467394082e-05, 9.691472468201876704e-06, - -3.323572704427471520e-05, -1.024556912293147473e-04, - 5.530809120954559223e-06, 5.211030391191971272e-05, - -3.851138686809524851e-06, 2.101414374153427902e-07, - 3.247573516972862344e-06, 4.561253716254950400e-05, - -3.865680092083280590e-06, -3.262252150841829144e-05, - -1.166788692566841262e-04, -1.814499890570940753e-05, - 2.155064011880968559e-05, -1.629918981392229854e-05, - -3.245631268444005592e-05, 2.968538417601228937e-05, - 2.463149007223104176e-05, 3.660689861518736949e-05, - -3.586518711234946879e-05, -1.424206401855391917e-04, - -1.017840928263488617e-05, 1.421307534994552908e-05, - -8.618294024757048886e-06, -2.192409332705383732e-05, - 3.461715847634955364e-05, 1.277625693457703254e-05, - 3.486479415793123331e-05, -5.604161168847292539e-05, - -8.612844407008925294e-05, 2.508361660152536905e-06, - -1.633895954532816838e-07, 1.903591783622687189e-06, - -3.028341203071209831e-05, 4.685511271783763848e-05, - 2.876824509984768127e-06, 4.576515617130315025e-05, - -7.108738780331672730e-05, -1.062354815105974551e-04, - -2.954644717832223544e-05, 4.075640001084848793e-05, - -3.138369091725678469e-05, -1.316088004849699161e-05, - 1.786389692843508954e-05, 4.579187321116929540e-05, - 1.869753034515593833e-05, -2.550749273395911822e-05}; + -1.589185601903579381e-01, 2.586167090689088510e-03, + -1.575150812458056548e-04, -1.855360549216640564e-02, + 1.949822308966445150e-02, -1.006552178977542650e-02, + 3.177030388421490936e-02, 1.714350280402104215e-03, + -1.290389705296313833e-03, -8.553511587973079699e-02, + -5.654638208496251539e-03, -1.286955066237439882e-02, + 2.464156699303176462e-02, -2.398203243424212178e-02, + -1.957110698882909630e-02, 2.233493653505165544e-02, + 6.107843889444162372e-03, 1.707076397717688723e-03, + -1.653994136896924094e-01, 3.894358809712639147e-02, + -2.169596032233910010e-02, 6.819702786556020371e-03, + -5.018240707559744503e-03, 2.640663592968431426e-03, + -1.985295554050418160e-03, -3.638422207618969423e-02, + 2.342932709960221863e-02, -8.501331666888653493e-02, + -2.181253119706856591e-03, 4.311299629418858387e-03, + -1.910329576491436726e-03, -1.808810428459609043e-03, + -1.540075460017477360e-03, -1.173703527688202929e-02, + -2.596307050960845741e-03, 6.705026635782097323e-03, + -9.038454847872562370e-02, 3.011717694088476838e-02, + -5.083053967307901710e-02, -2.951212926932282599e-03, + 2.342446057919112673e-02, -4.091208178777860222e-02, + -1.648470670751139844e-02, -2.872262362355524484e-02, + 4.763925761561256522e-02, -8.300037376164930147e-02, + 1.020429200603871836e-03, -1.026734257188876599e-03, + 5.678534821710372327e-02, 1.273635858276599142e-02, + -1.530143401888291177e-02, -1.061672032476311256e-01, + -2.486859787145567074e-02, 2.875323543588798395e-02}; int natoms; double expected_tot_e; std::vector expected_tot_v; @@ -345,3 +345,5 @@ TYPED_TEST(TestInferDeepPotNoDefaultFParamPtExpt, no_default_fparam) { EXPECT_EQ(dp.dim_fparam(), 1); EXPECT_FALSE(dp.has_default_fparam()); } +// DefaultFParam tests with expected values are in +// test_deeppot_default_fparam_ptexpt.cc (from upstream #5343). diff --git a/source/api_cc/tests/test_deeppot_default_fparam_ptexpt.cc b/source/api_cc/tests/test_deeppot_default_fparam_ptexpt.cc index e7d5795777..88db060521 100644 --- a/source/api_cc/tests/test_deeppot_default_fparam_ptexpt.cc +++ b/source/api_cc/tests/test_deeppot_default_fparam_ptexpt.cc @@ -31,48 +31,49 @@ class TestInferDeepPotDefaultFParamPtExpt : public ::testing::Test { // explicit fparam for backward compat test std::vector fparam = {0.25852028}; // expected values computed with default fparam + // (from pre-committed fparam_aparam_default.pth via gen_fparam_aparam.py) std::vector expected_e = { - 1.596836265688982293e-01, 1.596933624175455035e-01, - 1.596859462832928844e-01, 1.596779837732069107e-01, - 1.596776702807257142e-01, 1.596869048501883825e-01}; + -1.038271223729636539e-01, -7.285433579124989123e-02, + -9.467600492266425860e-02, -1.467050207422957442e-01, + -7.660561676973243195e-02, -7.277296000253175023e-02}; std::vector expected_f = { - 1.112134318320098417e-05, 1.085230789880272913e-04, - 9.298442641358670786e-07, 1.491517597320257801e-04, - -1.250419527572718750e-05, -9.768265174690742383e-05, - -5.021052645725076108e-05, -9.741678916887853762e-05, - 9.375317764392499637e-05, 8.664103999852429459e-05, - -4.538513400016661465e-05, 8.561605116672728300e-05, - -2.454811055475983474e-05, 1.079491454988312375e-04, - -1.656974003674590982e-04, -1.721555059017404292e-04, - -6.116610604208619416e-05, 8.308097903957838235e-05}; + 6.622266941151369601e-02, 5.278739714221529489e-02, + 2.265728009692277028e-02, -2.606048291367509331e-02, + -4.538812303131847109e-02, 1.058247419681241676e-02, + 1.679392617013223121e-01, -2.257826240741929533e-03, + -4.490146347357203138e-02, -1.148364179422036724e-01, + -1.169790528013799069e-02, 6.140403441496700837e-02, + -8.078778123309421355e-02, -5.838879041789352825e-02, + 6.773641084621376263e-02, -1.247724902386305318e-02, + 6.494524782787665373e-02, -1.174787360813439457e-01}; std::vector expected_v = { - -1.264062189119718516e-04, -1.636544077308298682e-05, - 4.453224130911559301e-05, -7.947403699518174416e-06, - -4.603504987332694676e-05, 9.491045850088937973e-06, - 4.131028921467351392e-05, 9.691472468201808941e-06, - -3.323572704427467454e-05, -1.024556912293136224e-04, - 5.530809120954762511e-06, 5.211030391191995666e-05, - -3.851138686809712045e-06, 2.101414374152978974e-07, - 3.247573516972806439e-06, 4.561253716254927361e-05, - -3.865680092083368681e-06, -3.262252150841838630e-05, - -1.166788692566848309e-04, -1.814499890570951256e-05, - 2.155064011880963138e-05, -1.629918981392338952e-05, - -3.245631268444034052e-05, 2.968538417601219450e-05, - 2.463149007223181425e-05, 3.660689861518750502e-05, - -3.586518711234942813e-05, -1.424206401855391917e-04, - -1.017840928263479808e-05, 1.421307534994556974e-05, - -8.618294024757196269e-06, -2.192409332705388475e-05, - 3.461715847634955364e-05, 1.277625693457723244e-05, - 3.486479415793142304e-05, -5.604161168847289151e-05, - -8.612844407008964597e-05, 2.508361660152530129e-06, - -1.633895954533155651e-07, 1.903591783622965016e-06, - -3.028341203071198989e-05, 4.685511271783774690e-05, - 2.876824509984395433e-06, 4.576515617130287920e-05, - -7.108738780331674085e-05, -1.062354815105980244e-04, - -2.954644717832236758e-05, 4.075640001084843372e-05, - -3.138369091725702186e-05, -1.316088004849702041e-05, - 1.786389692843502177e-05, 4.579187321116953935e-05, - 1.869753034515599593e-05, -2.550749273395904029e-05}; + -1.589185601903579381e-01, 2.586167090689088510e-03, + -1.575150812458056548e-04, -1.855360549216640564e-02, + 1.949822308966445150e-02, -1.006552178977542650e-02, + 3.177030388421490936e-02, 1.714350280402104215e-03, + -1.290389705296313833e-03, -8.553511587973079699e-02, + -5.654638208496251539e-03, -1.286955066237439882e-02, + 2.464156699303176462e-02, -2.398203243424212178e-02, + -1.957110698882909630e-02, 2.233493653505165544e-02, + 6.107843889444162372e-03, 1.707076397717688723e-03, + -1.653994136896924094e-01, 3.894358809712639147e-02, + -2.169596032233910010e-02, 6.819702786556020371e-03, + -5.018240707559744503e-03, 2.640663592968431426e-03, + -1.985295554050418160e-03, -3.638422207618969423e-02, + 2.342932709960221863e-02, -8.501331666888653493e-02, + -2.181253119706856591e-03, 4.311299629418858387e-03, + -1.910329576491436726e-03, -1.808810428459609043e-03, + -1.540075460017477360e-03, -1.173703527688202929e-02, + -2.596307050960845741e-03, 6.705026635782097323e-03, + -9.038454847872562370e-02, 3.011717694088476838e-02, + -5.083053967307901710e-02, -2.951212926932282599e-03, + 2.342446057919112673e-02, -4.091208178777860222e-02, + -1.648470670751139844e-02, -2.872262362355524484e-02, + 4.763925761561256522e-02, -8.300037376164930147e-02, + 1.020429200603871836e-03, -1.026734257188876599e-03, + 5.678534821710372327e-02, 1.273635858276599142e-02, + -1.530143401888291177e-02, -1.061672032476311256e-01, + -2.486859787145567074e-02, 2.875323543588798395e-02}; int natoms; double expected_tot_e; std::vector expected_tot_v; diff --git a/source/api_cc/tests/test_deeppot_dpa1_pt.cc b/source/api_cc/tests/test_deeppot_dpa1_pt.cc index d5bdec9f6b..324537b87e 100644 --- a/source/api_cc/tests/test_deeppot_dpa1_pt.cc +++ b/source/api_cc/tests/test_deeppot_dpa1_pt.cc @@ -13,9 +13,8 @@ #include "neighbor_list.h" #include "test_utils.h" -// DPA1 models need relaxed epsilon #undef EPSILON -#define EPSILON (std::is_same::value ? 1e-7 : 1e-1) +#define EPSILON (std::is_same::value ? 1e-10 : 1e-4) template class TestInferDeepPotDpa1Pt : public ::testing::Test { diff --git a/source/api_cc/tests/test_deeppot_dpa1_ptexpt.cc b/source/api_cc/tests/test_deeppot_dpa1_ptexpt.cc new file mode 100644 index 0000000000..53a631f6f8 --- /dev/null +++ b/source/api_cc/tests/test_deeppot_dpa1_ptexpt.cc @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Test C++ inference for pt_expt (.pt2) backend with DPA1 (mixed-type) model. +// Uses the same model weights as test_deeppot_dpa1_pt.cc (generated from +// dpmodel with type_one_side=True via gen_dpa1.py). +#include + +#include +#include +#include +#include + +#include "DeepPot.h" +#include "neighbor_list.h" +#include "test_utils.h" + +#undef EPSILON +#define EPSILON (std::is_same::value ? 1e-10 : 1e-4) + +template +class TestInferDeepPotDpa1PtExpt : public ::testing::Test { + protected: + std::vector coord = {12.83, 2.56, 2.18, 12.09, 2.87, 2.74, + 00.25, 3.32, 1.68, 3.36, 3.00, 1.81, + 3.51, 2.51, 2.60, 4.27, 3.22, 1.56}; + std::vector atype = {0, 1, 1, 0, 1, 1}; + std::vector box = {13., 0., 0., 0., 13., 0., 0., 0., 13.}; + // Generated by source/tests/infer/gen_dpa1.py (PBC) + // Same weights as deeppot_dpa1.pt2 + std::vector expected_e = { + 5.386638169248214592e-02, 1.059305581653169626e-01, + 1.056662428333008108e-01, 5.332579202194744072e-02, + 1.055440262465702078e-01, 1.057385624627426024e-01}; + std::vector expected_f = { + 1.648023333711822261e-03, -1.810048390860390793e-03, + -5.640562778586664765e-04, 3.013935312554897467e-03, + 2.211467677140969111e-03, -2.373856990519772683e-03, + -2.310432060523760333e-03, -5.200587083006632388e-04, + 2.984950804954828157e-03, -2.118412923109444426e-03, + -1.589600452323191747e-04, -5.031858091203992222e-05, + 2.128234223750871245e-03, 2.940938628730816254e-03, + -4.128223543526785520e-03, -2.361347886384386214e-03, + -2.663339161478411616e-03, 4.131504587862436066e-03}; + std::vector expected_v = { + -5.780431274806642440e-04, -6.731158836777899682e-04, + 8.928830465533880122e-05, -4.786034663686604580e-04, + 8.892019221515710860e-04, 2.219211131658886931e-04, + -4.434673793050388494e-05, 1.863130189461770551e-04, + -3.790236017741206937e-05, -3.369804496903394848e-03, + 7.855880134671675268e-04, 1.743143964676567093e-03, + -1.426107460769170674e-03, 1.064489517343411660e-04, + 1.175541688057654688e-03, 1.938223402174633642e-03, + -6.492213546077969669e-04, -1.365956216959145295e-03, + -2.861954432782880600e-03, -1.773174860048345016e-03, + 1.277805252537157332e-03, 2.791729211298507834e-04, + -6.792377647208073643e-04, 1.097327565792464358e-04, + 9.259305614577704058e-04, 1.990815741066720955e-03, + -1.427531754436174886e-03, -6.007921168277090230e-04, + -4.342612523625760624e-04, 9.518674371962711689e-04, + -4.117735433350122251e-04, -1.422578249509541906e-04, + 1.669790464078422653e-04, 7.799013571504639876e-04, + 1.602754100511418107e-04, -1.667144996316817203e-04, + -2.790099105181320996e-03, -1.030047613180968972e-03, + 1.674076778664501837e-03, 6.636718025016051081e-04, + -1.175563308688121634e-03, 1.897194888911759789e-03, + -7.706408238661761889e-04, 1.660842536210703924e-03, + -2.685666208624810317e-03, -3.130480608254443929e-03, + -2.935998256729172636e-04, 2.485449126469695103e-04, + -2.044971674634045271e-03, -6.830748118353441615e-04, + 8.723614811463991899e-04, 3.155658891390614521e-03, + 1.094705622601841247e-03, -1.414833135157768764e-03}; + int natoms; + double expected_tot_e; + std::vector expected_tot_v; + + deepmd::DeepPot dp; + + void SetUp() override { +#ifndef BUILD_PYTORCH + GTEST_SKIP() << "Skip because PyTorch support is not enabled."; +#endif + dp.init("../../tests/infer/deeppot_dpa1.pt2"); + + natoms = expected_e.size(); + EXPECT_EQ(natoms * 3, expected_f.size()); + EXPECT_EQ(natoms * 9, expected_v.size()); + expected_tot_e = 0.; + expected_tot_v.resize(9); + std::fill(expected_tot_v.begin(), expected_tot_v.end(), 0.); + for (int ii = 0; ii < natoms; ++ii) { + expected_tot_e += expected_e[ii]; + } + for (int ii = 0; ii < natoms; ++ii) { + for (int dd = 0; dd < 9; ++dd) { + expected_tot_v[dd] += expected_v[ii * 9 + dd]; + } + } + }; + + void TearDown() override {}; +}; + +TYPED_TEST_SUITE(TestInferDeepPotDpa1PtExpt, ValueTypes); + +TYPED_TEST(TestInferDeepPotDpa1PtExpt, cpu_build_nlist) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_f = this->expected_f; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + double ener; + std::vector force, virial; + dp.compute(ener, force, virial, coord, atype, box); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotDpa1PtExpt, cpu_build_nlist_atomic) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + double ener; + std::vector force, virial, atom_ener, atom_vir; + dp.compute(ener, force, virial, atom_ener, atom_vir, coord, atype, box); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + EXPECT_EQ(atom_ener.size(), natoms); + EXPECT_EQ(atom_vir.size(), natoms * 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + for (int ii = 0; ii < natoms; ++ii) { + EXPECT_LT(fabs(atom_ener[ii] - expected_e[ii]), EPSILON); + } + for (int ii = 0; ii < natoms * 9; ++ii) { + EXPECT_LT(fabs(atom_vir[ii] - expected_v[ii]), EPSILON); + } +} + +template +class TestInferDeepPotDpa1PtExptNoPbc : public ::testing::Test { + protected: + std::vector coord = {12.83, 2.56, 2.18, 12.09, 2.87, 2.74, + 00.25, 3.32, 1.68, 3.36, 3.00, 1.81, + 3.51, 2.51, 2.60, 4.27, 3.22, 1.56}; + std::vector atype = {0, 1, 1, 0, 1, 1}; + std::vector box = {}; + // Generated by source/tests/infer/gen_dpa1.py (NoPbc) + std::vector expected_e = { + 5.641189028707595948e-02, 1.061963332622648665e-01, + 1.061816546452520604e-01, 5.345089797582680546e-02, + 1.056741039642918600e-01, 1.057524180734084607e-01}; + std::vector expected_f = { + -3.072930739880543310e-04, 1.287308823463468883e-04, + 2.325461100450139330e-04, 3.072930739880543310e-04, + -1.287308823463468883e-04, -2.325461100450139330e-04, + 8.145642124706340120e-04, -1.013443003399291530e-04, + 1.027376450650044392e-04, -1.525042369849348087e-03, + 9.724660386441295185e-06, -2.637088172652348470e-04, + 2.943676073439134653e-03, 2.725788047293053844e-03, + -3.882816498501044569e-03, -2.233197916060420361e-03, + -2.634168407339566257e-03, 4.043787670701274976e-03}; + std::vector expected_v = { + 7.690235058806288090e-04, -3.221584957067486670e-04, + -5.819637341799332227e-04, -3.221584957067492091e-04, + 1.349582887420156126e-04, 2.437956183726743475e-04, + -5.819637341799395111e-04, 2.437956183726751064e-04, + 4.404049880280591417e-04, -9.964203806317891397e-04, + 4.174193486430455681e-04, 7.540478556132436524e-04, + 4.174193486430458934e-04, -1.748648622693831176e-04, + -3.158849124866286288e-04, 7.540478556132498324e-04, + -3.158849124866294419e-04, -5.706308096532668791e-04, + -1.502820864633459211e-03, 2.276206872366074579e-04, + -2.355522888214717139e-04, 2.038607715354323201e-04, + -3.769132204670805602e-05, 3.903774408363665086e-05, + -2.601012918842465858e-04, 4.388984753508028396e-05, + -3.958932226991843692e-05, 2.399483162566252992e-04, + -4.383767135451629299e-04, 9.058040635350809132e-04, + -3.480376811900160578e-04, -1.023460282968784163e-04, + 1.069757548267320551e-04, 7.554026784872882325e-04, + 1.052835549305723749e-04, -8.105515243794243893e-05, + -5.835881428269892868e-04, -1.264750641879006793e-03, + 1.968091530552973020e-03, 3.905956580851997068e-04, + -1.141849627737433551e-03, 1.833849831634470733e-03, + -6.399943610726106791e-04, 1.624578487870950022e-03, + -2.620009641506095173e-03, -2.277492702178959827e-03, + -1.975376013403041739e-04, 1.395669241442570353e-04, + -1.919463017958483383e-03, -6.656963908160604126e-04, + 8.452260876957553085e-04, 2.922603203880410130e-03, + 1.051337527903990765e-03, -1.351073729135638529e-03}; + int natoms; + double expected_tot_e; + std::vector expected_tot_v; + + deepmd::DeepPot dp; + + void SetUp() override { +#ifndef BUILD_PYTORCH + GTEST_SKIP() << "Skip because PyTorch support is not enabled."; +#endif + dp.init("../../tests/infer/deeppot_dpa1.pt2"); + + natoms = expected_e.size(); + EXPECT_EQ(natoms * 3, expected_f.size()); + EXPECT_EQ(natoms * 9, expected_v.size()); + expected_tot_e = 0.; + expected_tot_v.resize(9); + std::fill(expected_tot_v.begin(), expected_tot_v.end(), 0.); + for (int ii = 0; ii < natoms; ++ii) { + expected_tot_e += expected_e[ii]; + } + for (int ii = 0; ii < natoms; ++ii) { + for (int dd = 0; dd < 9; ++dd) { + expected_tot_v[dd] += expected_v[ii * 9 + dd]; + } + } + }; + + void TearDown() override {}; +}; + +TYPED_TEST_SUITE(TestInferDeepPotDpa1PtExptNoPbc, ValueTypes); + +TYPED_TEST(TestInferDeepPotDpa1PtExptNoPbc, cpu_build_nlist) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_f = this->expected_f; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + double ener; + std::vector force, virial; + dp.compute(ener, force, virial, coord, atype, box); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} + +TYPED_TEST(TestInferDeepPotDpa1PtExptNoPbc, cpu_lmp_nlist) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_f = this->expected_f; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + double ener; + std::vector force, virial; + + std::vector > nlist_data = { + {1, 2, 3, 4, 5}, {0, 2, 3, 4, 5}, {0, 1, 3, 4, 5}, + {0, 1, 2, 4, 5}, {0, 1, 2, 3, 5}, {0, 1, 2, 3, 4}}; + std::vector ilist(natoms), numneigh(natoms); + std::vector firstneigh(natoms); + deepmd::InputNlist inlist(natoms, &ilist[0], &numneigh[0], &firstneigh[0]); + convert_nlist(inlist, nlist_data); + dp.compute(ener, force, virial, coord, atype, box, 0, inlist, 0); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } +} diff --git a/source/api_cc/tests/test_deeppot_dpa2_pt.cc b/source/api_cc/tests/test_deeppot_dpa2_pt.cc index 146c8a6803..180bf8363c 100644 --- a/source/api_cc/tests/test_deeppot_dpa2_pt.cc +++ b/source/api_cc/tests/test_deeppot_dpa2_pt.cc @@ -14,7 +14,7 @@ // DPA2 models need relaxed epsilon for float32 due to attention layers #undef EPSILON -#define EPSILON (std::is_same::value ? 1e-7 : 1e-4) +#define EPSILON (std::is_same::value ? 1e-10 : 1e-4) template class TestInferDeepPotDpa2Pt : public ::testing::Test { diff --git a/source/api_cc/tests/test_deeppot_dpa2_ptexpt.cc b/source/api_cc/tests/test_deeppot_dpa2_ptexpt.cc index b51b55985b..4bbf1292b0 100644 --- a/source/api_cc/tests/test_deeppot_dpa2_ptexpt.cc +++ b/source/api_cc/tests/test_deeppot_dpa2_ptexpt.cc @@ -14,7 +14,7 @@ // DPA2 models need relaxed epsilon (same as test_deeppot_dpa2_pt.cc) #undef EPSILON -#define EPSILON (std::is_same::value ? 1e-7 : 1e-1) +#define EPSILON (std::is_same::value ? 1e-10 : 1e-4) template class TestInferDeepPotDpa2PtExpt : public ::testing::Test { diff --git a/source/api_cc/tests/test_deeppot_dpa3_pt.cc b/source/api_cc/tests/test_deeppot_dpa3_pt.cc index 4ad91f8a28..5b9c53f167 100644 --- a/source/api_cc/tests/test_deeppot_dpa3_pt.cc +++ b/source/api_cc/tests/test_deeppot_dpa3_pt.cc @@ -14,7 +14,7 @@ // DPA3 models need relaxed epsilon #undef EPSILON -#define EPSILON (std::is_same::value ? 1e-7 : 1e-1) +#define EPSILON (std::is_same::value ? 1e-10 : 1e-4) template class TestInferDeepPotDpa3Pt : public ::testing::Test { diff --git a/source/api_cc/tests/test_deeppot_dpa3_ptexpt.cc b/source/api_cc/tests/test_deeppot_dpa3_ptexpt.cc index 16069b007b..2767f5c641 100644 --- a/source/api_cc/tests/test_deeppot_dpa3_ptexpt.cc +++ b/source/api_cc/tests/test_deeppot_dpa3_ptexpt.cc @@ -14,7 +14,7 @@ // DPA3 models need relaxed epsilon (same as test_deeppot_dpa3_pt.cc) #undef EPSILON -#define EPSILON (std::is_same::value ? 1e-7 : 1e-1) +#define EPSILON (std::is_same::value ? 1e-10 : 1e-4) template class TestInferDeepPotDpa3PtExpt : public ::testing::Test { diff --git a/source/api_cc/tests/test_deeppot_dpa_pt.cc b/source/api_cc/tests/test_deeppot_dpa_pt.cc index 0de83278a9..04f42ba94a 100644 --- a/source/api_cc/tests/test_deeppot_dpa_pt.cc +++ b/source/api_cc/tests/test_deeppot_dpa_pt.cc @@ -15,7 +15,7 @@ // 1e-10 cannot pass; unclear bug or not #undef EPSILON -#define EPSILON (std::is_same::value ? 1e-7 : 1e-1) +#define EPSILON (std::is_same::value ? 1e-10 : 1e-4) template class TestInferDeepPotDpaPt : public ::testing::Test { diff --git a/source/api_cc/tests/test_deeppot_dpa_ptexpt.cc b/source/api_cc/tests/test_deeppot_dpa_ptexpt.cc index 1a380760a8..769c166d12 100644 --- a/source/api_cc/tests/test_deeppot_dpa_ptexpt.cc +++ b/source/api_cc/tests/test_deeppot_dpa_ptexpt.cc @@ -13,7 +13,7 @@ // DPA1 models need relaxed epsilon (same as test_deeppot_dpa_pt.cc) #undef EPSILON -#define EPSILON (std::is_same::value ? 1e-7 : 1e-1) +#define EPSILON (std::is_same::value ? 1e-10 : 1e-4) template class TestInferDeepPotDpaPtExpt : public ::testing::Test { diff --git a/source/api_cc/tests/test_deeppot_model_devi_ptexpt.cc b/source/api_cc/tests/test_deeppot_model_devi_ptexpt.cc index 7b5a3bbd9e..69f5082295 100644 --- a/source/api_cc/tests/test_deeppot_model_devi_ptexpt.cc +++ b/source/api_cc/tests/test_deeppot_model_devi_ptexpt.cc @@ -408,11 +408,11 @@ class TestInferDeepPotModeDeviPtExptPrecomputed : public ::testing::Test { 0.25852028, 0.25852028, 0.25852028}; int natoms; std::vector expected_md_f = { - 8.458165365077899029e-04, 5.029083479824372890e-04, - 6.882240709962303546e-04}; // max min avg + 3.443412186399823823e-04, 1.456244134905365292e-04, + 2.585092315848599867e-04}; // max min avg std::vector expected_md_v = { - 2.467581010020606898e-04, 4.340036212959217315e-05, - 1.286020468466399431e-04}; // max min mystd + 1.735782978938303146e-04, 2.441022592033971239e-05, + 7.333090508662540210e-05}; // max min mystd deepmd::DeepPot dp0; deepmd::DeepPot dp1; diff --git a/source/api_cc/tests/test_deeppot_ptexpt.cc b/source/api_cc/tests/test_deeppot_ptexpt.cc index 6a1cabefba..2cda44bb19 100644 --- a/source/api_cc/tests/test_deeppot_ptexpt.cc +++ b/source/api_cc/tests/test_deeppot_ptexpt.cc @@ -19,6 +19,10 @@ class TestInferDeepPotAPtExpt : public ::testing::Test { std::vector coord = {12.83, 2.56, 2.18, 12.09, 2.87, 2.74, 00.25, 3.32, 1.68, 3.36, 3.00, 1.81, 3.51, 2.51, 2.60, 4.27, 3.22, 1.56}; + // Alternative coords for multi-frame tests (must give different energy) + std::vector coord_alt = {10.06, 5.71, 11.16, 9.07, 1.22, 12.68, + 9.89, 10.22, 1.67, 5.86, 4.82, 12.05, + 8.37, 10.70, 5.76, 2.95, 7.21, 0.83}; std::vector atype = {0, 1, 1, 0, 1, 1}; std::vector box = {13., 0., 0., 0., 13., 0., 0., 0., 13.}; // Same reference values as test_deeppot_pt.cc (model converted from .pth) @@ -414,6 +418,9 @@ class TestInferDeepPotAPtExptNoPbc : public ::testing::Test { std::vector coord = {12.83, 2.56, 2.18, 12.09, 2.87, 2.74, 00.25, 3.32, 1.68, 3.36, 3.00, 1.81, 3.51, 2.51, 2.60, 4.27, 3.22, 1.56}; + std::vector coord_alt = {10.06, 5.71, 11.16, 9.07, 1.22, 12.68, + 9.89, 10.22, 1.67, 5.86, 4.82, 12.05, + 8.37, 10.70, 5.76, 2.95, 7.21, 0.83}; std::vector atype = {0, 1, 1, 0, 1, 1}; std::vector box = {}; // Same reference values as TestInferDeepPotAPtNoPbc in test_deeppot_pt.cc @@ -500,3 +507,259 @@ TYPED_TEST(TestInferDeepPotAPtExptNoPbc, cpu_build_nlist) { EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); } } + +TYPED_TEST(TestInferDeepPotAPtExptNoPbc, cpu_build_nlist_atomic) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_e = this->expected_e; + std::vector& expected_f = this->expected_f; + std::vector& expected_v = this->expected_v; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + double ener; + std::vector force, virial, atom_ener, atom_vir; + dp.compute(ener, force, virial, atom_ener, atom_vir, coord, atype, box); + + EXPECT_EQ(force.size(), natoms * 3); + EXPECT_EQ(virial.size(), 9); + EXPECT_EQ(atom_ener.size(), natoms); + EXPECT_EQ(atom_vir.size(), natoms * 9); + + EXPECT_LT(fabs(ener - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + for (int ii = 0; ii < 3 * 3; ++ii) { + EXPECT_LT(fabs(virial[ii] - expected_tot_v[ii]), EPSILON); + } + for (int ii = 0; ii < natoms; ++ii) { + EXPECT_LT(fabs(atom_ener[ii] - expected_e[ii]), EPSILON); + } + for (int ii = 0; ii < natoms * 9; ++ii) { + EXPECT_LT(fabs(atom_vir[ii] - expected_v[ii]), EPSILON); + } +} + +// Multi-frame PBC test via compute_mixed_type +TYPED_TEST(TestInferDeepPotAPtExpt, cpu_build_nlist_nframes) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& coord_alt = this->coord_alt; + std::vector& atype = this->atype; + std::vector& box = this->box; + std::vector& expected_f = this->expected_f; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + + int nframes = 2; + // Frame 0: original coords. Frame 1: alternative coords (coord_alt). + std::vector coord_2f(coord); + coord_2f.insert(coord_2f.end(), coord_alt.begin(), coord_alt.end()); + std::vector atype_2f(atype); + atype_2f.insert(atype_2f.end(), atype.begin(), atype.end()); + std::vector box_2f(box); + box_2f.insert(box_2f.end(), box.begin(), box.end()); + + std::vector ener; + std::vector force, virial; + dp.compute_mixed_type(ener, force, virial, nframes, coord_2f, atype_2f, + box_2f); + + EXPECT_EQ(ener.size(), nframes); + EXPECT_EQ(force.size(), nframes * natoms * 3); + EXPECT_EQ(virial.size(), nframes * 9); + + // Frame 0 should match reference + EXPECT_LT(fabs(ener[0] - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + // Frame 1 should be different (perturbed coords) + EXPECT_GT(fabs(ener[1] - ener[0]), 1e-10); +} + +// Multi-frame NoPBC test via compute_mixed_type +TYPED_TEST(TestInferDeepPotAPtExptNoPbc, cpu_build_nlist_nframes) { + using VALUETYPE = TypeParam; + std::vector& coord = this->coord; + std::vector& atype = this->atype; + std::vector& coord_alt = this->coord_alt; + std::vector& box = this->box; // empty + std::vector& expected_f = this->expected_f; + int& natoms = this->natoms; + double& expected_tot_e = this->expected_tot_e; + std::vector& expected_tot_v = this->expected_tot_v; + deepmd::DeepPot& dp = this->dp; + + int nframes = 2; + std::vector coord_2f(coord); + coord_2f.insert(coord_2f.end(), coord_alt.begin(), coord_alt.end()); + std::vector atype_2f(atype); + atype_2f.insert(atype_2f.end(), atype.begin(), atype.end()); + + std::vector ener; + std::vector force, virial; + dp.compute_mixed_type(ener, force, virial, nframes, coord_2f, atype_2f, box); + + EXPECT_EQ(ener.size(), nframes); + EXPECT_EQ(force.size(), nframes * natoms * 3); + EXPECT_EQ(virial.size(), nframes * 9); + + // Frame 0 should match reference + EXPECT_LT(fabs(ener[0] - expected_tot_e), EPSILON); + for (int ii = 0; ii < natoms * 3; ++ii) { + EXPECT_LT(fabs(force[ii] - expected_f[ii]), EPSILON); + } + // Frame 1 should be different (perturbed coords) + EXPECT_GT(fabs(ener[1] - ener[0]), 1e-10); +} + +// ========== Parser / metadata coverage tests ========== + +TEST(TestDeepPotPTExptParser, load_nonexistent_file) { +#ifndef BUILD_PYTORCH + GTEST_SKIP() << "Skip because PyTorch support is not enabled."; +#endif + deepmd::DeepPot dp; + EXPECT_THROW(dp.init("nonexistent_model.pt2"), deepmd::deepmd_exception); +} + +TEST(TestDeepPotPTExptParser, load_invalid_zip) { +#ifndef BUILD_PYTORCH + GTEST_SKIP() << "Skip because PyTorch support is not enabled."; +#endif + std::string tmpfile = "test_invalid.pt2"; + { + std::ofstream ofs(tmpfile, std::ios::binary); + ASSERT_TRUE(ofs.is_open()) << "Failed to create temp file"; + ofs << "not a zip file at all"; + } + deepmd::DeepPot dp; + EXPECT_THROW(dp.init(tmpfile), deepmd::deepmd_exception); + std::remove(tmpfile.c_str()); +} + +TEST(TestDeepPotPTExptParser, load_tiny_file) { +#ifndef BUILD_PYTORCH + GTEST_SKIP() << "Skip because PyTorch support is not enabled."; +#endif + std::string tmpfile = "test_tiny.pt2"; + { + std::ofstream ofs(tmpfile, std::ios::binary); + ASSERT_TRUE(ofs.is_open()) << "Failed to create temp file"; + ofs << "abc"; + } + deepmd::DeepPot dp; + EXPECT_THROW(dp.init(tmpfile), deepmd::deepmd_exception); + std::remove(tmpfile.c_str()); +} + +// Metadata accessor tests — exercise JSON parser on a real model +template +class TestDeepPotPTExptMetadata : public ::testing::Test { + protected: + deepmd::DeepPot dp; + void SetUp() override { +#ifndef BUILD_PYTORCH + GTEST_SKIP() << "Skip because PyTorch support is not enabled."; +#endif + dp.init("../../tests/infer/deeppot_sea.pt2"); + }; + void TearDown() override {}; +}; + +TYPED_TEST_SUITE(TestDeepPotPTExptMetadata, ValueTypes); + +TYPED_TEST(TestDeepPotPTExptMetadata, type_map) { + std::string type_map; + this->dp.get_type_map(type_map); + EXPECT_NE(type_map.find("O"), std::string::npos); + EXPECT_NE(type_map.find("H"), std::string::npos); +} + +TYPED_TEST(TestDeepPotPTExptMetadata, cutoff) { + EXPECT_GT(this->dp.cutoff(), 0.0); +} + +TYPED_TEST(TestDeepPotPTExptMetadata, ntypes) { + EXPECT_EQ(this->dp.numb_types(), 2); +} + +TYPED_TEST(TestDeepPotPTExptMetadata, dim_fparam_zero) { + EXPECT_EQ(this->dp.dim_fparam(), 0); +} + +TYPED_TEST(TestDeepPotPTExptMetadata, dim_aparam_zero) { + EXPECT_EQ(this->dp.dim_aparam(), 0); +} + +TYPED_TEST(TestDeepPotPTExptMetadata, no_default_fparam) { + EXPECT_FALSE(this->dp.has_default_fparam()); +} + +// JSON parser type-coverage via fparam model +template +class TestDeepPotPTExptJsonTypes : public ::testing::Test { + protected: + deepmd::DeepPot dp; + void SetUp() override { +#ifndef BUILD_PYTORCH + GTEST_SKIP() << "Skip because PyTorch support is not enabled."; +#endif + dp.init("../../tests/infer/fparam_aparam.pt2"); + }; + void TearDown() override {}; +}; + +TYPED_TEST_SUITE(TestDeepPotPTExptJsonTypes, ValueTypes); + +TYPED_TEST(TestDeepPotPTExptJsonTypes, integer_fields) { + EXPECT_EQ(this->dp.dim_fparam(), 1); + EXPECT_EQ(this->dp.dim_aparam(), 1); +} + +TYPED_TEST(TestDeepPotPTExptJsonTypes, boolean_field) { + EXPECT_FALSE(this->dp.has_default_fparam()); +} + +TYPED_TEST(TestDeepPotPTExptJsonTypes, string_array) { + std::string type_map; + this->dp.get_type_map(type_map); + EXPECT_FALSE(type_map.empty()); +} + +TYPED_TEST(TestDeepPotPTExptJsonTypes, float_field) { + EXPECT_GT(this->dp.cutoff(), 0.0); + EXPECT_LT(this->dp.cutoff(), 100.0); +} + +// Default fparam model — tests JSON parsing of boolean true + float array +template +class TestDeepPotPTExptJsonDefaults : public ::testing::Test { + protected: + deepmd::DeepPot dp; + void SetUp() override { +#ifndef BUILD_PYTORCH + GTEST_SKIP() << "Skip because PyTorch support is not enabled."; +#endif + dp.init("../../tests/infer/fparam_aparam_default.pt2"); + }; + void TearDown() override {}; +}; + +TYPED_TEST_SUITE(TestDeepPotPTExptJsonDefaults, ValueTypes); + +TYPED_TEST(TestDeepPotPTExptJsonDefaults, boolean_true) { + EXPECT_TRUE(this->dp.has_default_fparam()); +} + +TYPED_TEST(TestDeepPotPTExptJsonDefaults, default_fparam_parsed) { + EXPECT_EQ(this->dp.dim_fparam(), 1); + EXPECT_TRUE(this->dp.has_default_fparam()); +} diff --git a/source/lmp/pair_base.cpp b/source/lmp/pair_base.cpp index ab60ccc780..63e5462590 100644 --- a/source/lmp/pair_base.cpp +++ b/source/lmp/pair_base.cpp @@ -99,6 +99,11 @@ int PairDeepBaseModel::get_node_rank() { } std::string PairDeepBaseModel::get_file_content(const std::string& model) { + // .pt2 (AOTInductor) models do not support in-memory loading. + // Return empty so that init() loads from the file path directly. + if (model.size() >= 4 && model.compare(model.size() - 4, 4, ".pt2") == 0) { + return std::string(); + } int myrank = 0, root = 0; MPI_Comm_rank(MPI_COMM_WORLD, &myrank); int nchar = 0; diff --git a/source/lmp/tests/test_lammps_dpa3_pt2.py b/source/lmp/tests/test_lammps_dpa3_pt2.py new file mode 100644 index 0000000000..7ce05f9a2d --- /dev/null +++ b/source/lmp/tests/test_lammps_dpa3_pt2.py @@ -0,0 +1,317 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Test LAMMPS with .pt2 (AOTInductor) DPA3 model. + +Mirrors test_lammps_pt2.py (se_e2_a) but for the DPA3 descriptor. +Reference values from source/tests/infer/gen_dpa3.py / C++ test. +""" + +import os +from pathlib import ( + Path, +) + +import constants +import numpy as np +import pytest +from lammps import ( + PyLammps, +) +from write_lmp_data import ( + write_lmp_data, +) + +pb_file = Path(__file__).parent.parent.parent / "tests" / "infer" / "deeppot_dpa3.pt2" +data_file = Path(__file__).parent / "data_dpa3_pt2.lmp" +data_file_si = Path(__file__).parent / "data_dpa3_pt2.si" +data_type_map_file = Path(__file__).parent / "data_type_map_dpa3_pt2.lmp" + +# Reference values from gen_dpa3.py / test_deeppot_dpa3_ptexpt.cc (PBC) +expected_ae = np.array( + [ + 2.733142942358297023e-01, + 2.768815473296480922e-01, + 2.781664369968356865e-01, + 2.697839344989072519e-01, + 2.741210600049306945e-01, + 2.752870928812235496e-01, + ] +) +expected_e = np.sum(expected_ae) +expected_f = np.array( + [ + -1.962618723134541832e-02, + 4.287158582278347702e-02, + 7.640666386947853050e-03, + 5.554130248696588501e-02, + -6.501206231527984977e-03, + -4.524468847893595158e-02, + -3.851051736663693714e-02, + -3.620789238677154381e-02, + 3.756162244251591564e-02, + 6.729090678104879264e-02, + -2.430710555108604037e-02, + 4.496058666120762021e-02, + 9.285825331084011924e-03, + 5.623126339971108029e-02, + -8.776072674283137698e-02, + -7.398133000111631330e-02, + -3.208664505310900028e-02, + 4.284253973109593966e-02, + ] +).reshape(6, 3) + +expected_v = -np.array( + [ + -2.519191242984861884e-02, + -7.976296517418629550e-04, + 2.293255716383547221e-02, + -1.129879902880513709e-04, + -2.480533869648754441e-02, + 5.147545203263749480e-03, + 2.250634701911344987e-02, + 5.288887046140826331e-03, + -2.010244267109611085e-02, + -1.779331319768159489e-02, + 3.093850189397499839e-03, + 1.469388965841003300e-02, + -3.857294749719837688e-03, + 1.122172669801067097e-03, + 3.015485878866499582e-03, + 1.588838841470147090e-02, + -2.814760933954751562e-03, + -1.277216714527013713e-02, + -8.763367643346370306e-03, + -1.305889135368112908e-02, + 1.181350951828694096e-02, + -6.506014073233991855e-03, + -6.021216432246893902e-03, + 6.406967309407277100e-03, + 1.054423249710041179e-02, + 1.210616766999832172e-02, + -1.127472660426425549e-02, + -3.873334330831591787e-02, + -3.620067664760272686e-03, + 1.173198873109224322e-03, + -3.979800321914496279e-03, + -1.483777776121806245e-02, + 2.311848485249741111e-02, + 1.659292900032220339e-03, + 2.315104663227764842e-02, + -3.645194750481960122e-02, + -1.668107738824501848e-04, + -7.331929353596922626e-03, + 1.141573012886789966e-02, + -1.498650485705460686e-03, + -1.339178008942835431e-02, + 2.104129816063767672e-02, + 2.247013447171188061e-03, + 2.035538814221872148e-02, + -3.195007182084359104e-02, + -2.339460083073257798e-02, + -1.001949167693141039e-02, + 1.320033846426920537e-02, + -1.577941189045228843e-02, + -6.283307183655661120e-03, + 8.237968913765561507e-03, + 2.238394952866012630e-02, + 8.881021761757389166e-03, + -1.162377795308391741e-02, + ] +).reshape(6, 9) + +box = np.array([0, 13, 0, 13, 0, 13, 0, 0, 0]) +coord = np.array( + [ + [12.83, 2.56, 2.18], + [12.09, 2.87, 2.74], + [0.25, 3.32, 1.68], + [3.36, 3.00, 1.81], + [3.51, 2.51, 2.60], + [4.27, 3.22, 1.56], + ] +) +type_OH = np.array([1, 2, 2, 1, 2, 2]) +type_HO = np.array([2, 1, 1, 2, 1, 1]) + + +def setup_module() -> None: + if os.environ.get("ENABLE_PYTORCH", "1") != "1": + pytest.skip( + "Skip test because PyTorch support is not enabled.", + ) + write_lmp_data(box, coord, type_OH, data_file) + write_lmp_data(box, coord, type_HO, data_type_map_file) + write_lmp_data( + box * constants.dist_metal2si, + coord * constants.dist_metal2si, + type_OH, + data_file_si, + ) + + +def teardown_module() -> None: + for f in [data_file, data_type_map_file, data_file_si]: + if f.exists(): + os.remove(f) + + +def _lammps(data_file, units="metal") -> PyLammps: + lammps = PyLammps() + lammps.units(units) + lammps.boundary("p p p") + lammps.atom_style("atomic") + lammps.atom_modify("map yes") + if units == "metal" or units == "real": + lammps.neighbor("2.0 bin") + elif units == "si": + lammps.neighbor("2.0e-10 bin") + else: + raise ValueError("units should be metal, real, or si") + lammps.neigh_modify("every 10 delay 0 check no") + lammps.read_data(data_file.resolve()) + if units == "metal" or units == "real": + lammps.mass("1 16") + lammps.mass("2 2") + elif units == "si": + lammps.mass("1 %.10e" % (16 * constants.mass_metal2si)) + lammps.mass("2 %.10e" % (2 * constants.mass_metal2si)) + else: + raise ValueError("units should be metal, real, or si") + if units == "metal": + lammps.timestep(0.0005) + elif units == "real": + lammps.timestep(0.5) + elif units == "si": + lammps.timestep(5e-16) + else: + raise ValueError("units should be metal, real, or si") + lammps.fix("1 all nve") + return lammps + + +@pytest.fixture +def lammps(): + lmp = _lammps(data_file=data_file) + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_type_map(): + lmp = _lammps(data_file=data_type_map_file) + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_real(): + lmp = _lammps(data_file=data_file, units="real") + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_si(): + lmp = _lammps(data_file=data_file_si, units="si") + yield lmp + lmp.close() + + +def test_pair_deepmd(lammps) -> None: + lammps.pair_style(f"deepmd {pb_file.resolve()}") + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + lammps.run(1) + + +def test_pair_deepmd_virial(lammps) -> None: + lammps.pair_style(f"deepmd {pb_file.resolve()}") + lammps.pair_coeff("* *") + lammps.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps.variable(f"virial{jj} atom c_virial[{ii + 1}]") + lammps.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + idx_map = lammps.lmp.numpy.extract_atom("id")[: coord.shape[0]] - 1 + for ii in range(9): + assert np.array( + lammps.variables[f"virial{ii}"].value + ) / constants.nktv2p == pytest.approx(expected_v[idx_map, ii]) + + +def test_pair_deepmd_type_map(lammps_type_map) -> None: + lammps_type_map.pair_style(f"deepmd {pb_file.resolve()}") + lammps_type_map.pair_coeff("* * H O") + lammps_type_map.run(0) + assert lammps_type_map.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps_type_map.atoms[ii].force == pytest.approx( + expected_f[lammps_type_map.atoms[ii].id - 1] + ) + lammps_type_map.run(1) + + +def test_pair_deepmd_real(lammps_real) -> None: + lammps_real.pair_style(f"deepmd {pb_file.resolve()}") + lammps_real.pair_coeff("* *") + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + lammps_real.run(1) + + +def test_pair_deepmd_virial_real(lammps_real) -> None: + lammps_real.pair_style(f"deepmd {pb_file.resolve()}") + lammps_real.pair_coeff("* *") + lammps_real.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps_real.variable(f"virial{jj} atom c_virial[{ii + 1}]") + lammps_real.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + idx_map = lammps_real.lmp.numpy.extract_atom("id")[: coord.shape[0]] - 1 + for ii in range(9): + assert np.array( + lammps_real.variables[f"virial{ii}"].value + ) / constants.nktv2p_real == pytest.approx( + expected_v[idx_map, ii] * constants.ener_metal2real + ) + + +def test_pair_deepmd_si(lammps_si) -> None: + lammps_si.pair_style(f"deepmd {pb_file.resolve()}") + lammps_si.pair_coeff("* *") + lammps_si.run(0) + assert lammps_si.eval("pe") == pytest.approx(expected_e * constants.ener_metal2si) + for ii in range(6): + assert lammps_si.atoms[ii].force == pytest.approx( + expected_f[lammps_si.atoms[ii].id - 1] * constants.force_metal2si + ) + lammps_si.run(1) diff --git a/source/lmp/tests/test_lammps_dpa3_pt2_nopbc.py b/source/lmp/tests/test_lammps_dpa3_pt2_nopbc.py new file mode 100644 index 0000000000..95f208ff1b --- /dev/null +++ b/source/lmp/tests/test_lammps_dpa3_pt2_nopbc.py @@ -0,0 +1,317 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Test LAMMPS with .pt2 (AOTInductor) DPA3 model, non-periodic boundary. + +Mirrors test_lammps_dpa3_pt2.py but with boundary "f f f" (NoPbc). +Reference values from source/tests/infer/gen_dpa3.py / C++ test (NoPbc). +""" + +import os +from pathlib import ( + Path, +) + +import constants +import numpy as np +import pytest +from lammps import ( + PyLammps, +) +from write_lmp_data import ( + write_lmp_data, +) + +pb_file = Path(__file__).parent.parent.parent / "tests" / "infer" / "deeppot_dpa3.pt2" +data_file = Path(__file__).parent / "data_dpa3_pt2_nopbc.lmp" +data_file_si = Path(__file__).parent / "data_dpa3_pt2_nopbc.si" +data_type_map_file = Path(__file__).parent / "data_type_map_dpa3_pt2_nopbc.lmp" + +# Reference values from gen_dpa3.py / test_deeppot_dpa3_ptexpt.cc (NoPbc) +expected_ae = np.array( + [ + 2.748896667984845887e-01, + 2.803947322373078754e-01, + 2.865499847997139971e-01, + 2.695555136277474895e-01, + 2.739584531066059925e-01, + 2.752217127378932537e-01, + ] +) +expected_e = np.sum(expected_ae) +expected_f = np.array( + [ + -4.469562373941994571e-02, + 1.872384237732456838e-02, + 3.382371526226372882e-02, + 4.469562373941994571e-02, + -1.872384237732456838e-02, + -3.382371526226372882e-02, + -8.962417443747255821e-04, + 6.973117535150641388e-05, + 3.708588577163370883e-05, + 6.643516471939500678e-02, + -2.418189932122343649e-02, + 4.484243027251725439e-02, + 9.031619071676464522e-03, + 5.637239343551967569e-02, + -8.796029317613156262e-02, + -7.457054204669674724e-02, + -3.226022528964775371e-02, + 4.308077701784267244e-02, + ] +).reshape(6, 3) + +expected_v = -np.array( + [ + -1.634330450074628072e-02, + 6.846519453015231793e-03, + 1.236790610867266604e-02, + 6.846519453015259549e-03, + -2.868136527614494058e-03, + -5.181149856335852399e-03, + 1.236790610867266604e-02, + -5.181149856335859338e-03, + -9.359496514671244993e-03, + -1.673145706642453767e-02, + 7.009123906204950405e-03, + 1.266164318540249911e-02, + 7.009123906204922649e-03, + -2.936254609356120371e-03, + -5.304201874965906727e-03, + 1.266164318540247136e-02, + -5.304201874965899788e-03, + -9.581784032196449807e-03, + 2.483905957089865488e-03, + -1.710616363479115602e-04, + -5.347582359011894028e-05, + -1.996686279554130779e-04, + 1.446275632786597548e-05, + 2.638112328458543858e-06, + -1.197563523836930226e-04, + 1.205600575305949503e-05, + -4.593499883389132697e-06, + -4.089897480719173473e-02, + -3.495830205935246404e-03, + 1.154978330068986980e-03, + -3.627142383941225900e-03, + -1.488475129792680290e-02, + 2.311785022979555293e-02, + 1.347848716528848856e-03, + 2.315545736893441509e-02, + -3.642400982788428221e-02, + -1.119743233540158867e-03, + -7.327254171127076110e-03, + 1.144439607350029517e-02, + -1.403015516843159061e-03, + -1.349644754565121341e-02, + 2.117430870829728473e-02, + 2.103115217604090148e-03, + 2.047643373328661420e-02, + -3.212706064943796069e-02, + -2.418232649309504101e-02, + -1.012366394018440752e-02, + 1.334822742508814941e-02, + -1.588798342485496506e-02, + -6.330672283764562924e-03, + 8.295385033255518736e-03, + 2.256291842331806241e-02, + 8.946234975702738179e-03, + -1.170798305154926999e-02, + ] +).reshape(6, 9) + +box = np.array([0, 13, 0, 13, 0, 13, 0, 0, 0]) +coord = np.array( + [ + [12.83, 2.56, 2.18], + [12.09, 2.87, 2.74], + [0.25, 3.32, 1.68], + [3.36, 3.00, 1.81], + [3.51, 2.51, 2.60], + [4.27, 3.22, 1.56], + ] +) +type_OH = np.array([1, 2, 2, 1, 2, 2]) +type_HO = np.array([2, 1, 1, 2, 1, 1]) + + +def setup_module() -> None: + if os.environ.get("ENABLE_PYTORCH", "1") != "1": + pytest.skip( + "Skip test because PyTorch support is not enabled.", + ) + write_lmp_data(box, coord, type_OH, data_file) + write_lmp_data(box, coord, type_HO, data_type_map_file) + write_lmp_data( + box * constants.dist_metal2si, + coord * constants.dist_metal2si, + type_OH, + data_file_si, + ) + + +def teardown_module() -> None: + for f in [data_file, data_type_map_file, data_file_si]: + if f.exists(): + os.remove(f) + + +def _lammps(data_file, units="metal") -> PyLammps: + lammps = PyLammps() + lammps.units(units) + lammps.boundary("f f f") + lammps.atom_style("atomic") + lammps.atom_modify("map yes") + if units == "metal" or units == "real": + lammps.neighbor("2.0 bin") + elif units == "si": + lammps.neighbor("2.0e-10 bin") + else: + raise ValueError("units should be metal, real, or si") + lammps.neigh_modify("every 10 delay 0 check no") + lammps.read_data(data_file.resolve()) + if units == "metal" or units == "real": + lammps.mass("1 16") + lammps.mass("2 2") + elif units == "si": + lammps.mass("1 %.10e" % (16 * constants.mass_metal2si)) + lammps.mass("2 %.10e" % (2 * constants.mass_metal2si)) + else: + raise ValueError("units should be metal, real, or si") + if units == "metal": + lammps.timestep(0.0005) + elif units == "real": + lammps.timestep(0.5) + elif units == "si": + lammps.timestep(5e-16) + else: + raise ValueError("units should be metal, real, or si") + lammps.fix("1 all nve") + return lammps + + +@pytest.fixture +def lammps(): + lmp = _lammps(data_file=data_file) + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_type_map(): + lmp = _lammps(data_file=data_type_map_file) + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_real(): + lmp = _lammps(data_file=data_file, units="real") + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_si(): + lmp = _lammps(data_file=data_file_si, units="si") + yield lmp + lmp.close() + + +def test_pair_deepmd(lammps) -> None: + lammps.pair_style(f"deepmd {pb_file.resolve()}") + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + lammps.run(1) + + +def test_pair_deepmd_virial(lammps) -> None: + lammps.pair_style(f"deepmd {pb_file.resolve()}") + lammps.pair_coeff("* *") + lammps.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps.variable(f"virial{jj} atom c_virial[{ii + 1}]") + lammps.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + idx_map = lammps.lmp.numpy.extract_atom("id")[: coord.shape[0]] - 1 + for ii in range(9): + assert np.array( + lammps.variables[f"virial{ii}"].value + ) / constants.nktv2p == pytest.approx(expected_v[idx_map, ii]) + + +def test_pair_deepmd_type_map(lammps_type_map) -> None: + lammps_type_map.pair_style(f"deepmd {pb_file.resolve()}") + lammps_type_map.pair_coeff("* * H O") + lammps_type_map.run(0) + assert lammps_type_map.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps_type_map.atoms[ii].force == pytest.approx( + expected_f[lammps_type_map.atoms[ii].id - 1] + ) + lammps_type_map.run(1) + + +def test_pair_deepmd_real(lammps_real) -> None: + lammps_real.pair_style(f"deepmd {pb_file.resolve()}") + lammps_real.pair_coeff("* *") + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + lammps_real.run(1) + + +def test_pair_deepmd_virial_real(lammps_real) -> None: + lammps_real.pair_style(f"deepmd {pb_file.resolve()}") + lammps_real.pair_coeff("* *") + lammps_real.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps_real.variable(f"virial{jj} atom c_virial[{ii + 1}]") + lammps_real.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + idx_map = lammps_real.lmp.numpy.extract_atom("id")[: coord.shape[0]] - 1 + for ii in range(9): + assert np.array( + lammps_real.variables[f"virial{ii}"].value + ) / constants.nktv2p_real == pytest.approx( + expected_v[idx_map, ii] * constants.ener_metal2real + ) + + +def test_pair_deepmd_si(lammps_si) -> None: + lammps_si.pair_style(f"deepmd {pb_file.resolve()}") + lammps_si.pair_coeff("* *") + lammps_si.run(0) + assert lammps_si.eval("pe") == pytest.approx(expected_e * constants.ener_metal2si) + for ii in range(6): + assert lammps_si.atoms[ii].force == pytest.approx( + expected_f[lammps_si.atoms[ii].id - 1] * constants.force_metal2si + ) + lammps_si.run(1) diff --git a/source/lmp/tests/test_lammps_faparam_pt2.py b/source/lmp/tests/test_lammps_faparam_pt2.py new file mode 100644 index 0000000000..b46c8c96fb --- /dev/null +++ b/source/lmp/tests/test_lammps_faparam_pt2.py @@ -0,0 +1,153 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Test LAMMPS with default_fparam (.pt2 AOTInductor backend).""" + +import os +from pathlib import ( + Path, +) + +import numpy as np +import pytest +from lammps import ( + PyLammps, +) +from write_lmp_data import ( + write_lmp_data, +) + +pt2_file = ( + Path(__file__).parent.parent.parent + / "tests" + / "infer" + / "fparam_aparam_default.pt2" +) +pt2_file_no_default = ( + Path(__file__).parent.parent.parent / "tests" / "infer" / "fparam_aparam.pt2" +) +data_file = Path(__file__).parent / "data.lmp" + +# expected values from fparam_aparam_default.pt2 with default_fparam=[0.25852028] +# (identical to .pth — generated from the same dpmodel checkpoint via gen_fparam_aparam.py) +expected_ae = np.array( + [ + -1.038271223729637e-01, + -7.285433579124989e-02, + -9.467600492266426e-02, + -1.467050207422953e-01, + -7.660561676973243e-02, + -7.277296000253175e-02, + ] +) +expected_e = np.sum(expected_ae) +expected_f = np.array( + [ + 6.622266941151356e-02, + 5.278739714221517e-02, + 2.265728009692279e-02, + -2.606048291367521e-02, + -4.538812303131843e-02, + 1.058247419681242e-02, + 1.679392617013225e-01, + -2.257826240741907e-03, + -4.490146347357200e-02, + -1.148364179422036e-01, + -1.169790528013792e-02, + 6.140403441496690e-02, + -8.078778123309406e-02, + -5.838879041789346e-02, + 6.773641084621368e-02, + -1.247724902386317e-02, + 6.494524782787654e-02, + -1.174787360813438e-01, + ] +).reshape(6, 3) + +box = np.array([0, 13, 0, 13, 0, 13, 0, 0, 0]) +coord = np.array( + [ + [12.83, 2.56, 2.18], + [12.09, 2.87, 2.74], + [0.25, 3.32, 1.68], + [3.36, 3.00, 1.81], + [3.51, 2.51, 2.60], + [4.27, 3.22, 1.56], + ] +) +type_OH = np.array([1, 1, 1, 1, 1, 1]) + + +def setup_module() -> None: + if os.environ.get("ENABLE_PYTORCH", "1") != "1": + pytest.skip( + "Skip test because PyTorch support is not enabled.", + ) + write_lmp_data(box, coord, type_OH, data_file) + + +def teardown_module() -> None: + if data_file.exists(): + os.remove(data_file) + + +def _lammps(data_file, units="metal") -> PyLammps: + lammps = PyLammps() + lammps.units(units) + lammps.boundary("p p p") + lammps.atom_style("atomic") + lammps.neighbor("2.0 bin") + lammps.neigh_modify("every 10 delay 0 check no") + lammps.read_data(data_file.resolve()) + lammps.mass("1 16") + lammps.timestep(0.0005) + lammps.fix("1 all nve") + return lammps + + +@pytest.fixture +def lammps(): + lmp = _lammps(data_file=data_file) + yield lmp + lmp.close() + + +def test_pair_deepmd_default_fparam(lammps) -> None: + """Test that .pt2 model with default_fparam works without providing fparam.""" + lammps.pair_style(f"deepmd {pt2_file.resolve()} aparam 0.25852028") + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + lammps.run(1) + + +def test_pair_deepmd_default_fparam_explicit(lammps) -> None: + """Test that explicit fparam still works with .pt2 default_fparam model.""" + lammps.pair_style( + f"deepmd {pt2_file.resolve()} fparam 0.25852028 aparam 0.25852028" + ) + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + lammps.run(1) + + +def test_pair_deepmd_fparam_aparam(lammps) -> None: + """Test .pt2 model without default_fparam (explicit fparam required).""" + lammps.pair_style( + f"deepmd {pt2_file_no_default.resolve()} fparam 0.25852028 aparam 0.25852028" + ) + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + lammps.run(1) diff --git a/source/lmp/tests/test_lammps_model_devi_pt2.py b/source/lmp/tests/test_lammps_model_devi_pt2.py new file mode 100644 index 0000000000..e802182e85 --- /dev/null +++ b/source/lmp/tests/test_lammps_model_devi_pt2.py @@ -0,0 +1,467 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Test LAMMPS model deviation with .pt2 (AOTInductor) models. + +Uses model_devi_md0.pt2 and model_devi_md1.pt2 (two SE(A) models with different +seeds, fparam+aparam+default_fparam). Expected values are computed dynamically +from Python DeepPot inference so they track model regeneration automatically. +""" + +import os +from pathlib import ( + Path, +) + +import constants +import numpy as np +import pytest +from lammps import ( + PyLammps, +) +from write_lmp_data import ( + write_lmp_data, +) + +pt2_file0 = ( + Path(__file__).parent.parent.parent / "tests" / "infer" / "model_devi_md0.pt2" +) +pt2_file1 = ( + Path(__file__).parent.parent.parent / "tests" / "infer" / "model_devi_md1.pt2" +) +data_file = Path(__file__).parent / "data_model_devi_pt2.lmp" +md_file = Path(__file__).parent / "md_pt2.out" + +# Coordinates and types — same coords as gen_model_devi.py +# type_map=["O"], so all atoms are type 1 in LAMMPS (1-indexed) +box = np.array([0, 13, 0, 13, 0, 13, 0, 0, 0]) +coord = np.array( + [ + [12.83, 2.56, 2.18], + [12.09, 2.87, 2.74], + [0.25, 3.32, 1.68], + [3.36, 3.00, 1.81], + [3.51, 2.51, 2.60], + [4.27, 3.22, 1.56], + ] +) +type_all = np.array([1, 1, 1, 1, 1, 1]) + +# fparam/aparam for the model_devi models (numb_fparam=1, numb_aparam=1) +fparam_val = 0.25852028 +aparam_val = 0.25852028 + +# Expected values are computed at module load from Python DeepPot inference. +expected_e = None +expected_f = None +expected_v = None +expected_f2 = None +expected_v2 = None + + +def _compute_expected(): + """Load both .pt2 models via DeepPot and compute reference values. + + Runs in a subprocess to avoid importing ``deepmd`` in the LAMMPS test + process — the LAMMPS plugin already loads ``libdeepmd_op_pt.so`` at the + C++ level, and importing the Python package on top of that can segfault + (e.g. ``hashlib`` native extension crashes due to library state conflicts). + """ + global expected_e, expected_f, expected_v, expected_f2, expected_v2 + + import json + import subprocess + import sys + import textwrap + + script = textwrap.dedent(f"""\ + import json, numpy as np + from deepmd.infer import DeepPot + + coord = {coord.tolist()!r} + box_9 = [13.0, 0.0, 0.0, 0.0, 13.0, 0.0, 0.0, 0.0, 13.0] + atype = [0, 0, 0, 0, 0, 0] + fp = [{fparam_val}] + ap = [{aparam_val}] * 6 + + results = [] + for pt2_path in [{str(pt2_file0)!r}, {str(pt2_file1)!r}]: + dp = DeepPot(pt2_path) + e, f, v, ae, av = dp.eval( + np.array(coord).reshape(1, -1), np.array(box_9), + atype, fparam=np.array(fp), aparam=np.array(ap), atomic=True, + ) + results.append({{"e": e[0, 0], "f": f[0].tolist(), "av": av[0].tolist()}}) + print(json.dumps(results)) + """) + proc = subprocess.run( + [sys.executable, "-c", script], + capture_output=True, + text=True, + ) + if proc.returncode != 0: + raise RuntimeError(f"Failed to compute expected values:\n{proc.stderr}") + results = json.loads(proc.stdout.strip()) + expected_e = results[0]["e"] + expected_f = np.array(results[0]["f"]) + # DeepPot returns virial; LAMMPS centroid/stress/atom returns stress = -virial + expected_v = -np.array(results[0]["av"]) + expected_f2 = np.array(results[1]["f"]) + expected_v2 = -np.array(results[1]["av"]) + + +def setup_module() -> None: + if not pt2_file0.exists() or not pt2_file1.exists(): + pytest.skip("model_devi_md0.pt2 / model_devi_md1.pt2 not found") + _compute_expected() + write_lmp_data(box, coord, type_all, data_file) + + +def teardown_module() -> None: + if data_file.exists(): + os.remove(data_file) + + +def _lammps(data_file_path=data_file, units="metal") -> PyLammps: + lammps = PyLammps() + lammps.units(units) + lammps.boundary("p p p") + lammps.atom_style("atomic") + lammps.neighbor("2.0 bin") + lammps.neigh_modify("every 10 delay 0 check no") + lammps.read_data(data_file_path.resolve()) + lammps.mass("1 16") + if units == "metal": + lammps.timestep(0.0005) + elif units == "real": + lammps.timestep(0.5) + lammps.fix("1 all nve") + return lammps + + +@pytest.fixture() +def lammps(): + lmp = _lammps() + yield lmp + lmp.close() + + +@pytest.fixture() +def lammps_real(): + lmp = _lammps(units="real") + yield lmp + lmp.close() + + +def test_pair_deepmd_model_devi(lammps) -> None: + lammps.pair_style( + f"deepmd {pt2_file0.resolve()} {pt2_file1.resolve()} " + f"out_file {md_file.resolve()} out_freq 1 atomic " + f"fparam {fparam_val} aparam {aparam_val}" + ) + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f) + assert md[4] == pytest.approx(np.max(expected_md_f)) + assert md[5] == pytest.approx(np.min(expected_md_f)) + assert md[6] == pytest.approx(np.mean(expected_md_f)) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v)) + assert md[2] == pytest.approx(np.min(expected_md_v)) + assert md[3] == pytest.approx(np.sqrt(np.mean(np.square(expected_md_v)))) + + +def test_pair_deepmd_model_devi_default_fparam(lammps) -> None: + """Model devi with default_fparam — fparam omitted, aparam explicit.""" + lammps.pair_style( + f"deepmd {pt2_file0.resolve()} {pt2_file1.resolve()} " + f"out_file {md_file.resolve()} out_freq 1 atomic " + f"aparam {aparam_val}" + ) + lammps.pair_coeff("* *") + lammps.run(0) + # default_fparam == fparam_val, so results should match explicit fparam + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + # model devi should also match + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f) + assert md[4] == pytest.approx(np.max(expected_md_f)) + assert md[5] == pytest.approx(np.min(expected_md_f)) + assert md[6] == pytest.approx(np.mean(expected_md_f)) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v)) + assert md[2] == pytest.approx(np.min(expected_md_v)) + assert md[3] == pytest.approx(np.sqrt(np.mean(np.square(expected_md_v)))) + + +def test_pair_deepmd_model_devi_virial(lammps) -> None: + lammps.pair_style( + f"deepmd {pt2_file0.resolve()} {pt2_file1.resolve()} " + f"out_file {md_file.resolve()} out_freq 1 atomic " + f"fparam {fparam_val} aparam {aparam_val}" + ) + lammps.pair_coeff("* *") + lammps.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps.variable(f"virial{jj} atom c_virial[{ii + 1}]") + lammps.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + idx_map = lammps.lmp.numpy.extract_atom("id")[: coord.shape[0]] - 1 + for ii in range(9): + assert np.array( + lammps.variables[f"virial{ii}"].value + ) / constants.nktv2p == pytest.approx(expected_v[idx_map, ii]) + # load model devi + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f) + assert md[4] == pytest.approx(np.max(expected_md_f)) + assert md[5] == pytest.approx(np.min(expected_md_f)) + assert md[6] == pytest.approx(np.mean(expected_md_f)) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v)) + assert md[2] == pytest.approx(np.min(expected_md_v)) + assert md[3] == pytest.approx(np.sqrt(np.mean(np.square(expected_md_v)))) + + +def test_pair_deepmd_model_devi_atomic_relative(lammps) -> None: + relative = 1.0 + lammps.pair_style( + f"deepmd {pt2_file0.resolve()} {pt2_file1.resolve()} " + f"out_file {md_file.resolve()} out_freq 1 atomic relative {relative} " + f"fparam {fparam_val} aparam {aparam_val}" + ) + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + norm = np.linalg.norm(np.mean([expected_f, expected_f2], axis=0), axis=1) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + expected_md_f /= norm + relative + assert md[7:] == pytest.approx(expected_md_f) + assert md[4] == pytest.approx(np.max(expected_md_f)) + assert md[5] == pytest.approx(np.min(expected_md_f)) + assert md[6] == pytest.approx(np.mean(expected_md_f)) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v)) + assert md[2] == pytest.approx(np.min(expected_md_v)) + assert md[3] == pytest.approx(np.sqrt(np.mean(np.square(expected_md_v)))) + + +def test_pair_deepmd_model_devi_atomic_relative_v(lammps) -> None: + relative = 1.0 + lammps.pair_style( + f"deepmd {pt2_file0.resolve()} {pt2_file1.resolve()} " + f"out_file {md_file.resolve()} out_freq 1 atomic relative_v {relative} " + f"fparam {fparam_val} aparam {aparam_val}" + ) + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f) + assert md[4] == pytest.approx(np.max(expected_md_f)) + assert md[5] == pytest.approx(np.min(expected_md_f)) + assert md[6] == pytest.approx(np.mean(expected_md_f)) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + norm = ( + np.abs( + np.mean([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) + ) + / 6 + ) + expected_md_v /= norm + relative + assert md[1] == pytest.approx(np.max(expected_md_v)) + assert md[2] == pytest.approx(np.min(expected_md_v)) + assert md[3] == pytest.approx(np.sqrt(np.mean(np.square(expected_md_v)))) + + +def test_pair_deepmd_model_devi_real(lammps_real) -> None: + lammps_real.pair_style( + f"deepmd {pt2_file0.resolve()} {pt2_file1.resolve()} " + f"out_file {md_file.resolve()} out_freq 1 atomic " + f"fparam {fparam_val} aparam {aparam_val}" + ) + lammps_real.pair_coeff("* *") + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f * constants.force_metal2real) + assert md[4] == pytest.approx(np.max(expected_md_f) * constants.force_metal2real) + assert md[5] == pytest.approx(np.min(expected_md_f) * constants.force_metal2real) + assert md[6] == pytest.approx(np.mean(expected_md_f) * constants.force_metal2real) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v) * constants.ener_metal2real) + assert md[2] == pytest.approx(np.min(expected_md_v) * constants.ener_metal2real) + assert md[3] == pytest.approx( + np.sqrt(np.mean(np.square(expected_md_v))) * constants.ener_metal2real + ) + + +def test_pair_deepmd_model_devi_virial_real(lammps_real) -> None: + lammps_real.pair_style( + f"deepmd {pt2_file0.resolve()} {pt2_file1.resolve()} " + f"out_file {md_file.resolve()} out_freq 1 atomic " + f"fparam {fparam_val} aparam {aparam_val}" + ) + lammps_real.pair_coeff("* *") + lammps_real.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps_real.variable(f"virial{jj} atom c_virial[{ii + 1}]") + lammps_real.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + idx_map = lammps_real.lmp.numpy.extract_atom("id")[: coord.shape[0]] - 1 + for ii in range(9): + assert np.array( + lammps_real.variables[f"virial{ii}"].value + ) / constants.nktv2p_real == pytest.approx( + expected_v[idx_map, ii] * constants.ener_metal2real + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f * constants.force_metal2real) + assert md[4] == pytest.approx(np.max(expected_md_f) * constants.force_metal2real) + assert md[5] == pytest.approx(np.min(expected_md_f) * constants.force_metal2real) + assert md[6] == pytest.approx(np.mean(expected_md_f) * constants.force_metal2real) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v) * constants.ener_metal2real) + assert md[2] == pytest.approx(np.min(expected_md_v) * constants.ener_metal2real) + assert md[3] == pytest.approx( + np.sqrt(np.mean(np.square(expected_md_v))) * constants.ener_metal2real + ) + + +def test_pair_deepmd_model_devi_atomic_relative_real(lammps_real) -> None: + relative = 1.0 + lammps_real.pair_style( + f"deepmd {pt2_file0.resolve()} {pt2_file1.resolve()} " + f"out_file {md_file.resolve()} out_freq 1 atomic relative {relative * constants.force_metal2real} " + f"fparam {fparam_val} aparam {aparam_val}" + ) + lammps_real.pair_coeff("* *") + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + # load model devi + md = np.loadtxt(md_file.resolve()) + norm = np.linalg.norm(np.mean([expected_f, expected_f2], axis=0), axis=1) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + expected_md_f /= norm + relative + assert md[7:] == pytest.approx(expected_md_f * constants.force_metal2real) + assert md[4] == pytest.approx(np.max(expected_md_f) * constants.force_metal2real) + assert md[5] == pytest.approx(np.min(expected_md_f) * constants.force_metal2real) + assert md[6] == pytest.approx(np.mean(expected_md_f) * constants.force_metal2real) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + assert md[1] == pytest.approx(np.max(expected_md_v) * constants.ener_metal2real) + assert md[2] == pytest.approx(np.min(expected_md_v) * constants.ener_metal2real) + assert md[3] == pytest.approx( + np.sqrt(np.mean(np.square(expected_md_v))) * constants.ener_metal2real + ) + + +def test_pair_deepmd_model_devi_atomic_relative_v_real(lammps_real) -> None: + relative = 1.0 + lammps_real.pair_style( + f"deepmd {pt2_file0.resolve()} {pt2_file1.resolve()} " + f"out_file {md_file.resolve()} out_freq 1 atomic relative_v {relative * constants.ener_metal2real} " + f"fparam {fparam_val} aparam {aparam_val}" + ) + lammps_real.pair_coeff("* *") + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + md = np.loadtxt(md_file.resolve()) + expected_md_f = np.linalg.norm(np.std([expected_f, expected_f2], axis=0), axis=1) + assert md[7:] == pytest.approx(expected_md_f * constants.force_metal2real) + assert md[4] == pytest.approx(np.max(expected_md_f) * constants.force_metal2real) + assert md[5] == pytest.approx(np.min(expected_md_f) * constants.force_metal2real) + assert md[6] == pytest.approx(np.mean(expected_md_f) * constants.force_metal2real) + expected_md_v = ( + np.std([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) / 6 + ) + norm = ( + np.abs( + np.mean([np.sum(expected_v, axis=0), np.sum(expected_v2, axis=0)], axis=0) + ) + / 6 + ) + expected_md_v /= norm + relative + assert md[1] == pytest.approx(np.max(expected_md_v) * constants.ener_metal2real) + assert md[2] == pytest.approx(np.min(expected_md_v) * constants.ener_metal2real) + assert md[3] == pytest.approx( + np.sqrt(np.mean(np.square(expected_md_v))) * constants.ener_metal2real + ) diff --git a/source/lmp/tests/test_lammps_pt2.py b/source/lmp/tests/test_lammps_pt2.py new file mode 100644 index 0000000000..a800e81838 --- /dev/null +++ b/source/lmp/tests/test_lammps_pt2.py @@ -0,0 +1,316 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Test LAMMPS with .pt2 (AOTInductor) se_e2_a model. + +Subset of test_lammps_pt.py tests for basic energy/force/virial evaluation. +Model deviation tests are in test_lammps_model_devi_pt2.py. +""" + +import os +from pathlib import ( + Path, +) + +import constants +import numpy as np +import pytest +from lammps import ( + PyLammps, +) +from write_lmp_data import ( + write_lmp_data, +) + +pb_file = Path(__file__).parent.parent.parent / "tests" / "infer" / "deeppot_sea.pt2" +data_file = Path(__file__).parent / "data_pt2.lmp" +data_file_si = Path(__file__).parent / "data_pt2.si" +data_type_map_file = Path(__file__).parent / "data_type_map_pt2.lmp" + +# this is as the same as python and c++ tests, test_deeppot_a.py +expected_ae = np.array( + [ + -93.016873944029, + -185.923296645958, + -185.927096544970, + -93.019371018039, + -185.926179995548, + -185.924351901852, + ] +) +expected_e = np.sum(expected_ae) +expected_f = np.array( + [ + 0.006277522211, + -0.001117962774, + 0.000618580445, + 0.009928999655, + 0.003026035654, + -0.006941982227, + 0.000667853212, + -0.002449963843, + 0.006506463508, + -0.007284129115, + 0.000530662205, + -0.000028806821, + 0.000068097781, + 0.006121331983, + -0.009019754602, + -0.009658343745, + -0.006110103225, + 0.008865499697, + ] +).reshape(6, 3) + +expected_v = -np.array( + [ + -0.000155238009, + 0.000116605516, + -0.007869862476, + 0.000465578340, + 0.008182547185, + -0.002398713212, + -0.008112887338, + -0.002423738425, + 0.007210716605, + -0.019203504012, + 0.001724938709, + 0.009909211091, + 0.001153857542, + -0.001600015103, + -0.000560024090, + 0.010727836276, + -0.001034836404, + -0.007973454377, + -0.021517399106, + -0.004064359664, + 0.004866398692, + -0.003360038617, + -0.007241406162, + 0.005920941051, + 0.004899151657, + 0.006290788591, + -0.006478820311, + 0.001921504710, + 0.001313470921, + -0.000304091236, + 0.001684345981, + 0.004124109256, + -0.006396084465, + -0.000701095618, + -0.006356507032, + 0.009818550859, + -0.015230664587, + -0.000110244376, + 0.000690319396, + 0.000045953023, + -0.005726548770, + 0.008769818495, + -0.000572380210, + 0.008860603423, + -0.013819348050, + -0.021227082558, + -0.004977781343, + 0.006646239696, + -0.005987066507, + -0.002767831232, + 0.003746502525, + 0.007697590397, + 0.003746130152, + -0.005172634748, + ] +).reshape(6, 9) + +box = np.array([0, 13, 0, 13, 0, 13, 0, 0, 0]) +coord = np.array( + [ + [12.83, 2.56, 2.18], + [12.09, 2.87, 2.74], + [0.25, 3.32, 1.68], + [3.36, 3.00, 1.81], + [3.51, 2.51, 2.60], + [4.27, 3.22, 1.56], + ] +) +type_OH = np.array([1, 2, 2, 1, 2, 2]) +type_HO = np.array([2, 1, 1, 2, 1, 1]) + + +def setup_module() -> None: + if os.environ.get("ENABLE_PYTORCH", "1") != "1": + pytest.skip( + "Skip test because PyTorch support is not enabled.", + ) + write_lmp_data(box, coord, type_OH, data_file) + write_lmp_data(box, coord, type_HO, data_type_map_file) + write_lmp_data( + box * constants.dist_metal2si, + coord * constants.dist_metal2si, + type_OH, + data_file_si, + ) + + +def teardown_module() -> None: + for f in [data_file, data_type_map_file, data_file_si]: + if f.exists(): + os.remove(f) + + +def _lammps(data_file, units="metal") -> PyLammps: + lammps = PyLammps() + lammps.units(units) + lammps.boundary("p p p") + lammps.atom_style("atomic") + if units == "metal" or units == "real": + lammps.neighbor("2.0 bin") + elif units == "si": + lammps.neighbor("2.0e-10 bin") + else: + raise ValueError("units should be metal, real, or si") + lammps.neigh_modify("every 10 delay 0 check no") + lammps.read_data(data_file.resolve()) + if units == "metal" or units == "real": + lammps.mass("1 16") + lammps.mass("2 2") + elif units == "si": + lammps.mass("1 %.10e" % (16 * constants.mass_metal2si)) + lammps.mass("2 %.10e" % (2 * constants.mass_metal2si)) + else: + raise ValueError("units should be metal, real, or si") + if units == "metal": + lammps.timestep(0.0005) + elif units == "real": + lammps.timestep(0.5) + elif units == "si": + lammps.timestep(5e-16) + else: + raise ValueError("units should be metal, real, or si") + lammps.fix("1 all nve") + return lammps + + +@pytest.fixture +def lammps(): + lmp = _lammps(data_file=data_file) + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_type_map(): + lmp = _lammps(data_file=data_type_map_file) + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_real(): + lmp = _lammps(data_file=data_file, units="real") + yield lmp + lmp.close() + + +@pytest.fixture +def lammps_si(): + lmp = _lammps(data_file=data_file_si, units="si") + yield lmp + lmp.close() + + +def test_pair_deepmd(lammps) -> None: + lammps.pair_style(f"deepmd {pb_file.resolve()}") + lammps.pair_coeff("* *") + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + lammps.run(1) + + +def test_pair_deepmd_virial(lammps) -> None: + lammps.pair_style(f"deepmd {pb_file.resolve()}") + lammps.pair_coeff("* *") + lammps.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps.variable(f"virial{jj} atom c_virial[{ii + 1}]") + lammps.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps.run(0) + assert lammps.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps.atoms[ii].force == pytest.approx( + expected_f[lammps.atoms[ii].id - 1] + ) + idx_map = lammps.lmp.numpy.extract_atom("id")[: coord.shape[0]] - 1 + for ii in range(9): + assert np.array( + lammps.variables[f"virial{ii}"].value + ) / constants.nktv2p == pytest.approx(expected_v[idx_map, ii]) + + +def test_pair_deepmd_type_map(lammps_type_map) -> None: + lammps_type_map.pair_style(f"deepmd {pb_file.resolve()}") + lammps_type_map.pair_coeff("* * H O") + lammps_type_map.run(0) + assert lammps_type_map.eval("pe") == pytest.approx(expected_e) + for ii in range(6): + assert lammps_type_map.atoms[ii].force == pytest.approx( + expected_f[lammps_type_map.atoms[ii].id - 1] + ) + lammps_type_map.run(1) + + +def test_pair_deepmd_real(lammps_real) -> None: + lammps_real.pair_style(f"deepmd {pb_file.resolve()}") + lammps_real.pair_coeff("* *") + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + lammps_real.run(1) + + +def test_pair_deepmd_virial_real(lammps_real) -> None: + lammps_real.pair_style(f"deepmd {pb_file.resolve()}") + lammps_real.pair_coeff("* *") + lammps_real.compute("virial all centroid/stress/atom NULL pair") + for ii in range(9): + jj = [0, 4, 8, 3, 6, 7, 1, 2, 5][ii] + lammps_real.variable(f"virial{jj} atom c_virial[{ii + 1}]") + lammps_real.dump( + "1 all custom 1 dump id " + " ".join([f"v_virial{ii}" for ii in range(9)]) + ) + lammps_real.run(0) + assert lammps_real.eval("pe") == pytest.approx( + expected_e * constants.ener_metal2real + ) + for ii in range(6): + assert lammps_real.atoms[ii].force == pytest.approx( + expected_f[lammps_real.atoms[ii].id - 1] * constants.force_metal2real + ) + idx_map = lammps_real.lmp.numpy.extract_atom("id")[: coord.shape[0]] - 1 + for ii in range(9): + assert np.array( + lammps_real.variables[f"virial{ii}"].value + ) / constants.nktv2p_real == pytest.approx( + expected_v[idx_map, ii] * constants.ener_metal2real + ) + + +def test_pair_deepmd_si(lammps_si) -> None: + lammps_si.pair_style(f"deepmd {pb_file.resolve()}") + lammps_si.pair_coeff("* *") + lammps_si.run(0) + assert lammps_si.eval("pe") == pytest.approx(expected_e * constants.ener_metal2si) + for ii in range(6): + assert lammps_si.atoms[ii].force == pytest.approx( + expected_f[lammps_si.atoms[ii].id - 1] * constants.force_metal2si + ) + lammps_si.run(1) diff --git a/source/tests/infer/fparam_aparam_default.pth b/source/tests/infer/fparam_aparam_default.pth index b28adf574c..0cfff737da 100644 Binary files a/source/tests/infer/fparam_aparam_default.pth and b/source/tests/infer/fparam_aparam_default.pth differ diff --git a/source/tests/infer/gen_fparam_aparam.py b/source/tests/infer/gen_fparam_aparam.py index 7ac032942e..f0d80d5764 100644 --- a/source/tests/infer/gen_fparam_aparam.py +++ b/source/tests/infer/gen_fparam_aparam.py @@ -1,13 +1,17 @@ #!/usr/bin/env python3 # SPDX-License-Identifier: LGPL-3.0-or-later -"""Generate fparam_aparam.pth and fparam_aparam.pt2 test models. +"""Generate fparam_aparam .pt2 test models from pre-committed .pth. -Creates a dpmodel model from config with type_one_side=True, serializes, -and exports to both .pth and .pt2 from the same weights. +Converts the pre-committed fparam_aparam_default.pth (checked into git) to +.pt2 format. Produces: + - fparam_aparam_default.pt2: model WITH default_fparam + - fparam_aparam.pt2: same weights WITHOUT default_fparam + - fparam_aparam.pth: same weights WITHOUT default_fparam (torch.jit) Also prints reference values for C++ tests. """ import copy +import json import os import sys @@ -23,108 +27,76 @@ def main(): - from deepmd.dpmodel.model.model import ( + import torch + + from deepmd.pt.model.model import ( get_model, ) + from deepmd.pt.utils.serialization import ( + deserialize_to_file as pt_deserialize_to_file, + ) + from deepmd.pt_expt.utils.serialization import ( + deserialize_to_file as pt_expt_deserialize_to_file, + ) ensure_inductor_compiler() + load_custom_ops() - # ---- 1. Model config (type_one_side=True) ---- - config = { - "type_map": ["O"], - "descriptor": { - "type": "se_e2_a", - "sel": [60], - "rcut": 6.0, - "rcut_smth": 1.8, - "neuron": [5, 10, 20], - "axis_neuron": 8, - "activation_function": "tanh", - "resnet_dt": False, - "type_one_side": True, - "exclude_types": [], - "set_davg_zero": False, - "precision": "default", - "trainable": True, - "seed": 1, - }, - "fitting_net": { - "type": "ener", - "neuron": [5, 5, 5], - "activation_function": "tanh", - "resnet_dt": True, - "numb_fparam": 1, - "numb_aparam": 1, - "precision": "default", - "seed": 1, - "atom_ener": [], - "rcond": 0.001, - "trainable": True, - "use_aparam_as_mask": False, - }, - } + base_dir = os.path.dirname(__file__) - # ---- 2. Build dpmodel and serialize ---- - model = get_model(copy.deepcopy(config)) + # ---- 1. Load pre-committed .pth (has type_one_side=False) ---- + pth_default_path = os.path.join(base_dir, "fparam_aparam_default.pth") + print(f"Loading {pth_default_path} ...") # noqa: T201 + saved = torch.jit.load(pth_default_path, map_location="cpu") + config = json.loads(saved.model_def_script) + # The pre-committed .pth has type_one_side=False, which is incompatible + # with torch.export. Since this model has only 1 atom type, flipping to + # type_one_side=True produces identical results (single embedding net). + config["descriptor"]["type_one_side"] = True + model = get_model(config) + model.load_state_dict(saved.state_dict(), strict=False) model_dict = model.serialize() - data = { + # ---- 2. Export fparam_aparam_default.pt2 (with default_fparam) ---- + data_default = { "model": model_dict, "model_def_script": config, - "backend": "dpmodel", + "backend": "PyTorch", "software": "deepmd-kit", "version": "3.0.0", } + pt2_default_path = os.path.join(base_dir, "fparam_aparam_default.pt2") + print(f"Exporting to {pt2_default_path} ...") # noqa: T201 + pt_expt_deserialize_to_file(pt2_default_path, copy.deepcopy(data_default)) - # ---- 3. Export to .pt2 and .pth ---- - from deepmd.pt.utils.serialization import ( - deserialize_to_file as pt_deserialize_to_file, - ) - from deepmd.pt_expt.utils.serialization import ( - deserialize_to_file as pt_expt_deserialize_to_file, - ) - - # Load custom ops after deepmd.pt import to avoid double registration - load_custom_ops() - - base_dir = os.path.dirname(__file__) + # ---- 3. Export fparam_aparam.pt2 and .pth (without default_fparam) ---- + config_no_default = copy.deepcopy(config) + config_no_default["fitting_net"].pop("default_fparam", None) + model_no_default = get_model(config_no_default) + model_no_default.load_state_dict(saved.state_dict(), strict=False) + data_no_default = { + "model": model_no_default.serialize(), + "model_def_script": config_no_default, + "backend": "PyTorch", + "software": "deepmd-kit", + "version": "3.0.0", + } pt2_path = os.path.join(base_dir, "fparam_aparam.pt2") print(f"Exporting to {pt2_path} ...") # noqa: T201 - pt_expt_deserialize_to_file(pt2_path, copy.deepcopy(data)) + pt_expt_deserialize_to_file(pt2_path, copy.deepcopy(data_no_default)) pth_path = os.path.join(base_dir, "fparam_aparam.pth") pth_exported = False print(f"Exporting to {pth_path} ...") # noqa: T201 try: - pt_deserialize_to_file(pth_path, copy.deepcopy(data)) + pt_deserialize_to_file(pth_path, copy.deepcopy(data_no_default)) pth_exported = True except RuntimeError as e: - # Custom ops (e.g. tabulate_fusion_se_t_tebd) may not be available - # in all build environments; .pth generation is not critical. print(f"WARNING: .pth export failed ({e}), skipping.") # noqa: T201 print("Export done.") # noqa: T201 - # ---- 3b. Export a model with default_fparam to .pt2 ---- - config_default = copy.deepcopy(config) - config_default["fitting_net"]["default_fparam"] = [0.25852028] - model_default = get_model(config_default) - data_default = { - "model": model_default.serialize(), - "model_def_script": config_default, - "backend": "dpmodel", - "software": "deepmd-kit", - "version": "3.0.0", - } - pt2_default_path = os.path.join(base_dir, "fparam_aparam_default.pt2") - print(f"Exporting to {pt2_default_path} ...") # noqa: T201 - pt_expt_deserialize_to_file(pt2_default_path, copy.deepcopy(data_default)) - - # NOTE: fparam_aparam_default.pth is committed to git (generated by PR #5311), - # NOT regenerated here — it has its own expected values in - # test_deeppot_default_fparam_pt.cc. - # ---- 4. Run inference via DeepPot to get reference values ---- from deepmd.infer import ( DeepPot, diff --git a/source/tests/infer/gen_model_devi.py b/source/tests/infer/gen_model_devi.py index cb8dbf7d97..bdceaa05aa 100644 --- a/source/tests/infer/gen_model_devi.py +++ b/source/tests/infer/gen_model_devi.py @@ -65,7 +65,11 @@ def main(): }, } - # ---- 2. Build two models with different seeds ---- + # ---- 2. Build two models with different seeds AND fitting sizes ---- + # Using different fitting neuron sizes ensures meaningfully different outputs + # (same arch + different seeds produces near-identical small random forces). + fitting_neurons = [[5, 5, 5], [10, 10, 10]] + from deepmd.pt.utils.serialization import ( # noqa: F401 deserialize_to_file, ) @@ -83,6 +87,7 @@ def main(): cfg = copy.deepcopy(config) cfg["descriptor"]["seed"] = seed cfg["fitting_net"]["seed"] = seed + cfg["fitting_net"]["neuron"] = fitting_neurons[idx] model = get_model(cfg) model_dict = model.serialize() data = { diff --git a/source/tests/pt_expt/model/test_model_compression.py b/source/tests/pt_expt/model/test_model_compression.py index 619bd10981..fb7681a8bb 100644 --- a/source/tests/pt_expt/model/test_model_compression.py +++ b/source/tests/pt_expt/model/test_model_compression.py @@ -290,6 +290,83 @@ def test_compress_cli_entry_point(self) -> None: if os.path.exists(compressed_path): os.unlink(compressed_path) + def test_freeze_compress_eval_pt2(self) -> None: + """Test pipeline: build → freeze (.pte) → compress → output (.pt2) → eval. + + Verifies that compressed models can be exported to .pt2 format and + produce consistent results when loaded via DeepPot. + """ + from deepmd.infer import ( + DeepPot, + ) + from deepmd.pt_expt.entrypoints.compress import ( + enable_compression as compress_entry, + ) + + # 1. Build and freeze to .pte + md = self._make_model() + md.min_nbor_dist = 0.5 + md.eval() + ret_frozen = self._eval_model(md) + + model_data = {"model": md.serialize(), "min_nbor_dist": 0.5} + with tempfile.NamedTemporaryFile(suffix=".pte", delete=False) as f: + frozen_path = f.name + with tempfile.NamedTemporaryFile(suffix=".pt2", delete=False) as f: + compressed_pt2_path = f.name + try: + deserialize_to_file(frozen_path, model_data) + + # 2. Compress with .pt2 output + compress_entry( + input_file=frozen_path, + output=compressed_pt2_path, + stride=0.01, + extrapolate=5, + check_frequency=-1, + ) + self.assertTrue(os.path.exists(compressed_pt2_path)) + + # 3. Load .pt2 and verify metadata has compression state + compressed_data = serialize_from_file(compressed_pt2_path) + descrpt_data = compressed_data["model"]["descriptor"] + self.assertIn("compress", descrpt_data) + + # 4. Eval via DeepPot and verify consistency + dp = DeepPot(compressed_pt2_path) + coord = self.coord.detach().cpu().numpy().reshape(-1) + box = self.cell.reshape(9).detach().cpu().numpy() + atype = self.atype[0].detach().cpu().numpy().tolist() + e_pt2, f_pt2, v_pt2 = dp.eval(coord, box, atype) + np.testing.assert_allclose( + ret_frozen["energy"], e_pt2, atol=1e-7, err_msg="energy" + ) + np.testing.assert_allclose( + ret_frozen["force"], f_pt2, atol=1e-7, err_msg="force" + ) + np.testing.assert_allclose( + ret_frozen["virial"], v_pt2, atol=1e-7, err_msg="virial" + ) + finally: + os.unlink(frozen_path) + if os.path.exists(compressed_pt2_path): + os.unlink(compressed_pt2_path) + + def test_min_nbor_dist_roundtrip_pt2(self) -> None: + """Test that min_nbor_dist survives freeze → load round-trip via .pt2.""" + md = self._make_model() + md.min_nbor_dist = 0.5 + + model_data = {"model": md.serialize(), "min_nbor_dist": 0.5} + with tempfile.NamedTemporaryFile(suffix=".pt2", delete=False) as f: + pt2_path = f.name + try: + deserialize_to_file(pt2_path, model_data) + loaded_data = serialize_from_file(pt2_path) + self.assertAlmostEqual(loaded_data["min_nbor_dist"], 0.5) + finally: + os.unlink(pt2_path) + if __name__ == "__main__": unittest.main() diff --git a/source/tests/pt_expt/test_change_bias.py b/source/tests/pt_expt/test_change_bias.py index 2ef754aa50..9dcd1811e3 100644 --- a/source/tests/pt_expt/test_change_bias.py +++ b/source/tests/pt_expt/test_change_bias.py @@ -250,6 +250,100 @@ def test_change_bias_frozen_pte(self) -> None: "Bias should have changed after change-bias on frozen model", ) + def test_change_bias_frozen_pt2(self) -> None: + """Change-bias on a .pt2 frozen model.""" + from deepmd.pt_expt.entrypoints.main import ( + freeze, + ) + from deepmd.pt_expt.model.model import ( + BaseModel, + ) + from deepmd.pt_expt.utils.serialization import ( + serialize_from_file, + ) + + pt2_path = os.path.join(self.tmpdir, "frozen.pt2") + freeze(model=self.model_path, output=pt2_path) + + original_data = serialize_from_file(pt2_path) + original_model = BaseModel.deserialize(original_data["model"]) + original_bias = to_numpy(original_model.get_out_bias()) + + output_pt2 = os.path.join(self.tmpdir, "frozen_updated.pt2") + run_dp( + f"dp --pt-expt change-bias {pt2_path} " + f"-s {self.data_file[0]} -o {output_pt2}" + ) + + updated_data = serialize_from_file(output_pt2) + updated_model = BaseModel.deserialize(updated_data["model"]) + updated_bias = to_numpy(updated_model.get_out_bias()) + + self.assertFalse( + np.allclose(original_bias, updated_bias), + "Bias should have changed after change-bias on .pt2 model", + ) + + def test_change_bias_frozen_pt2_user_defined(self) -> None: + """Change-bias with user-defined values on a .pt2 model.""" + from deepmd.pt_expt.entrypoints.main import ( + freeze, + ) + from deepmd.pt_expt.model.model import ( + BaseModel, + ) + from deepmd.pt_expt.utils.serialization import ( + serialize_from_file, + ) + + pt2_path = os.path.join(self.tmpdir, "frozen_ud.pt2") + freeze(model=self.model_path, output=pt2_path) + + output_pt2 = os.path.join(self.tmpdir, "frozen_ud_updated.pt2") + run_dp(f"dp --pt-expt change-bias {pt2_path} -b 1.0 2.0 -o {output_pt2}") + + updated_data = serialize_from_file(output_pt2) + updated_model = BaseModel.deserialize(updated_data["model"]) + updated_bias = to_numpy(updated_model.get_out_bias()) + + expected = np.array([[1.0], [2.0]]) + np.testing.assert_allclose(updated_bias.squeeze(0), expected, atol=1e-10) + + def test_change_bias_pt2_pte_consistency(self) -> None: + """Change-bias on .pte and .pt2 should produce same bias values.""" + from deepmd.pt_expt.entrypoints.main import ( + freeze, + ) + from deepmd.pt_expt.model.model import ( + BaseModel, + ) + from deepmd.pt_expt.utils.serialization import ( + serialize_from_file, + ) + + pte_path = os.path.join(self.tmpdir, "cons.pte") + pt2_path = os.path.join(self.tmpdir, "cons.pt2") + freeze(model=self.model_path, output=pte_path) + freeze(model=self.model_path, output=pt2_path) + + output_pte = os.path.join(self.tmpdir, "cons_updated.pte") + output_pt2 = os.path.join(self.tmpdir, "cons_updated.pt2") + run_dp( + f"dp --pt-expt change-bias {pte_path} " + f"-s {self.data_file[0]} -o {output_pte}" + ) + run_dp( + f"dp --pt-expt change-bias {pt2_path} " + f"-s {self.data_file[0]} -o {output_pt2}" + ) + + pte_data = serialize_from_file(output_pte) + pt2_data = serialize_from_file(output_pt2) + pte_bias = to_numpy(BaseModel.deserialize(pte_data["model"]).get_out_bias()) + pt2_bias = to_numpy(BaseModel.deserialize(pt2_data["model"]).get_out_bias()) + + np.testing.assert_allclose(pte_bias, pt2_bias, atol=1e-10) + def test_change_bias_pte_preserves_model_def_script(self) -> None: """change_bias on .pte should preserve model_def_script (training config).""" from deepmd.pt_expt.entrypoints.main import ( diff --git a/source/tests/pt_expt/test_dp_freeze.py b/source/tests/pt_expt/test_dp_freeze.py index f1c78eda16..60eab4f50a 100644 --- a/source/tests/pt_expt/test_dp_freeze.py +++ b/source/tests/pt_expt/test_dp_freeze.py @@ -96,6 +96,153 @@ def test_freeze_default_suffix(self) -> None: expected = os.path.join(self.tmpdir, "frozen_default_suffix.pte") self.assertTrue(os.path.exists(expected)) + def test_freeze_pt2(self) -> None: + """Freeze to .pt2 (AOTInductor) and verify the file is loadable.""" + output = os.path.join(self.tmpdir, "frozen_model.pt2") + freeze(model=self.ckpt_file, output=output) + self.assertTrue(os.path.exists(output)) + + # Verify the .pt2 can be loaded and evaluated via DeepPot + import numpy as np + + from deepmd.infer import ( + DeepPot, + ) + + dp = DeepPot(output) + self.assertEqual(dp.get_type_map(), ["O", "H", "B"]) + rcut = dp.get_rcut() + self.assertGreater(rcut, 0.0) + + # Quick smoke-test eval + coord = np.array( + [0.0, 0.0, 0.1, 1.0, 0.3, 0.2, 0.1, 1.9, 3.4], + dtype=np.float64, + ) + box = np.array([5.0, 0.0, 0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 5.0], dtype=np.float64) + atype = [0, 1, 2] + e, f, v = dp.eval(coord, box, atype) + self.assertEqual(e.shape, (1, 1)) + self.assertEqual(f.shape, (1, 3, 3)) + self.assertEqual(v.shape, (1, 9)) + + def test_freeze_pt2_eval_consistency(self) -> None: + """Verify .pte and .pt2 produce identical results.""" + import numpy as np + + from deepmd.infer import ( + DeepPot, + ) + + pte_path = os.path.join(self.tmpdir, "consistency.pte") + pt2_path = os.path.join(self.tmpdir, "consistency.pt2") + freeze(model=self.ckpt_file, output=pte_path) + freeze(model=self.ckpt_file, output=pt2_path) + + coord = np.array( + [0.0, 0.0, 0.1, 1.0, 0.3, 0.2, 0.1, 1.9, 3.4], + dtype=np.float64, + ) + box = np.array([5.0, 0.0, 0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 5.0], dtype=np.float64) + atype = [0, 1, 2] + + dp_pte = DeepPot(pte_path) + dp_pt2 = DeepPot(pt2_path) + + e_pte, f_pte, v_pte = dp_pte.eval(coord, box, atype) + e_pt2, f_pt2, v_pt2 = dp_pt2.eval(coord, box, atype) + + np.testing.assert_allclose(e_pte, e_pt2, atol=1e-10) + np.testing.assert_allclose(f_pte, f_pt2, atol=1e-10) + np.testing.assert_allclose(v_pte, v_pt2, atol=1e-10) + + def test_freeze_pt2_nopbc_negative_coords(self) -> None: + """Verify .pt2 NoPBC works with negative coordinates. + + Regression test: the C++ NoPBC path creates a fake box and must + shift coordinates so atoms with negative values are inside [0, L). + Compares .pt2 (C++ fake-box path) against .pte (Python no-ghost path) + — these are independent NoPBC implementations so cross-comparison + validates both. + """ + import numpy as np + + from deepmd.infer import ( + DeepPot, + ) + + pte_path = os.path.join(self.tmpdir, "nopbc_neg.pte") + pt2_path = os.path.join(self.tmpdir, "nopbc_neg.pt2") + freeze(model=self.ckpt_file, output=pte_path) + freeze(model=self.ckpt_file, output=pt2_path) + + # Coordinates with negative values — no periodic box + coord = np.array( + [-1.0, -2.0, 0.5, 1.0, 0.3, -0.2, 0.1, -1.9, 3.4], + dtype=np.float64, + ) + atype = [0, 1, 2] + + dp_pte = DeepPot(pte_path) + dp_pt2 = DeepPot(pt2_path) + + e_pte, f_pte, v_pte = dp_pte.eval(coord, None, atype) + e_pt2, f_pt2, v_pt2 = dp_pt2.eval(coord, None, atype) + + np.testing.assert_allclose(e_pte, e_pt2, atol=1e-10) + np.testing.assert_allclose(f_pte, f_pt2, atol=1e-10) + np.testing.assert_allclose(v_pte, v_pt2, atol=1e-10) + + +class TestDPFreezePt2DefaultFparam(unittest.TestCase): + """Test .pt2 with default fparam — eval without providing fparam.""" + + @classmethod + def setUpClass(cls) -> None: + cls.tmpdir = tempfile.mkdtemp() + + model_params = deepcopy(model_se_e2_a) + model_params["fitting_net"]["numb_fparam"] = 1 + model_params["fitting_net"]["default_fparam"] = [0.5] + model = get_model(model_params) + wrapper = ModelWrapper(model, model_params=model_params) + state_dict = wrapper.state_dict() + cls.ckpt_file = os.path.join(cls.tmpdir, "model_dfp.pt") + torch.save({"model": state_dict}, cls.ckpt_file) + + @classmethod + def tearDownClass(cls) -> None: + shutil.rmtree(cls.tmpdir) + + def test_pt2_eval_default_fparam(self) -> None: + """Eval .pt2 without fparam should match eval with explicit default value.""" + import numpy as np + + from deepmd.infer import ( + DeepPot, + ) + + pt2_path = os.path.join(self.tmpdir, "dfp.pt2") + freeze(model=self.ckpt_file, output=pt2_path) + + coord = np.array( + [0.0, 0.0, 0.1, 1.0, 0.3, 0.2, 0.1, 1.9, 3.4], + dtype=np.float64, + ) + box = np.array([5.0, 0.0, 0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 5.0], dtype=np.float64) + atype = [0, 1, 2] + + dp = DeepPot(pt2_path) + + # Eval WITHOUT fparam — model should use default (0.5) + e_no, f_no, v_no = dp.eval(coord, box, atype) + # Eval WITH explicit default value + e_ex, f_ex, v_ex = dp.eval(coord, box, atype, fparam=[0.5]) + + np.testing.assert_allclose(e_no, e_ex, atol=1e-10) + np.testing.assert_allclose(f_no, f_ex, atol=1e-10) + np.testing.assert_allclose(v_no, v_ex, atol=1e-10) + if __name__ == "__main__": unittest.main() diff --git a/source/tests/pt_expt/test_finetune.py b/source/tests/pt_expt/test_finetune.py index 250ba85d46..063bb85f71 100644 --- a/source/tests/pt_expt/test_finetune.py +++ b/source/tests/pt_expt/test_finetune.py @@ -877,6 +877,125 @@ def test_finetune_from_pte_use_pretrain_script(self) -> None: os.chdir(old_cwd) shutil.rmtree(tmpdir, ignore_errors=True) + def test_finetune_from_pt2(self) -> None: + """Train -> freeze to .pt2 -> finetune from .pt2 -> verify checkpoint.""" + from deepmd.pt_expt.entrypoints.main import ( + freeze, + main, + ) + + tmpdir = tempfile.mkdtemp(prefix="pt_expt_ft_pt2_") + old_cwd = os.getcwd() + os.chdir(tmpdir) + try: + # Phase 1: train pretrained model + config = _make_config(self.data_dir, model_se_e2_a, numb_steps=1) + config = update_deepmd_input(config, warning=False) + config = normalize(config) + ckpt_path = self._train_pretrained(config, tmpdir) + + # Phase 2: freeze to .pt2 + pt2_path = os.path.join(tmpdir, "frozen.pt2") + freeze(model=ckpt_path, output=pt2_path) + self.assertTrue(os.path.exists(pt2_path)) + + # Save pretrained weights before finetune overwrites model.ckpt.pt + pre_ckpt_copy = os.path.join(tmpdir, "pretrained_copy.pt") + shutil.copy2(ckpt_path, pre_ckpt_copy) + + # Phase 3: finetune from .pt2 via CLI (lr=0 so weights stay unchanged) + ft_config = _make_config(self.data_dir, model_se_e2_a, numb_steps=1) + ft_config["learning_rate"]["start_lr"] = 1e-30 + ft_config["learning_rate"]["stop_lr"] = 1e-30 + ft_config_file = os.path.join(tmpdir, "finetune_input.json") + with open(ft_config_file, "w") as f: + json.dump(ft_config, f) + + main( + [ + "train", + ft_config_file, + "--finetune", + pt2_path, + "--skip-neighbor-stat", + ] + ) + + # Verify new checkpoint exists + ft_ckpt = os.path.join(tmpdir, "model.ckpt.pt") + self.assertTrue(os.path.exists(ft_ckpt), "Finetune checkpoint not found") + + # Load finetuned model and verify it's valid + ft_state = torch.load(ft_ckpt, map_location=DEVICE, weights_only=True) + ft_model_state = ft_state["model"] if "model" in ft_state else ft_state + self.assertIn("_extra_state", ft_model_state) + + # Load pretrained from saved copy (finetune overwrites model.ckpt.pt) + pre_state = torch.load( + pre_ckpt_copy, map_location=DEVICE, weights_only=True + ) + pre_model_state = pre_state["model"] if "model" in pre_state else pre_state + + # Inherited weights must match pretrained + self._assert_inherited_weights_match( + ft_model_state, pre_model_state, random_fitting=False + ) + finally: + os.chdir(old_cwd) + shutil.rmtree(tmpdir, ignore_errors=True) + + def test_finetune_from_pt2_use_pretrain_script(self) -> None: + """Train -> freeze to .pt2 -> finetune with --use-pretrain-script.""" + from deepmd.pt_expt.entrypoints.main import ( + freeze, + main, + ) + + tmpdir = tempfile.mkdtemp(prefix="pt_expt_ft_pt2_ups_") + old_cwd = os.getcwd() + os.chdir(tmpdir) + try: + # Phase 1: train pretrained model + config = _make_config(self.data_dir, model_se_e2_a, numb_steps=1) + config = update_deepmd_input(config, warning=False) + config = normalize(config) + ckpt_path = self._train_pretrained(config, tmpdir) + + # Phase 2: freeze to .pt2 (embeds model_params) + pt2_path = os.path.join(tmpdir, "frozen.pt2") + freeze(model=ckpt_path, output=pt2_path) + + # Phase 3: finetune from .pt2 with --use-pretrain-script + ft_model_params = deepcopy(model_se_e2_a) + ft_model_params["descriptor"]["neuron"] = [4, 8] # different + ft_config = _make_config(self.data_dir, ft_model_params, numb_steps=1) + ft_config_file = os.path.join(tmpdir, "finetune_input.json") + with open(ft_config_file, "w") as f: + json.dump(ft_config, f) + + main( + [ + "train", + ft_config_file, + "--finetune", + pt2_path, + "--use-pretrain-script", + "--skip-neighbor-stat", + ] + ) + + # Verify the output config was updated from pretrained + with open(os.path.join(tmpdir, "out.json")) as f: + output_config = json.load(f) + # Descriptor neuron should be from pretrained, not from ft_config + self.assertEqual( + output_config["model"]["descriptor"]["neuron"], + model_se_e2_a["descriptor"]["neuron"], + ) + finally: + os.chdir(old_cwd) + shutil.rmtree(tmpdir, ignore_errors=True) + if __name__ == "__main__": unittest.main()