diff --git a/deepmd/calculator.py b/deepmd/calculator.py index 1d8e955de7..356bfeb9ce 100644 --- a/deepmd/calculator.py +++ b/deepmd/calculator.py @@ -137,8 +137,14 @@ def calculate( fparam = self.atoms.info.get("fparam", None) aparam = self.atoms.info.get("aparam", None) + charge_spin = self.atoms.info.get("charge_spin", None) e, f, v = self.dp.eval( - coords=coord, cells=cell, atom_types=atype, fparam=fparam, aparam=aparam + coords=coord, + cells=cell, + atom_types=atype, + fparam=fparam, + aparam=aparam, + charge_spin=charge_spin, )[:3] self.results["energy"] = e[0][0] # see https://gitlab.com/ase/ase/-/merge_requests/2485 diff --git a/deepmd/dpmodel/atomic_model/base_atomic_model.py b/deepmd/dpmodel/atomic_model/base_atomic_model.py index debddba6e7..cf59af94db 100644 --- a/deepmd/dpmodel/atomic_model/base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/base_atomic_model.py @@ -156,6 +156,22 @@ def get_default_fparam(self) -> list[float] | None: """Get the default frame parameters.""" return None + def has_chg_spin_ebd(self) -> bool: + """Check if the model has charge spin embedding.""" + return False + + def get_dim_chg_spin(self) -> int: + """Get the dimension of charge_spin input.""" + return 0 + + def has_default_chg_spin(self) -> bool: + """Check if the model has default charge_spin values.""" + return False + + def get_default_chg_spin(self) -> list[float] | None: + """Get the default charge_spin values.""" + return None + def reinit_atom_exclude( self, exclude_types: list[int] = [], @@ -232,6 +248,7 @@ def forward_common_atomic( fparam: Array | None = None, aparam: Array | None = None, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> dict[str, Array]: """Common interface for atomic inference. @@ -284,6 +301,7 @@ def forward_common_atomic( fparam=fparam, aparam=aparam, comm_dict=comm_dict, + charge_spin=charge_spin, ) ret_dict = self.apply_out_stat(ret_dict, atype) @@ -312,6 +330,7 @@ def call( mapping: Array | None = None, fparam: Array | None = None, aparam: Array | None = None, + charge_spin: Array | None = None, ) -> dict[str, Array]: return self.forward_common_atomic( extended_coord, @@ -320,6 +339,7 @@ def call( mapping=mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, ) def get_intensive(self) -> bool: @@ -524,6 +544,7 @@ def model_forward( box: np.ndarray | None, fparam: np.ndarray | None = None, aparam: np.ndarray | None = None, + charge_spin: np.ndarray | None = None, ) -> dict[str, np.ndarray]: # Get reference array to determine the target array type and device # Use out_bias as reference since it's always present @@ -543,6 +564,8 @@ def model_forward( fparam = xp.asarray(fparam, device=device) if aparam is not None: aparam = xp.asarray(aparam, device=device) + if charge_spin is not None: + charge_spin = xp.asarray(charge_spin, device=device) ( extended_coord, @@ -564,6 +587,7 @@ def model_forward( mapping=mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, ) # Convert outputs back to numpy arrays return {kk: to_numpy_array(vv) for kk, vv in atomic_ret.items()} diff --git a/deepmd/dpmodel/atomic_model/dp_atomic_model.py b/deepmd/dpmodel/atomic_model/dp_atomic_model.py index 0505f63d83..a2b49f47e3 100644 --- a/deepmd/dpmodel/atomic_model/dp_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/dp_atomic_model.py @@ -79,6 +79,28 @@ def __init__( ) super().init_out_stat() + def has_chg_spin_ebd(self) -> bool: + """Check if the model has charge spin embedding.""" + return self.add_chg_spin_ebd + + def get_dim_chg_spin(self) -> int: + """Get the dimension of charge_spin input.""" + if self.add_chg_spin_ebd: + return self.descriptor.get_dim_chg_spin() + return 0 + + def has_default_chg_spin(self) -> bool: + """Check if the model has default charge_spin values.""" + if self.add_chg_spin_ebd: + return self.descriptor.has_default_chg_spin() + return False + + def get_default_chg_spin(self) -> list[float] | None: + """Get the default charge_spin values.""" + if self.add_chg_spin_ebd and self.descriptor.has_default_chg_spin(): + return self.descriptor.get_default_chg_spin() + return None + def fitting_output_def(self) -> FittingOutputDef: """Get the output def of the fitting net.""" return self.fitting_net.output_def() @@ -158,6 +180,7 @@ def forward_atomic( fparam: Array | None = None, aparam: Array | None = None, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> dict[str, Array]: """Models' atomic predictions. @@ -178,6 +201,8 @@ def forward_atomic( comm_dict MPI communication metadata for parallel inference. ``None`` for non-parallel inference (default). Forwarded to the descriptor. + charge_spin + charge and spin parameter for descriptor. nf x 2 Returns ------- @@ -188,38 +213,29 @@ def forward_atomic( nframes, nloc, nnei = nlist.shape atype = xp_take_first_n(extended_atype, 1, nloc) - # Handle default fparam if fitting net supports it - if ( - hasattr(self.fitting_net, "get_dim_fparam") - and self.fitting_net.get_dim_fparam() > 0 - and fparam is None - ): - # use default fparam - from deepmd.dpmodel.array_api import ( - array_api_compat, - ) - - default_fparam = self.fitting_net.get_default_fparam() - assert default_fparam is not None - xp = array_api_compat.array_namespace(extended_coord) - default_fparam_array = xp.asarray( - default_fparam, - dtype=extended_coord.dtype, - device=array_api_compat.device(extended_coord), - ) - fparam_input_for_des = xp.tile( - xp.reshape(default_fparam_array, (1, -1)), (nframes, 1) - ) - else: - fparam_input_for_des = fparam + # Handle default charge_spin if descriptor supports it + if self.add_chg_spin_ebd and charge_spin is None: + default_cs = self.descriptor.get_default_chg_spin() + if default_cs is not None: + from deepmd.dpmodel.array_api import ( + array_api_compat, + ) + + xp = array_api_compat.array_namespace(extended_coord) + cs_array = xp.asarray( + default_cs, + dtype=extended_coord.dtype, + device=array_api_compat.device(extended_coord), + ) + charge_spin = xp.tile(xp.reshape(cs_array, (1, -1)), (nframes, 1)) descriptor, rot_mat, g2, h2, sw = self.descriptor( extended_coord, extended_atype, nlist, mapping=mapping, - fparam=fparam_input_for_des if self.add_chg_spin_ebd else None, comm_dict=comm_dict, + charge_spin=charge_spin if self.add_chg_spin_ebd else None, ) ret = self.fitting_net( descriptor, diff --git a/deepmd/dpmodel/atomic_model/linear_atomic_model.py b/deepmd/dpmodel/atomic_model/linear_atomic_model.py index 05ff8499f8..8d4d6f2f8f 100644 --- a/deepmd/dpmodel/atomic_model/linear_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/linear_atomic_model.py @@ -225,6 +225,7 @@ def forward_atomic( fparam: Array | None = None, aparam: Array | None = None, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> dict[str, Array]: """Return atomic prediction. @@ -286,6 +287,7 @@ def forward_atomic( fparam, aparam, comm_dict, + charge_spin=charge_spin, )["energy"] ) weights = self._compute_weight(extended_coord, extended_atype, nlists_) diff --git a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py index 3e48e88c87..7118aa5d7a 100644 --- a/deepmd/dpmodel/atomic_model/make_base_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/make_base_atomic_model.py @@ -138,6 +138,7 @@ def fwd( mapping: t_tensor | None = None, fparam: t_tensor | None = None, aparam: t_tensor | None = None, + charge_spin: t_tensor | None = None, ) -> dict[str, t_tensor]: pass diff --git a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py index c1ec9d2a00..cea0403812 100644 --- a/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py +++ b/deepmd/dpmodel/atomic_model/pairtab_atomic_model.py @@ -254,6 +254,7 @@ def forward_atomic( fparam: Array | None = None, aparam: Array | None = None, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> dict[str, Array]: del comm_dict # pairtab is local; no MPI ghost exchange needed. xp = array_api_compat.array_namespace(extended_coord, extended_atype, nlist) diff --git a/deepmd/dpmodel/descriptor/dpa1.py b/deepmd/dpmodel/descriptor/dpa1.py index 04d0420009..2311858180 100644 --- a/deepmd/dpmodel/descriptor/dpa1.py +++ b/deepmd/dpmodel/descriptor/dpa1.py @@ -509,6 +509,7 @@ def call( mapping: Array | None = None, fparam: Array | None = None, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> Array: """Compute the descriptor. diff --git a/deepmd/dpmodel/descriptor/dpa2.py b/deepmd/dpmodel/descriptor/dpa2.py index e530398ca6..928c58a9b4 100644 --- a/deepmd/dpmodel/descriptor/dpa2.py +++ b/deepmd/dpmodel/descriptor/dpa2.py @@ -842,6 +842,7 @@ def call( mapping: Array | None = None, fparam: Array | None = None, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> tuple[Array, Array, Array, Array, Array]: """Compute the descriptor. diff --git a/deepmd/dpmodel/descriptor/dpa3.py b/deepmd/dpmodel/descriptor/dpa3.py index 59ee20ec24..76124fa544 100644 --- a/deepmd/dpmodel/descriptor/dpa3.py +++ b/deepmd/dpmodel/descriptor/dpa3.py @@ -377,6 +377,7 @@ def __init__( use_loc_mapping: bool = True, type_map: list[str] | None = None, add_chg_spin_ebd: bool = False, + default_chg_spin: list[float] | None = None, ) -> None: super().__init__() @@ -433,6 +434,11 @@ def init_subclass_params(sub_data: dict | Any, sub_class: type) -> Any: self.use_econf_tebd = use_econf_tebd self.add_chg_spin_ebd = add_chg_spin_ebd + if default_chg_spin is not None and len(default_chg_spin) != 2: + raise ValueError( + "default_chg_spin must have exactly 2 values [charge, spin]" + ) + self.default_chg_spin = default_chg_spin self.use_tebd_bias = use_tebd_bias self.use_loc_mapping = use_loc_mapping self.type_map = type_map @@ -499,6 +505,18 @@ def get_rcut(self) -> float: """Returns the cut-off radius.""" return self.rcut + def get_dim_chg_spin(self) -> int: + """Returns the dimension of charge_spin input.""" + return 2 if self.add_chg_spin_ebd else 0 + + def has_default_chg_spin(self) -> bool: + """Returns whether default charge_spin values are set.""" + return self.default_chg_spin is not None + + def get_default_chg_spin(self) -> list[float] | None: + """Returns the default charge_spin values.""" + return self.default_chg_spin + def get_rcut_smth(self) -> float: """Returns the radius where the neighbor information starts to smoothly decay to 0.""" return self.rcut_smth @@ -647,6 +665,7 @@ def call( mapping: Array | None = None, fparam: Array | None = None, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> tuple[Array, Array, Array, Array, Array]: """Compute the descriptor. @@ -702,13 +721,13 @@ def call( ) if self.add_chg_spin_ebd: - assert fparam is not None + assert charge_spin is not None assert self.chg_embedding is not None assert self.spin_embedding is not None chg_tebd = self.chg_embedding.call() spin_tebd = self.spin_embedding.call() - charge = xp.astype(fparam[:, 0], xp.int64) + 100 - spin = xp.astype(fparam[:, 1], xp.int64) + charge = xp.astype(charge_spin[:, 0], xp.int64) + 100 + spin = xp.astype(charge_spin[:, 1], xp.int64) chg_ebd = xp.reshape( xp.take(chg_tebd, xp.reshape(charge, (-1,)), axis=0), (nframes, self.tebd_dim), @@ -753,6 +772,7 @@ def serialize(self) -> dict: "use_tebd_bias": self.use_tebd_bias, "use_loc_mapping": self.use_loc_mapping, "add_chg_spin_ebd": self.add_chg_spin_ebd, + "default_chg_spin": self.default_chg_spin, "type_map": self.type_map, "type_embedding": self.type_embedding.serialize(), } diff --git a/deepmd/dpmodel/descriptor/hybrid.py b/deepmd/dpmodel/descriptor/hybrid.py index a51220c5e2..110ae1fbe0 100644 --- a/deepmd/dpmodel/descriptor/hybrid.py +++ b/deepmd/dpmodel/descriptor/hybrid.py @@ -123,6 +123,40 @@ def get_rcut(self) -> float: """Returns the cut-off radius.""" return np.max([descrpt.get_rcut() for descrpt in self.descrpt_list]).item() + def get_dim_chg_spin(self) -> int: + """Returns the dimension of charge_spin input (0 if not supported).""" + return max( + (descrpt.get_dim_chg_spin() for descrpt in self.descrpt_list), default=0 + ) + + def has_default_chg_spin(self) -> bool: + """Returns whether the descriptor has a default charge_spin value.""" + default_chg_spin = None + found_chg_spin = False + for descrpt in self.descrpt_list: + if descrpt.get_dim_chg_spin() == 0: + continue + found_chg_spin = True + if not descrpt.has_default_chg_spin(): + return False + child_default_chg_spin = descrpt.get_default_chg_spin() + if child_default_chg_spin is None: + return False + if default_chg_spin is None: + default_chg_spin = child_default_chg_spin + elif child_default_chg_spin != default_chg_spin: + return False + return found_chg_spin + + def get_default_chg_spin(self) -> list[float] | None: + """Returns the default charge_spin value, or None.""" + if not self.has_default_chg_spin(): + return None + for descrpt in self.descrpt_list: + if descrpt.get_dim_chg_spin() > 0: + return descrpt.get_default_chg_spin() + return None + def get_rcut_smth(self) -> float: """Returns the radius where the neighbor information starts to smoothly decay to 0.""" # may not be a good idea... @@ -287,6 +321,7 @@ def call( mapping: Array | None = None, fparam: Array | None = None, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> tuple[ Array, Array | None, @@ -344,7 +379,13 @@ def call( assert nl_distinguish_types is not None nl = nl_distinguish_types[:, :, nci] odescriptor, gr, _g2, _h2, _sw = descrpt( - coord_ext, atype_ext, nl, mapping, comm_dict=comm_dict + coord_ext, + atype_ext, + nl, + mapping, + fparam=fparam, + comm_dict=comm_dict, + charge_spin=charge_spin, ) out_descriptor.append(odescriptor) if gr is not None: diff --git a/deepmd/dpmodel/descriptor/make_base_descriptor.py b/deepmd/dpmodel/descriptor/make_base_descriptor.py index 8184b4e42a..c1938a3b01 100644 --- a/deepmd/dpmodel/descriptor/make_base_descriptor.py +++ b/deepmd/dpmodel/descriptor/make_base_descriptor.py @@ -96,6 +96,18 @@ def get_dim_emb(self) -> int: """Returns the embedding dimension of g2.""" pass + def get_dim_chg_spin(self) -> int: + """Returns the dimension of charge_spin input (0 if not supported).""" + return 0 + + def has_default_chg_spin(self) -> bool: + """Returns whether the descriptor has a default charge_spin value.""" + return False + + def get_default_chg_spin(self) -> Any: + """Returns the default charge_spin value, or None.""" + return None + @abstractmethod def mixed_types(self) -> bool: """Returns if the descriptor requires a neighbor list that distinguish different @@ -205,6 +217,7 @@ def fwd( nlist: Array, mapping: Array | None = None, fparam: Array | None = None, + charge_spin: Array | None = None, ) -> Array: """Calculate descriptor.""" pass diff --git a/deepmd/dpmodel/descriptor/se_e2_a.py b/deepmd/dpmodel/descriptor/se_e2_a.py index f72b6f75e8..bdac1e0cc0 100644 --- a/deepmd/dpmodel/descriptor/se_e2_a.py +++ b/deepmd/dpmodel/descriptor/se_e2_a.py @@ -404,6 +404,7 @@ def call( mapping: Array | None = None, fparam: Array | None = None, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> Array: """Compute the descriptor. diff --git a/deepmd/dpmodel/descriptor/se_r.py b/deepmd/dpmodel/descriptor/se_r.py index 6846710735..624889d85f 100644 --- a/deepmd/dpmodel/descriptor/se_r.py +++ b/deepmd/dpmodel/descriptor/se_r.py @@ -376,6 +376,7 @@ def call( mapping: Array | None = None, fparam: Array | None = None, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> Array: """Compute the descriptor. diff --git a/deepmd/dpmodel/descriptor/se_t.py b/deepmd/dpmodel/descriptor/se_t.py index 2d61736235..8eff7b81d1 100644 --- a/deepmd/dpmodel/descriptor/se_t.py +++ b/deepmd/dpmodel/descriptor/se_t.py @@ -351,6 +351,7 @@ def call( mapping: Array | None = None, fparam: Array | None = None, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> tuple[Array, Array]: """Compute the descriptor. diff --git a/deepmd/dpmodel/descriptor/se_t_tebd.py b/deepmd/dpmodel/descriptor/se_t_tebd.py index 2f6e749e19..3c972ba29c 100644 --- a/deepmd/dpmodel/descriptor/se_t_tebd.py +++ b/deepmd/dpmodel/descriptor/se_t_tebd.py @@ -359,6 +359,7 @@ def call( mapping: Array | None = None, fparam: Array | None = None, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> tuple[Array, Array]: """Compute the descriptor. diff --git a/deepmd/dpmodel/model/dipole_model.py b/deepmd/dpmodel/model/dipole_model.py index fa5a76e0af..9e85403e9f 100644 --- a/deepmd/dpmodel/model/dipole_model.py +++ b/deepmd/dpmodel/model/dipole_model.py @@ -44,6 +44,7 @@ def call( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: model_ret = self.call_common( coord, @@ -52,6 +53,7 @@ def call( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) model_predict = {} model_predict["dipole"] = model_ret["dipole"] @@ -75,6 +77,7 @@ def call_lower( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: model_ret = self.call_common_lower( extended_coord, @@ -84,6 +87,7 @@ def call_lower( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) model_predict = {} model_predict["dipole"] = model_ret["dipole"] diff --git a/deepmd/dpmodel/model/dos_model.py b/deepmd/dpmodel/model/dos_model.py index b75c9a2bcc..4d854ae007 100644 --- a/deepmd/dpmodel/model/dos_model.py +++ b/deepmd/dpmodel/model/dos_model.py @@ -44,6 +44,7 @@ def call( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: model_ret = self.call_common( coord, @@ -52,6 +53,7 @@ def call( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) model_predict = {} model_predict["atom_dos"] = model_ret["dos"] @@ -69,6 +71,7 @@ def call_lower( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: model_ret = self.call_common_lower( extended_coord, @@ -78,6 +81,7 @@ def call_lower( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) model_predict = {} model_predict["atom_dos"] = model_ret["dos"] diff --git a/deepmd/dpmodel/model/dp_zbl_model.py b/deepmd/dpmodel/model/dp_zbl_model.py index d864b3b61e..d389c0b7b2 100644 --- a/deepmd/dpmodel/model/dp_zbl_model.py +++ b/deepmd/dpmodel/model/dp_zbl_model.py @@ -46,6 +46,7 @@ def call( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: model_ret = self.call_common( coord, @@ -54,6 +55,7 @@ def call( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) model_predict = {} model_predict["atom_energy"] = model_ret["energy"] @@ -77,6 +79,7 @@ def call_lower( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: model_ret = self.call_common_lower( extended_coord, @@ -86,6 +89,7 @@ def call_lower( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) model_predict = {} model_predict["atom_energy"] = model_ret["energy"] diff --git a/deepmd/dpmodel/model/ener_model.py b/deepmd/dpmodel/model/ener_model.py index 57b518d75d..c8c75d3cca 100644 --- a/deepmd/dpmodel/model/ener_model.py +++ b/deepmd/dpmodel/model/ener_model.py @@ -87,6 +87,7 @@ def call( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: model_ret = self.call_common( coord, @@ -94,6 +95,7 @@ def call( box, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -120,6 +122,7 @@ def call_lower( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: model_ret = self.call_common_lower( extended_coord, @@ -128,6 +131,7 @@ def call_lower( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} diff --git a/deepmd/dpmodel/model/make_model.py b/deepmd/dpmodel/model/make_model.py index d9617e981a..ebcc671f62 100644 --- a/deepmd/dpmodel/model/make_model.py +++ b/deepmd/dpmodel/model/make_model.py @@ -77,6 +77,7 @@ def model_call_from_call_lower( aparam: Array | None = None, do_atomic_virial: bool = False, coord_corr_for_virial: Array | None = None, + charge_spin: Array | None = None, ) -> dict[str, Array]: """Return model prediction from lower interface. @@ -146,6 +147,7 @@ def model_call_from_call_lower( "fparam": fp, "aparam": ap, "do_atomic_virial": do_atomic_virial, + "charge_spin": charge_spin, } if extended_coord_corr is not None: call_lower_kwargs["extended_coord_corr"] = extended_coord_corr @@ -266,6 +268,7 @@ def call_common( aparam: Array | None = None, do_atomic_virial: bool = False, coord_corr_for_virial: Array | None = None, + charge_spin: Array | None = None, ) -> dict[str, Array]: """Return model prediction. @@ -295,10 +298,10 @@ def call_common( The keys are defined by the `ModelOutputDef`. """ - cc, bb, fp, ap, input_prec = self._input_type_cast( - coord, box=box, fparam=fparam, aparam=aparam + cc, bb, fp, ap, cs, input_prec = self._input_type_cast( + coord, box=box, fparam=fparam, aparam=aparam, charge_spin=charge_spin ) - del coord, box, fparam, aparam + del coord, box, fparam, aparam, charge_spin model_predict = model_call_from_call_lower( call_lower=self.call_common_lower, rcut=self.get_rcut(), @@ -312,6 +315,7 @@ def call_common( aparam=ap, do_atomic_virial=do_atomic_virial, coord_corr_for_virial=coord_corr_for_virial, + charge_spin=cs, ) model_predict = self._output_type_cast(model_predict, input_prec) return model_predict @@ -327,6 +331,7 @@ def call_common_lower( do_atomic_virial: bool = False, extended_coord_corr: Array | None = None, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> dict[str, Array]: """Return model prediction. Lower interface that takes extended atomic coordinates and types, nlist, and mapping @@ -372,10 +377,10 @@ def call_common_lower( nlist, extra_nlist_sort=self.need_sorted_nlist_for_lower(), ) - cc_ext, _, fp, ap, input_prec = self._input_type_cast( - extended_coord, fparam=fparam, aparam=aparam + cc_ext, _, fp, ap, cs, input_prec = self._input_type_cast( + extended_coord, fparam=fparam, aparam=aparam, charge_spin=charge_spin ) - del extended_coord, fparam, aparam + del extended_coord, fparam, aparam, charge_spin model_predict = self.forward_common_atomic( cc_ext, extended_atype, @@ -386,6 +391,7 @@ def call_common_lower( do_atomic_virial=do_atomic_virial, extended_coord_corr=extended_coord_corr, comm_dict=comm_dict, + charge_spin=cs, ) model_predict = self._output_type_cast(model_predict, input_prec) return model_predict @@ -401,6 +407,7 @@ def forward_common_atomic( do_atomic_virial: bool = False, extended_coord_corr: Array | None = None, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> dict[str, Array]: atomic_ret = self.atomic_model.forward_common_atomic( extended_coord, @@ -410,6 +417,7 @@ def forward_common_atomic( fparam=fparam, aparam=aparam, comm_dict=comm_dict, + charge_spin=charge_spin, ) return fit_output_to_model_output( atomic_ret, @@ -474,7 +482,8 @@ def _input_type_cast( box: Array | None = None, fparam: Array | None = None, aparam: Array | None = None, - ) -> tuple[Array, Array | None, Array | None, Array | None, Any]: + charge_spin: Array | None = None, + ) -> tuple[Array, Array | None, Array | None, Array | None, Array | None, Any]: """Cast the input data to global float type.""" xp = array_api_compat.array_namespace(coord) input_dtype = coord.dtype @@ -486,17 +495,20 @@ def _input_type_cast( ### _lst: list[Array | None] = [ xp.astype(vv, input_dtype) if vv is not None else None - for vv in [box, fparam, aparam] + for vv in [box, fparam, aparam, charge_spin] ] - box, fparam, aparam = _lst + box, fparam, aparam, charge_spin = _lst if input_dtype == global_dtype: - return coord, box, fparam, aparam, input_dtype + return coord, box, fparam, aparam, charge_spin, input_dtype else: return ( xp.astype(coord, global_dtype), xp.astype(box, global_dtype) if box is not None else None, xp.astype(fparam, global_dtype) if fparam is not None else None, xp.astype(aparam, global_dtype) if aparam is not None else None, + xp.astype(charge_spin, global_dtype) + if charge_spin is not None + else None, input_dtype, ) @@ -709,6 +721,22 @@ def get_default_fparam(self) -> list[float] | None: """Get the default frame parameters.""" return self.atomic_model.get_default_fparam() + def has_chg_spin_ebd(self) -> bool: + """Check if the model has charge spin embedding.""" + return self.atomic_model.has_chg_spin_ebd() + + def get_dim_chg_spin(self) -> int: + """Get the dimension of charge_spin input.""" + return self.atomic_model.get_dim_chg_spin() + + def has_default_chg_spin(self) -> bool: + """Check if the model has default charge_spin values.""" + return self.atomic_model.has_default_chg_spin() + + def get_default_chg_spin(self) -> list[float] | None: + """Get the default charge_spin values.""" + return self.atomic_model.get_default_chg_spin() + def get_sel_type(self) -> list[int]: """Get the selected atom types of this model. diff --git a/deepmd/dpmodel/model/polar_model.py b/deepmd/dpmodel/model/polar_model.py index 5031166a5e..83c4b62f01 100644 --- a/deepmd/dpmodel/model/polar_model.py +++ b/deepmd/dpmodel/model/polar_model.py @@ -44,6 +44,7 @@ def call( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: model_ret = self.call_common( coord, @@ -52,6 +53,7 @@ def call( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) model_predict = {} model_predict["polar"] = model_ret["polarizability"] @@ -69,6 +71,7 @@ def call_lower( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: model_ret = self.call_common_lower( extended_coord, @@ -78,6 +81,7 @@ def call_lower( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) model_predict = {} model_predict["polar"] = model_ret["polarizability"] diff --git a/deepmd/dpmodel/model/property_model.py b/deepmd/dpmodel/model/property_model.py index bc1657f0bd..d3153b92f9 100644 --- a/deepmd/dpmodel/model/property_model.py +++ b/deepmd/dpmodel/model/property_model.py @@ -51,6 +51,7 @@ def call( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: model_ret = self.call_common( coord, @@ -59,6 +60,7 @@ def call( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) var_name = self.get_var_name() model_predict = {} @@ -77,6 +79,7 @@ def call_lower( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: model_ret = self.call_common_lower( extended_coord, @@ -86,6 +89,7 @@ def call_lower( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) var_name = self.get_var_name() model_predict = {} diff --git a/deepmd/dpmodel/model/spin_model.py b/deepmd/dpmodel/model/spin_model.py index 2de41945f3..373294bece 100644 --- a/deepmd/dpmodel/model/spin_model.py +++ b/deepmd/dpmodel/model/spin_model.py @@ -580,6 +580,7 @@ def call_common( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: """Return model prediction with raw internal keys. @@ -624,6 +625,7 @@ def call_common( box, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, coord_corr_for_virial=coord_corr_for_virial, ) @@ -674,6 +676,7 @@ def call( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: """Return model prediction with translated user-facing keys. @@ -710,6 +713,7 @@ def call( box, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_output_type = self.backbone_model.model_output_type() @@ -749,6 +753,7 @@ def call_common_lower( aparam: Array | None = None, do_atomic_virial: bool = False, comm_dict: dict | None = None, + charge_spin: Array | None = None, ) -> dict[str, Array]: """Return model prediction with raw internal keys. Lower interface that takes extended atomic coordinates, types and spins, nlist, and mapping @@ -799,6 +804,7 @@ def call_common_lower( mapping=mapping_updated, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, extended_coord_corr=extended_coord_corr, comm_dict=comm_dict, @@ -854,6 +860,7 @@ def call_lower( fparam: Array | None = None, aparam: Array | None = None, do_atomic_virial: bool = False, + charge_spin: Array | None = None, ) -> dict[str, Array]: """Return model prediction with translated user-facing keys. Lower interface. @@ -891,6 +898,7 @@ def call_lower( mapping=mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_output_type = self.backbone_model.model_output_type() diff --git a/deepmd/dpmodel/utils/batch.py b/deepmd/dpmodel/utils/batch.py index 204ae9771f..2cbf8a72ff 100644 --- a/deepmd/dpmodel/utils/batch.py +++ b/deepmd/dpmodel/utils/batch.py @@ -11,7 +11,7 @@ _DROP_KEYS = {"default_mesh", "sid", "fid"} # Keys that belong to model input (everything else is label). -_INPUT_KEYS = {"coord", "atype", "spin", "box", "fparam", "aparam"} +_INPUT_KEYS = {"coord", "atype", "spin", "box", "fparam", "aparam", "charge_spin"} def normalize_batch(batch: dict[str, Any]) -> dict[str, Any]: diff --git a/deepmd/dpmodel/utils/lmdb_data.py b/deepmd/dpmodel/utils/lmdb_data.py index 29253263a6..dc207f4aa1 100644 --- a/deepmd/dpmodel/utils/lmdb_data.py +++ b/deepmd/dpmodel/utils/lmdb_data.py @@ -667,8 +667,8 @@ def __getitem__(self, index: int) -> dict[str, Any]: np.repeat(frame[req_key], repeat).reshape(-1).astype(req_dtype) ) - # Add find_* for fparam/aparam/spin if not already set - for extra_key in ["fparam", "aparam", "spin"]: + # Add find_* for fparam/aparam/spin/charge_spin if not already set + for extra_key in ["fparam", "aparam", "spin", "charge_spin"]: if f"find_{extra_key}" not in frame: frame[f"find_{extra_key}"] = ( np.float32(1.0) if extra_key in frame else np.float32(0.0) diff --git a/deepmd/dpmodel/utils/stat.py b/deepmd/dpmodel/utils/stat.py index 2c170da705..ca00f6c064 100644 --- a/deepmd/dpmodel/utils/stat.py +++ b/deepmd/dpmodel/utils/stat.py @@ -215,8 +215,11 @@ def _compute_model_predict( box = to_numpy_array(system["box"]) fparam = to_numpy_array(system.get("fparam", None)) aparam = to_numpy_array(system.get("aparam", None)) + charge_spin = to_numpy_array(system.get("charge_spin", None)) - sample_predict = model_forward(coord, atype, box, fparam=fparam, aparam=aparam) + sample_predict = model_forward( + coord, atype, box, fparam=fparam, aparam=aparam, charge_spin=charge_spin + ) for kk in keys: model_predict[kk].append( sample_predict[kk] # already numpy from model_forward diff --git a/deepmd/entrypoints/test.py b/deepmd/entrypoints/test.py index 604857c837..8f30d0c30c 100644 --- a/deepmd/entrypoints/test.py +++ b/deepmd/entrypoints/test.py @@ -590,6 +590,14 @@ def test_ener( ) if dp.get_dim_aparam() > 0: data.add("aparam", dp.get_dim_aparam(), atomic=True, must=True, high_prec=False) + if dp.has_chg_spin_ebd(): + data.add( + "charge_spin", + 2, + atomic=False, + must=not dp.has_default_chg_spin(), + high_prec=False, + ) if dp.has_spin: data.add("spin", 3, atomic=True, must=True, high_prec=False) data.add("force_mag", 3, atomic=True, must=False, high_prec=False) @@ -631,6 +639,10 @@ def test_ener( aparam = test_data["aparam"][:numb_test] else: aparam = None + if dp.has_chg_spin_ebd() and test_data.get("find_charge_spin", 0.0) != 0.0: + charge_spin = test_data["charge_spin"][:numb_test] + else: + charge_spin = None ret = dp.eval( coord, @@ -642,6 +654,7 @@ def test_ener( efield=efield, mixed_type=mixed_type, spin=spin, + charge_spin=charge_spin, ) energy = ret[0] force = ret[1] diff --git a/deepmd/infer/deep_eval.py b/deepmd/infer/deep_eval.py index 807414fa5d..557f3ddd23 100644 --- a/deepmd/infer/deep_eval.py +++ b/deepmd/infer/deep_eval.py @@ -166,6 +166,14 @@ def has_default_fparam(self) -> bool: """Check if the model has default frame parameters.""" return False + def has_chg_spin_ebd(self) -> bool: + """Check if the model has charge spin embedding.""" + return False + + def has_default_chg_spin(self) -> bool: + """Check if the model has default charge_spin values.""" + return False + @abstractmethod def get_dim_aparam(self) -> int: """Get the number (dimension) of atomic parameters of this DP.""" @@ -451,6 +459,14 @@ def has_default_fparam(self) -> bool: """Check if the model has default frame parameters.""" return self.deep_eval.has_default_fparam() + def has_chg_spin_ebd(self) -> bool: + """Check if the model has charge spin embedding.""" + return self.deep_eval.has_chg_spin_ebd() + + def has_default_chg_spin(self) -> bool: + """Check if the model has default charge_spin values.""" + return self.deep_eval.has_default_chg_spin() + def get_dim_aparam(self) -> int: """Get the number (dimension) of atomic parameters of this DP.""" return self.deep_eval.get_dim_aparam() diff --git a/deepmd/jax/atomic_model/dp_atomic_model.py b/deepmd/jax/atomic_model/dp_atomic_model.py index 319b8e94a2..74a1f481ea 100644 --- a/deepmd/jax/atomic_model/dp_atomic_model.py +++ b/deepmd/jax/atomic_model/dp_atomic_model.py @@ -58,6 +58,7 @@ def forward_common_atomic( fparam: jnp.ndarray | None = None, aparam: jnp.ndarray | None = None, comm_dict: dict | None = None, + charge_spin: jnp.ndarray | None = None, ) -> dict[str, jnp.ndarray]: del comm_dict # JAX path has no MPI ghost exchange return super().forward_common_atomic( @@ -67,6 +68,7 @@ def forward_common_atomic( mapping=mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, ) return jax_atomic_model diff --git a/deepmd/jax/atomic_model/linear_atomic_model.py b/deepmd/jax/atomic_model/linear_atomic_model.py index ecfc74cf95..1453e8f495 100644 --- a/deepmd/jax/atomic_model/linear_atomic_model.py +++ b/deepmd/jax/atomic_model/linear_atomic_model.py @@ -62,6 +62,7 @@ def forward_common_atomic( fparam: jnp.ndarray | None = None, aparam: jnp.ndarray | None = None, comm_dict: dict | None = None, + charge_spin: jnp.ndarray | None = None, ) -> dict[str, jnp.ndarray]: del comm_dict # JAX path has no MPI ghost exchange return super().forward_common_atomic( @@ -71,4 +72,5 @@ def forward_common_atomic( mapping=mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, ) diff --git a/deepmd/jax/atomic_model/pairtab_atomic_model.py b/deepmd/jax/atomic_model/pairtab_atomic_model.py index 0117bf1d2c..4f5a5d0ece 100644 --- a/deepmd/jax/atomic_model/pairtab_atomic_model.py +++ b/deepmd/jax/atomic_model/pairtab_atomic_model.py @@ -47,6 +47,7 @@ def forward_common_atomic( fparam: jnp.ndarray | None = None, aparam: jnp.ndarray | None = None, comm_dict: dict | None = None, + charge_spin: jnp.ndarray | None = None, ) -> dict[str, jnp.ndarray]: del comm_dict # JAX path has no MPI ghost exchange return super().forward_common_atomic( @@ -56,4 +57,5 @@ def forward_common_atomic( mapping=mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, ) diff --git a/deepmd/jax/jax2tf/tfmodel.py b/deepmd/jax/jax2tf/tfmodel.py index 1c968c8f41..2d820810cc 100644 --- a/deepmd/jax/jax2tf/tfmodel.py +++ b/deepmd/jax/jax2tf/tfmodel.py @@ -187,6 +187,7 @@ def call_lower( fparam: jnp.ndarray | None = None, aparam: jnp.ndarray | None = None, do_atomic_virial: bool = False, + charge_spin: jnp.ndarray | None = None, ) -> dict[str, jnp.ndarray]: if do_atomic_virial: call_lower = self._call_lower_atomic_virial diff --git a/deepmd/jax/model/base_model.py b/deepmd/jax/model/base_model.py index f99fccd276..481ca1656d 100644 --- a/deepmd/jax/model/base_model.py +++ b/deepmd/jax/model/base_model.py @@ -27,6 +27,7 @@ def forward_common_atomic( do_atomic_virial: bool = False, extended_coord_corr: jnp.ndarray | None = None, comm_dict: dict | None = None, + charge_spin: jnp.ndarray | None = None, ) -> dict[str, jnp.ndarray]: del comm_dict # JAX path has no MPI ghost exchange atomic_ret = self.atomic_model.forward_common_atomic( @@ -36,6 +37,7 @@ def forward_common_atomic( mapping=mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, ) atomic_output_def = self.atomic_output_def() model_predict = {} @@ -66,6 +68,7 @@ def eval_output( mapping: jnp.ndarray | None, fparam: jnp.ndarray | None, aparam: jnp.ndarray | None, + charge_spin_: jnp.ndarray | None, *, _kk: str = kk, _atom_axis: int = atom_axis, @@ -77,6 +80,9 @@ def eval_output( mapping=mapping[None, ...] if mapping is not None else None, fparam=fparam[None, ...] if fparam is not None else None, aparam=aparam[None, ...] if aparam is not None else None, + charge_spin=charge_spin_[None, ...] + if charge_spin_ is not None + else None, ) return jnp.sum(atomic_ret[_kk][0], axis=_atom_axis) @@ -89,6 +95,7 @@ def eval_output( mapping, fparam, aparam, + charge_spin, ) # extended_force: [nf, nall, *def, 3] def_ndim = len(vdef.shape) @@ -106,6 +113,7 @@ def eval_output( mapping, fparam, aparam, + charge_spin, ) kk_hessian = get_hessian_name(kk) model_predict[kk_hessian] = hessian @@ -127,6 +135,7 @@ def eval_ce( mapping: jnp.ndarray | None, fparam: jnp.ndarray | None, aparam: jnp.ndarray | None, + charge_spin_: jnp.ndarray | None, *, _kk: str = kk, _atom_axis: int = atom_axis - 1, @@ -139,6 +148,9 @@ def eval_ce( mapping=mapping[None, ...] if mapping is not None else None, fparam=fparam[None, ...] if fparam is not None else None, aparam=aparam[None, ...] if aparam is not None else None, + charge_spin=charge_spin_[None, ...] + if charge_spin_ is not None + else None, ) nloc = nlist.shape[0] cc_loc = jax.lax.stop_gradient(cc_ext)[:nloc, ...] @@ -156,6 +168,7 @@ def eval_ce( mapping, fparam, aparam, + charge_spin, ) # move the first 3 to the last # [nf, *def, nall, 3, 3] diff --git a/deepmd/jax/model/dp_model.py b/deepmd/jax/model/dp_model.py index 55239bb608..a265229e0e 100644 --- a/deepmd/jax/model/dp_model.py +++ b/deepmd/jax/model/dp_model.py @@ -57,6 +57,7 @@ def forward_common_atomic( do_atomic_virial: bool = False, extended_coord_corr: jnp.ndarray | None = None, comm_dict: dict | None = None, + charge_spin: jnp.ndarray | None = None, ) -> dict[str, jnp.ndarray]: del comm_dict # JAX path has no MPI ghost exchange return forward_common_atomic( @@ -69,6 +70,7 @@ def forward_common_atomic( aparam=aparam, do_atomic_virial=do_atomic_virial, extended_coord_corr=extended_coord_corr, + charge_spin=charge_spin, ) def format_nlist( diff --git a/deepmd/jax/model/dp_zbl_model.py b/deepmd/jax/model/dp_zbl_model.py index f2aa68ea1f..8d66084b8e 100644 --- a/deepmd/jax/model/dp_zbl_model.py +++ b/deepmd/jax/model/dp_zbl_model.py @@ -39,6 +39,7 @@ def forward_common_atomic( do_atomic_virial: bool = False, extended_coord_corr: jnp.ndarray | None = None, comm_dict: dict | None = None, + charge_spin: jnp.ndarray | None = None, ) -> dict[str, jnp.ndarray]: del comm_dict # JAX path has no MPI ghost exchange return forward_common_atomic( @@ -51,6 +52,7 @@ def forward_common_atomic( aparam=aparam, do_atomic_virial=do_atomic_virial, extended_coord_corr=extended_coord_corr, + charge_spin=charge_spin, ) def format_nlist( diff --git a/deepmd/jax/model/hlo.py b/deepmd/jax/model/hlo.py index c79bc727cf..8c1e85c59c 100644 --- a/deepmd/jax/model/hlo.py +++ b/deepmd/jax/model/hlo.py @@ -183,6 +183,7 @@ def call_lower( fparam: jnp.ndarray | None = None, aparam: jnp.ndarray | None = None, do_atomic_virial: bool = False, + charge_spin: jnp.ndarray | None = None, ) -> dict[str, jnp.ndarray]: if extended_coord.shape[1] > nlist.shape[1]: if do_atomic_virial: diff --git a/deepmd/pt/infer/deep_eval.py b/deepmd/pt/infer/deep_eval.py index 2e30b8574a..3a44bde4fc 100644 --- a/deepmd/pt/infer/deep_eval.py +++ b/deepmd/pt/infer/deep_eval.py @@ -252,6 +252,20 @@ def has_default_fparam(self) -> bool: # for compatibility with old models return False + def has_chg_spin_ebd(self) -> bool: + """Check if the model has charge spin embedding.""" + try: + return self.dp.model["Default"].has_chg_spin_ebd() + except AttributeError: + return False + + def has_default_chg_spin(self) -> bool: + """Check if the model has default charge_spin values.""" + try: + return self.dp.model["Default"].has_default_chg_spin() + except AttributeError: + return False + def get_intensive(self) -> bool: return self.dp.model["Default"].get_intensive() @@ -344,6 +358,7 @@ def eval( atomic: bool = False, fparam: np.ndarray | None = None, aparam: np.ndarray | None = None, + charge_spin: np.ndarray | None = None, **kwargs: Any, ) -> dict[str, np.ndarray]: """Evaluate the energy, force and virial by using this DP. @@ -393,7 +408,7 @@ def eval( request_defs = self._get_request_defs(atomic) if "spin" not in kwargs or kwargs["spin"] is None: out = self._eval_func(self._eval_model, numb_test, natoms)( - coords, cells, atom_types, fparam, aparam, request_defs + coords, cells, atom_types, fparam, aparam, request_defs, charge_spin ) else: out = self._eval_func(self._eval_model_spin, numb_test, natoms)( @@ -404,6 +419,7 @@ def eval( fparam, aparam, request_defs, + charge_spin, ) return dict( zip( @@ -505,6 +521,7 @@ def _eval_model( fparam: np.ndarray | None, aparam: np.ndarray | None, request_defs: list[OutputVariableDef], + charge_spin: np.ndarray | None, ) -> tuple[np.ndarray, ...]: model = self.dp.to(DEVICE) prec = NP_PRECISION_DICT[RESERVED_PRECISION_DICT[GLOBAL_PT_FLOAT_PRECISION]] @@ -546,6 +563,10 @@ def _eval_model( ) else: aparam_input = None + if charge_spin is not None: + charge_spin_input = to_torch_tensor(charge_spin.reshape(nframes, 2)) + else: + charge_spin_input = None do_atomic_virial = any( x.category == OutputVariableCategory.DERV_C for x in request_defs ) @@ -556,6 +577,7 @@ def _eval_model( do_atomic_virial=do_atomic_virial, fparam=fparam_input, aparam=aparam_input, + charge_spin=charge_spin_input, ) if isinstance(batch_output, tuple): batch_output = batch_output[0] @@ -583,6 +605,7 @@ def _eval_model_spin( fparam: np.ndarray | None, aparam: np.ndarray | None, request_defs: list[OutputVariableDef], + charge_spin: np.ndarray | None, ) -> tuple[np.ndarray, ...]: model = self.dp.to(DEVICE) @@ -624,6 +647,10 @@ def _eval_model_spin( ) else: aparam_input = None + if charge_spin is not None: + charge_spin_input = to_torch_tensor(charge_spin.reshape(nframes, 2)) + else: + charge_spin_input = None do_atomic_virial = any( x.category == OutputVariableCategory.DERV_C_REDU for x in request_defs @@ -636,6 +663,7 @@ def _eval_model_spin( do_atomic_virial=do_atomic_virial, fparam=fparam_input, aparam=aparam_input, + charge_spin=charge_spin_input, ) if isinstance(batch_output, tuple): batch_output = batch_output[0] diff --git a/deepmd/pt/model/atomic_model/base_atomic_model.py b/deepmd/pt/model/atomic_model/base_atomic_model.py index bfc67cf82b..8605db9359 100644 --- a/deepmd/pt/model/atomic_model/base_atomic_model.py +++ b/deepmd/pt/model/atomic_model/base_atomic_model.py @@ -189,6 +189,23 @@ def get_default_fparam(self) -> torch.Tensor | None: """Get the default frame parameters.""" return None + def has_chg_spin_ebd(self) -> bool: + """Check if the model has charge spin embedding.""" + return False + + @torch.jit.export + def get_dim_chg_spin(self) -> int: + """Get the dimension of charge_spin input.""" + return 0 + + def has_default_chg_spin(self) -> bool: + """Check if the model has default charge_spin values.""" + return False + + def get_default_chg_spin(self) -> torch.Tensor | None: + """Get the default charge_spin values.""" + return None + def _make_wrapped_sampler( self, sampled_func: Callable[[], list[dict]], @@ -305,6 +322,7 @@ def forward_common_atomic( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, comm_dict: dict[str, torch.Tensor] | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: """Common interface for atomic inference. @@ -356,6 +374,7 @@ def forward_common_atomic( fparam=fparam, aparam=aparam, comm_dict=comm_dict, + charge_spin=charge_spin, ) ret_dict = self.apply_out_stat(ret_dict, atype) @@ -386,6 +405,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, comm_dict: dict[str, torch.Tensor] | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: return self.forward_common_atomic( extended_coord, @@ -395,6 +415,7 @@ def forward( fparam=fparam, aparam=aparam, comm_dict=comm_dict, + charge_spin=charge_spin, ) def change_type_map( @@ -621,6 +642,7 @@ def model_forward( box: torch.Tensor | None, fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: with ( torch.no_grad() @@ -638,6 +660,10 @@ def model_forward( mixed_types=self.mixed_types(), box=box, ) + if charge_spin is not None and not isinstance( + charge_spin, torch.Tensor + ): + charge_spin = to_torch_tensor(charge_spin) atomic_ret = self.forward_common_atomic( extended_coord, extended_atype, @@ -645,6 +671,7 @@ def model_forward( mapping=mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, ) return {kk: vv.detach() for kk, vv in atomic_ret.items()} diff --git a/deepmd/pt/model/atomic_model/dp_atomic_model.py b/deepmd/pt/model/atomic_model/dp_atomic_model.py index efb2a532e5..783ee9e766 100644 --- a/deepmd/pt/model/atomic_model/dp_atomic_model.py +++ b/deepmd/pt/model/atomic_model/dp_atomic_model.py @@ -244,6 +244,7 @@ def forward_atomic( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, comm_dict: dict[str, torch.Tensor] | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: """Return atomic prediction. @@ -273,20 +274,12 @@ def forward_atomic( if self.do_grad_r() or self.do_grad_c(): extended_coord.requires_grad_(True) - # Handle default fparam if fitting net supports it - if ( - hasattr(self.fitting_net, "get_dim_fparam") - and self.fitting_net.get_dim_fparam() > 0 - and fparam is None - ): - # use default fparam - default_fparam_tensor = self.fitting_net.get_default_fparam() - assert default_fparam_tensor is not None - fparam_input_for_des = torch.tile( - default_fparam_tensor.unsqueeze(0), [nframes, 1] - ) - else: - fparam_input_for_des = fparam + # Handle default chg_spin if descriptor supports it + if self.add_chg_spin_ebd and charge_spin is None: + default_cs_tensor = self.descriptor.get_default_chg_spin() + if default_cs_tensor is not None: + default_cs_tensor = default_cs_tensor.to(device=extended_coord.device) + charge_spin = torch.tile(default_cs_tensor.unsqueeze(0), [nframes, 1]) descriptor, rot_mat, g2, h2, sw = self.descriptor( extended_coord, @@ -294,7 +287,7 @@ def forward_atomic( nlist, mapping=mapping, comm_dict=comm_dict, - fparam=fparam_input_for_des if self.add_chg_spin_ebd else None, + charge_spin=charge_spin if self.add_chg_spin_ebd else None, ) assert descriptor is not None if self.enable_eval_descriptor_hook: @@ -394,6 +387,32 @@ def has_default_fparam(self) -> bool: def get_default_fparam(self) -> torch.Tensor | None: return self.fitting_net.get_default_fparam() + @torch.jit.export + def has_chg_spin_ebd(self) -> bool: + """Check if the model has charge spin embedding.""" + return self.add_chg_spin_ebd + + @torch.jit.export + def get_dim_chg_spin(self) -> int: + """Get the dimension of charge_spin input.""" + if self.add_chg_spin_ebd: + return self.descriptor.get_dim_chg_spin() + return 0 + + @torch.jit.export + def has_default_chg_spin(self) -> bool: + """Check if the model has default charge_spin values.""" + if self.add_chg_spin_ebd: + return self.descriptor.has_default_chg_spin() + return False + + @torch.jit.export + def get_default_chg_spin(self) -> torch.Tensor | None: + """Get the default charge_spin values as a tensor.""" + if self.add_chg_spin_ebd and self.descriptor.has_default_chg_spin(): + return self.descriptor.get_default_chg_spin() + return None + def get_dim_aparam(self) -> int: """Get the number (dimension) of atomic parameters of this atomic model.""" return self.fitting_net.get_dim_aparam() diff --git a/deepmd/pt/model/atomic_model/linear_atomic_model.py b/deepmd/pt/model/atomic_model/linear_atomic_model.py index 4c415658e2..5c0f616634 100644 --- a/deepmd/pt/model/atomic_model/linear_atomic_model.py +++ b/deepmd/pt/model/atomic_model/linear_atomic_model.py @@ -233,6 +233,7 @@ def forward_atomic( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, comm_dict: dict[str, torch.Tensor] | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: """Return atomic prediction. @@ -292,6 +293,7 @@ def forward_atomic( fparam, aparam, comm_dict=comm_dict, + charge_spin=charge_spin, )["energy"] ) weights = self._compute_weight(extended_coord, extended_atype, nlists_) diff --git a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py index e838a7a24d..5750f7cfd1 100644 --- a/deepmd/pt/model/atomic_model/pairtab_atomic_model.py +++ b/deepmd/pt/model/atomic_model/pairtab_atomic_model.py @@ -271,6 +271,7 @@ def forward_atomic( aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, comm_dict: dict[str, torch.Tensor] | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: nframes, nloc, nnei = nlist.shape extended_coord = extended_coord.view(nframes, -1, 3) diff --git a/deepmd/pt/model/descriptor/dpa1.py b/deepmd/pt/model/descriptor/dpa1.py index b4ff2dd46f..9751db621c 100644 --- a/deepmd/pt/model/descriptor/dpa1.py +++ b/deepmd/pt/model/descriptor/dpa1.py @@ -325,6 +325,18 @@ def __init__( for param in self.parameters(): param.requires_grad = trainable + def get_dim_chg_spin(self) -> int: + """Returns the dimension of charge_spin input (0 if not supported).""" + return 0 + + def has_default_chg_spin(self) -> bool: + """Returns whether the descriptor has a default charge_spin value.""" + return False + + def get_default_chg_spin(self) -> None: + """Returns the default charge_spin value, or None.""" + return None + def get_rcut(self) -> float: """Returns the cut-off radius.""" return self.se_atten.get_rcut() @@ -672,6 +684,7 @@ def forward( mapping: torch.Tensor | None = None, comm_dict: dict[str, torch.Tensor] | None = None, fparam: torch.Tensor | None = None, + charge_spin: torch.Tensor | None = None, ) -> tuple[ torch.Tensor, torch.Tensor | None, diff --git a/deepmd/pt/model/descriptor/dpa2.py b/deepmd/pt/model/descriptor/dpa2.py index 89aff38168..d6e38999b7 100644 --- a/deepmd/pt/model/descriptor/dpa2.py +++ b/deepmd/pt/model/descriptor/dpa2.py @@ -330,6 +330,18 @@ def init_subclass_params(sub_data: Any, sub_class: Any) -> Any: param.requires_grad = trainable self.compress = False + def get_dim_chg_spin(self) -> int: + """Returns the dimension of charge_spin input (0 if not supported).""" + return 0 + + def has_default_chg_spin(self) -> bool: + """Returns whether the descriptor has a default charge_spin value.""" + return False + + def get_default_chg_spin(self) -> None: + """Returns the default charge_spin value, or None.""" + return None + def get_rcut(self) -> float: """Returns the cut-off radius.""" return self.rcut @@ -717,6 +729,7 @@ def forward( mapping: torch.Tensor | None = None, comm_dict: dict[str, torch.Tensor] | None = None, fparam: torch.Tensor | None = None, + charge_spin: torch.Tensor | None = None, ) -> tuple[ torch.Tensor, torch.Tensor | None, diff --git a/deepmd/pt/model/descriptor/dpa3.py b/deepmd/pt/model/descriptor/dpa3.py index a5f79280fa..99f315af17 100644 --- a/deepmd/pt/model/descriptor/dpa3.py +++ b/deepmd/pt/model/descriptor/dpa3.py @@ -4,6 +4,7 @@ ) from typing import ( Any, + Optional, ) import torch @@ -122,8 +123,13 @@ def __init__( use_loc_mapping: bool = True, type_map: list[str] | None = None, add_chg_spin_ebd: bool = False, + default_chg_spin: list[float] | None = None, ) -> None: super().__init__() + if default_chg_spin is not None and len(default_chg_spin) != 2: + raise ValueError( + "default_chg_spin must be a list of length 2 [charge, spin]." + ) def init_subclass_params(sub_data: Any, sub_class: Any) -> Any: if isinstance(sub_data, dict): @@ -178,6 +184,7 @@ def init_subclass_params(sub_data: Any, sub_class: Any) -> Any: self.use_econf_tebd = use_econf_tebd self.add_chg_spin_ebd = add_chg_spin_ebd + self.default_chg_spin = default_chg_spin self.use_loc_mapping = use_loc_mapping self.use_tebd_bias = use_tebd_bias self.type_map = type_map @@ -198,14 +205,14 @@ def init_subclass_params(sub_data: Any, sub_class: Any) -> Any: if self.add_chg_spin_ebd: self.act = ActivationFn(activation_function) - # -100 ~ 100 is a conservative bound + # charge range [-100, 99] mapped to indices [0, 199] self.chg_embedding = TypeEmbedNet( 200, self.tebd_dim, precision=precision, seed=child_seed(seed, 3), ) - # 100 is a conservative upper bound + # spin range [0, 99] mapped to indices [0, 99] self.spin_embedding = TypeEmbedNet( 100, self.tebd_dim, @@ -250,6 +257,27 @@ def get_rcut(self) -> float: """Returns the cut-off radius.""" return self.rcut + @torch.jit.export + def get_dim_chg_spin(self) -> int: + """Get the dimension of charge_spin input.""" + return 2 if self.add_chg_spin_ebd else 0 + + @torch.jit.export + def has_default_chg_spin(self) -> bool: + """Check if the descriptor has default charge_spin values.""" + return self.default_chg_spin is not None + + @torch.jit.export + def get_default_chg_spin(self) -> Optional[torch.Tensor]: # noqa: UP045 + """Get the default charge_spin values as a tensor.""" + if self.default_chg_spin is None: + return None + return torch.tensor( + self.default_chg_spin, + dtype=self.prec, + device=env.DEVICE, + ) + def get_rcut_smth(self) -> float: """Returns the radius where the neighbor information starts to smoothly decay to 0.""" return self.rcut_smth @@ -428,6 +456,7 @@ def serialize(self) -> dict: "use_tebd_bias": self.use_tebd_bias, "use_loc_mapping": self.use_loc_mapping, "add_chg_spin_ebd": self.add_chg_spin_ebd, + "default_chg_spin": self.default_chg_spin, "type_map": self.type_map, "type_embedding": self.type_embedding.embedding.serialize(), } @@ -505,6 +534,7 @@ def forward( mapping: torch.Tensor | None = None, comm_dict: dict[str, torch.Tensor] | None = None, fparam: torch.Tensor | None = None, + charge_spin: torch.Tensor | None = None, ) -> tuple[ torch.Tensor, torch.Tensor | None, @@ -556,12 +586,22 @@ def forward( node_ebd_ext = self.type_embedding(extended_atype) if self.add_chg_spin_ebd: - assert fparam is not None + assert charge_spin is not None assert self.chg_embedding is not None assert self.spin_embedding is not None - charge = fparam[:, 0].to(dtype=torch.int64) + 100 - spin = fparam[:, 1].to(dtype=torch.int64) - chg_ebd = self.chg_embedding(charge) + charge = charge_spin[:, 0].to(dtype=torch.int64) + spin = charge_spin[:, 1].to(dtype=torch.int64) + # Validate charge range [-100, 99] (200 embedding entries) + if torch.any(charge < -100) or torch.any(charge > 99): + raise ValueError( + f"charge must be in range [-100, 99], got min={charge.min().item()}, max={charge.max().item()}" + ) + # Validate spin range [0, 99] (100 embedding entries) + if torch.any(spin < 0) or torch.any(spin >= 100): + raise ValueError( + f"spin must be in range [0, 99], got min={spin.min().item()}, max={spin.max().item()}" + ) + chg_ebd = self.chg_embedding(charge + 100) spin_ebd = self.spin_embedding(spin) sys_cs_embd = self.act( self.mix_cs_mlp(torch.cat((chg_ebd, spin_ebd), dim=-1)) diff --git a/deepmd/pt/model/descriptor/hybrid.py b/deepmd/pt/model/descriptor/hybrid.py index 55c1f9d2e3..97a49b750a 100644 --- a/deepmd/pt/model/descriptor/hybrid.py +++ b/deepmd/pt/model/descriptor/hybrid.py @@ -99,6 +99,45 @@ def __init__( ).astype(np.int64) self.nlist_cut_idx.append(to_torch_tensor(cut_idx)) + def get_dim_chg_spin(self) -> int: + """Returns the dimension of charge_spin input (0 if not supported).""" + # JIT-compiled via DPAtomicModel.get_dim_chg_spin; avoid generator + # expressions and `max(..., default=...)` which TorchScript rejects. + dim: int = 0 + for descrpt in self.descrpt_list: + d = descrpt.get_dim_chg_spin() + if d > dim: + dim = d + return dim + + def has_default_chg_spin(self) -> bool: + """Returns whether the descriptor has a default charge_spin value.""" + default_chg_spin: torch.Tensor | None = None + found_chg_spin: bool = False + for descrpt in self.descrpt_list: + if descrpt.get_dim_chg_spin() > 0: + found_chg_spin = True + if not descrpt.has_default_chg_spin(): + return False + child_default_chg_spin = descrpt.get_default_chg_spin() + if child_default_chg_spin is None: + return False + if default_chg_spin is None: + default_chg_spin = child_default_chg_spin + elif not torch.equal(default_chg_spin, child_default_chg_spin): + return False + return found_chg_spin + + @torch.jit.export + def get_default_chg_spin(self) -> Optional[torch.Tensor]: # noqa: UP045 + """Returns the default charge_spin value, or None.""" + if not self.has_default_chg_spin(): + return None + for descrpt in self.descrpt_list: + if descrpt.get_dim_chg_spin() > 0: + return descrpt.get_default_chg_spin() + return None + def get_rcut(self) -> float: """Returns the cut-off radius.""" # do not use numpy here - jit is not happy @@ -269,6 +308,7 @@ def forward( mapping: torch.Tensor | None = None, comm_dict: dict[str, torch.Tensor] | None = None, fparam: torch.Tensor | None = None, + charge_spin: torch.Tensor | None = None, ) -> tuple[ torch.Tensor, torch.Tensor | None, @@ -332,7 +372,15 @@ def forward( nl = nl_distinguish_types[ :, :, self.nlist_cut_idx[ii].to(atype_ext.device) ] - odescriptor, gr, g2, h2, sw = descrpt(coord_ext, atype_ext, nl, mapping) + odescriptor, gr, g2, h2, sw = descrpt( + coord_ext, + atype_ext, + nl, + mapping, + comm_dict=comm_dict, + fparam=fparam, + charge_spin=charge_spin, + ) out_descriptor.append(odescriptor) if gr is not None: out_gr.append(gr) diff --git a/deepmd/pt/model/descriptor/se_a.py b/deepmd/pt/model/descriptor/se_a.py index d4ee032d49..d840c8c001 100644 --- a/deepmd/pt/model/descriptor/se_a.py +++ b/deepmd/pt/model/descriptor/se_a.py @@ -137,6 +137,18 @@ def __init__( seed=seed, ) + def get_dim_chg_spin(self) -> int: + """Returns the dimension of charge_spin input (0 if not supported).""" + return 0 + + def has_default_chg_spin(self) -> bool: + """Returns whether the descriptor has a default charge_spin value.""" + return False + + def get_default_chg_spin(self) -> None: + """Returns the default charge_spin value, or None.""" + return None + def get_rcut(self) -> float: """Returns the cut-off radius.""" return self.sea.get_rcut() @@ -309,6 +321,7 @@ def forward( mapping: torch.Tensor | None = None, comm_dict: dict[str, torch.Tensor] | None = None, fparam: torch.Tensor | None = None, + charge_spin: torch.Tensor | None = None, ) -> tuple[ torch.Tensor, torch.Tensor | None, diff --git a/deepmd/pt/model/descriptor/se_r.py b/deepmd/pt/model/descriptor/se_r.py index 92ef4c800d..654e2e16bc 100644 --- a/deepmd/pt/model/descriptor/se_r.py +++ b/deepmd/pt/model/descriptor/se_r.py @@ -167,6 +167,18 @@ def __init__( ] ) + def get_dim_chg_spin(self) -> int: + """Returns the dimension of charge_spin input (0 if not supported).""" + return 0 + + def has_default_chg_spin(self) -> bool: + """Returns whether the descriptor has a default charge_spin value.""" + return False + + def get_default_chg_spin(self) -> None: + """Returns the default charge_spin value, or None.""" + return None + def get_rcut(self) -> float: """Returns the cut-off radius.""" return self.rcut @@ -428,6 +440,7 @@ def forward( mapping: torch.Tensor | None = None, comm_dict: dict[str, torch.Tensor] | None = None, fparam: torch.Tensor | None = None, + charge_spin: torch.Tensor | None = None, ) -> tuple[ torch.Tensor, torch.Tensor | None, diff --git a/deepmd/pt/model/descriptor/se_t.py b/deepmd/pt/model/descriptor/se_t.py index 9e0de85f49..bd2a9a60cd 100644 --- a/deepmd/pt/model/descriptor/se_t.py +++ b/deepmd/pt/model/descriptor/se_t.py @@ -171,6 +171,18 @@ def __init__( seed=seed, ) + def get_dim_chg_spin(self) -> int: + """Returns the dimension of charge_spin input (0 if not supported).""" + return 0 + + def has_default_chg_spin(self) -> bool: + """Returns whether the descriptor has a default charge_spin value.""" + return False + + def get_default_chg_spin(self) -> None: + """Returns the default charge_spin value, or None.""" + return None + def get_rcut(self) -> float: """Returns the cut-off radius.""" return self.seat.get_rcut() @@ -344,6 +356,7 @@ def forward( mapping: torch.Tensor | None = None, comm_dict: dict[str, torch.Tensor] | None = None, fparam: torch.Tensor | None = None, + charge_spin: torch.Tensor | None = None, ) -> tuple[ torch.Tensor, torch.Tensor | None, diff --git a/deepmd/pt/model/descriptor/se_t_tebd.py b/deepmd/pt/model/descriptor/se_t_tebd.py index f3c7544549..6937bb99e8 100644 --- a/deepmd/pt/model/descriptor/se_t_tebd.py +++ b/deepmd/pt/model/descriptor/se_t_tebd.py @@ -210,6 +210,18 @@ def __init__( for param in self.parameters(): param.requires_grad = trainable + def get_dim_chg_spin(self) -> int: + """Returns the dimension of charge_spin input (0 if not supported).""" + return 0 + + def has_default_chg_spin(self) -> bool: + """Returns whether the descriptor has a default charge_spin value.""" + return False + + def get_default_chg_spin(self) -> None: + """Returns the default charge_spin value, or None.""" + return None + def get_rcut(self) -> float: """Returns the cut-off radius.""" return self.se_ttebd.get_rcut() @@ -442,6 +454,7 @@ def forward( mapping: torch.Tensor | None = None, comm_dict: dict[str, torch.Tensor] | None = None, fparam: torch.Tensor | None = None, + charge_spin: torch.Tensor | None = None, ) -> tuple[ torch.Tensor, torch.Tensor | None, diff --git a/deepmd/pt/model/model/dipole_model.py b/deepmd/pt/model/model/dipole_model.py index 9bd52dd428..7301659958 100644 --- a/deepmd/pt/model/model/dipole_model.py +++ b/deepmd/pt/model/model/dipole_model.py @@ -60,6 +60,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common( coord, @@ -68,6 +69,7 @@ def forward( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) if self.get_fitting_net() is not None: model_predict = {} @@ -97,6 +99,7 @@ def forward_lower( aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, comm_dict: dict[str, torch.Tensor] | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common_lower( extended_coord, @@ -108,6 +111,7 @@ def forward_lower( do_atomic_virial=do_atomic_virial, comm_dict=comm_dict, extra_nlist_sort=self.need_sorted_nlist_for_lower(), + charge_spin=charge_spin, ) if self.get_fitting_net() is not None: model_predict = {} diff --git a/deepmd/pt/model/model/dos_model.py b/deepmd/pt/model/model/dos_model.py index d28487ed9c..daebc2fb9f 100644 --- a/deepmd/pt/model/model/dos_model.py +++ b/deepmd/pt/model/model/dos_model.py @@ -52,6 +52,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common( coord, @@ -60,6 +61,7 @@ def forward( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) if self.get_fitting_net() is not None: model_predict = {} @@ -89,6 +91,7 @@ def forward_lower( aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, comm_dict: dict[str, torch.Tensor] | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common_lower( extended_coord, @@ -100,6 +103,7 @@ def forward_lower( do_atomic_virial=do_atomic_virial, comm_dict=comm_dict, extra_nlist_sort=self.need_sorted_nlist_for_lower(), + charge_spin=charge_spin, ) if self.get_fitting_net() is not None: model_predict = {} diff --git a/deepmd/pt/model/model/dp_linear_model.py b/deepmd/pt/model/model/dp_linear_model.py index b95f568cb1..c3004b5a5d 100644 --- a/deepmd/pt/model/model/dp_linear_model.py +++ b/deepmd/pt/model/model/dp_linear_model.py @@ -65,6 +65,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common( coord, @@ -73,6 +74,7 @@ def forward( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) model_predict = {} @@ -101,6 +103,7 @@ def forward_lower( aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, comm_dict: dict[str, torch.Tensor] | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common_lower( extended_coord, @@ -112,6 +115,7 @@ def forward_lower( do_atomic_virial=do_atomic_virial, comm_dict=comm_dict, extra_nlist_sort=self.need_sorted_nlist_for_lower(), + charge_spin=charge_spin, ) model_predict = {} diff --git a/deepmd/pt/model/model/dp_zbl_model.py b/deepmd/pt/model/model/dp_zbl_model.py index ea2cd17f38..0de2d7fadd 100644 --- a/deepmd/pt/model/model/dp_zbl_model.py +++ b/deepmd/pt/model/model/dp_zbl_model.py @@ -62,6 +62,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common( coord, @@ -70,6 +71,7 @@ def forward( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) model_predict = {} @@ -98,6 +100,7 @@ def forward_lower( aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, comm_dict: dict[str, torch.Tensor] | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common_lower( extended_coord, @@ -109,6 +112,7 @@ def forward_lower( do_atomic_virial=do_atomic_virial, comm_dict=comm_dict, extra_nlist_sort=self.need_sorted_nlist_for_lower(), + charge_spin=charge_spin, ) model_predict = {} diff --git a/deepmd/pt/model/model/ener_model.py b/deepmd/pt/model/model/ener_model.py index 1680d1e258..28387553fb 100644 --- a/deepmd/pt/model/model/ener_model.py +++ b/deepmd/pt/model/model/ener_model.py @@ -72,6 +72,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common( coord, @@ -80,6 +81,7 @@ def forward( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) if self.get_fitting_net() is not None: model_predict = {} @@ -115,6 +117,7 @@ def forward_lower( aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, comm_dict: dict[str, torch.Tensor] | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common_lower( extended_coord, @@ -126,6 +129,7 @@ def forward_lower( do_atomic_virial=do_atomic_virial, comm_dict=comm_dict, extra_nlist_sort=self.need_sorted_nlist_for_lower(), + charge_spin=charge_spin, ) if self.get_fitting_net() is not None: model_predict = {} diff --git a/deepmd/pt/model/model/make_hessian_model.py b/deepmd/pt/model/model/make_hessian_model.py index 1b1bc3feba..c9e8eea078 100644 --- a/deepmd/pt/model/model/make_hessian_model.py +++ b/deepmd/pt/model/model/make_hessian_model.py @@ -68,6 +68,7 @@ def forward_common( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: """Return model prediction. @@ -101,6 +102,7 @@ def forward_common( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) vdef = self.atomic_output_def() hess_yes = [vdef[kk].r_hessian for kk in vdef.keys()] diff --git a/deepmd/pt/model/model/make_model.py b/deepmd/pt/model/model/make_model.py index 83e0209ad8..713eab3d8c 100644 --- a/deepmd/pt/model/model/make_model.py +++ b/deepmd/pt/model/model/make_model.py @@ -139,6 +139,7 @@ def forward_common( aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, coord_corr_for_virial: torch.Tensor | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: """Return model prediction. @@ -204,6 +205,7 @@ def forward_common( fparam=fp, aparam=ap, extended_coord_corr=extended_coord_corr, + charge_spin=charge_spin, ) model_predict = communicate_extended_output( model_predict_lower, @@ -259,6 +261,7 @@ def forward_common_lower( comm_dict: dict[str, torch.Tensor] | None = None, extra_nlist_sort: bool = False, extended_coord_corr: torch.Tensor | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: """Return model prediction. Lower interface that takes extended atomic coordinates and types, nlist, and mapping @@ -311,6 +314,7 @@ def forward_common_lower( fparam=fp, aparam=ap, comm_dict=comm_dict, + charge_spin=charge_spin, ) model_predict = fit_output_to_model_output( atomic_ret, @@ -551,6 +555,26 @@ def has_default_fparam(self) -> bool: def get_default_fparam(self) -> torch.Tensor | None: return self.atomic_model.get_default_fparam() + @torch.jit.export + def has_chg_spin_ebd(self) -> bool: + """Check if the model has charge spin embedding.""" + return self.atomic_model.has_chg_spin_ebd() + + @torch.jit.export + def get_dim_chg_spin(self) -> int: + """Get the dimension of charge_spin input.""" + return self.atomic_model.get_dim_chg_spin() + + @torch.jit.export + def has_default_chg_spin(self) -> bool: + """Check if the model has default charge_spin values.""" + return self.atomic_model.has_default_chg_spin() + + @torch.jit.export + def get_default_chg_spin(self) -> torch.Tensor | None: + """Get the default charge_spin values.""" + return self.atomic_model.get_default_chg_spin() + @torch.jit.export def get_dim_aparam(self) -> int: """Get the number (dimension) of atomic parameters of this atomic model.""" @@ -670,6 +694,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: # directly call the forward_common method when no specific transform rule return self.forward_common( @@ -679,6 +704,7 @@ def forward( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) return CM diff --git a/deepmd/pt/model/model/polar_model.py b/deepmd/pt/model/model/polar_model.py index 7c9550dc3a..78bbd069a8 100644 --- a/deepmd/pt/model/model/polar_model.py +++ b/deepmd/pt/model/model/polar_model.py @@ -55,6 +55,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common( coord, @@ -63,6 +64,7 @@ def forward( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) if self.get_fitting_net() is not None: model_predict = {} @@ -86,6 +88,7 @@ def forward_lower( aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, comm_dict: dict[str, torch.Tensor] | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common_lower( extended_coord, @@ -97,6 +100,7 @@ def forward_lower( do_atomic_virial=do_atomic_virial, comm_dict=comm_dict, extra_nlist_sort=self.need_sorted_nlist_for_lower(), + charge_spin=charge_spin, ) if self.get_fitting_net() is not None: model_predict = {} diff --git a/deepmd/pt/model/model/property_model.py b/deepmd/pt/model/model/property_model.py index c24ca7fa64..0c0e76d6c0 100644 --- a/deepmd/pt/model/model/property_model.py +++ b/deepmd/pt/model/model/property_model.py @@ -55,6 +55,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common( coord, @@ -63,6 +64,7 @@ def forward( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, ) model_predict = {} model_predict[f"atom_{self.get_var_name()}"] = model_ret[self.get_var_name()] @@ -97,6 +99,7 @@ def forward_lower( aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, comm_dict: dict[str, torch.Tensor] | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common_lower( extended_coord, @@ -108,6 +111,7 @@ def forward_lower( do_atomic_virial=do_atomic_virial, comm_dict=comm_dict, extra_nlist_sort=self.need_sorted_nlist_for_lower(), + charge_spin=charge_spin, ) model_predict = {} model_predict[f"atom_{self.get_var_name()}"] = model_ret[self.get_var_name()] diff --git a/deepmd/pt/model/model/spin_model.py b/deepmd/pt/model/model/spin_model.py index 91c6e2ea71..e0e1002bf0 100644 --- a/deepmd/pt/model/model/spin_model.py +++ b/deepmd/pt/model/model/spin_model.py @@ -527,6 +527,7 @@ def forward_common( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: nframes, nloc = atype.shape coord_updated, atype_updated, coord_corr_for_virial = self.process_spin_input( @@ -540,6 +541,7 @@ def forward_common( box, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, coord_corr_for_virial=coord_corr_for_virial, ) @@ -581,6 +583,7 @@ def forward_common_lower( do_atomic_virial: bool = False, comm_dict: dict[str, torch.Tensor] | None = None, extra_nlist_sort: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: nframes, nloc = nlist.shape[:2] ( @@ -601,6 +604,7 @@ def forward_common_lower( mapping=mapping_updated, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, comm_dict=comm_dict, extra_nlist_sort=extra_nlist_sort, @@ -696,6 +700,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common( coord, @@ -704,6 +709,7 @@ def forward( box, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -731,6 +737,7 @@ def forward_lower( aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, comm_dict: dict[str, torch.Tensor] | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.forward_common_lower( extended_coord, @@ -740,6 +747,7 @@ def forward_lower( mapping=mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, comm_dict=comm_dict, extra_nlist_sort=self.backbone_model.need_sorted_nlist_for_lower(), diff --git a/deepmd/pt/modifier/base_modifier.py b/deepmd/pt/modifier/base_modifier.py index 5a8c6538b0..957e7a81dc 100644 --- a/deepmd/pt/modifier/base_modifier.py +++ b/deepmd/pt/modifier/base_modifier.py @@ -83,6 +83,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: """Compute energy, force, and virial corrections.""" diff --git a/deepmd/pt/train/training.py b/deepmd/pt/train/training.py index 012cdb3a65..a5b799dbdc 100644 --- a/deepmd/pt/train/training.py +++ b/deepmd/pt/train/training.py @@ -2036,13 +2036,20 @@ def get_data( "box", "fparam", "aparam", + "charge_spin", ] input_dict = dict.fromkeys(input_keys) label_dict = {} for item_key in batch_data: if item_key in input_keys: - if item_key != "fparam" or batch_data["find_fparam"] != 0.0: - input_dict[item_key] = batch_data[item_key] + if item_key == "fparam" and batch_data.get("find_fparam", 1.0) == 0.0: + continue + if ( + item_key == "charge_spin" + and batch_data.get("find_charge_spin", 1.0) == 0.0 + ): + continue + input_dict[item_key] = batch_data[item_key] else: if item_key not in ["sid", "fid"]: label_dict[item_key] = batch_data[item_key] @@ -2156,6 +2163,20 @@ def get_additional_data_requirement(_model: Any) -> list[DataRequirementItem]: DataRequirementItem("spin", ndof=3, atomic=True, must=True) ] additional_data_requirement += spin_requirement_items + if _model.has_chg_spin_ebd(): + has_default_cs = _model.has_default_chg_spin() + cs_default = ( + _model.get_default_chg_spin().cpu().numpy() if has_default_cs else 0.0 + ) + additional_data_requirement.append( + DataRequirementItem( + "charge_spin", + ndof=2, + atomic=False, + must=not has_default_cs, + default=cs_default, + ) + ) return additional_data_requirement diff --git a/deepmd/pt/train/wrapper.py b/deepmd/pt/train/wrapper.py index ddb4a4323d..1d741dd534 100644 --- a/deepmd/pt/train/wrapper.py +++ b/deepmd/pt/train/wrapper.py @@ -165,6 +165,7 @@ def forward( do_atomic_virial: bool = False, fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, + charge_spin: torch.Tensor | None = None, ) -> tuple[Any, Any, Any]: if not self.multi_task: task_key = "Default" @@ -179,6 +180,7 @@ def forward( "do_atomic_virial": do_atomic_virial, "fparam": fparam, "aparam": aparam, + "charge_spin": charge_spin, } has_spin = getattr(self.model[task_key], "has_spin", False) if callable(has_spin): diff --git a/deepmd/pt/utils/stat.py b/deepmd/pt/utils/stat.py index dc4d43f508..f8c3685b78 100644 --- a/deepmd/pt/utils/stat.py +++ b/deepmd/pt/utils/stat.py @@ -189,6 +189,7 @@ def _compute_model_predict( ) fparam = system.get("fparam", None) aparam = system.get("aparam", None) + charge_spin = system.get("charge_spin", None) def model_forward_auto_batch_size(*args: Any, **kwargs: Any) -> Any: return auto_batch_size.execute_all( @@ -200,7 +201,7 @@ def model_forward_auto_batch_size(*args: Any, **kwargs: Any) -> Any: ) sample_predict = model_forward_auto_batch_size( - coord, atype, box, fparam=fparam, aparam=aparam + coord, atype, box, fparam=fparam, aparam=aparam, charge_spin=charge_spin ) for kk in keys: model_predict[kk].append( diff --git a/deepmd/pt_expt/descriptor/dpa1.py b/deepmd/pt_expt/descriptor/dpa1.py index c43b07f9c2..a0ea96b2eb 100644 --- a/deepmd/pt_expt/descriptor/dpa1.py +++ b/deepmd/pt_expt/descriptor/dpa1.py @@ -184,6 +184,7 @@ def call( mapping: torch.Tensor | None = None, fparam: torch.Tensor | None = None, comm_dict: dict | None = None, + charge_spin: torch.Tensor | None = None, ) -> Any: if not self.compress: return DescrptDPA1DP.call.__wrapped__( diff --git a/deepmd/pt_expt/descriptor/dpa2.py b/deepmd/pt_expt/descriptor/dpa2.py index 21c392cd3c..0c8535ab00 100644 --- a/deepmd/pt_expt/descriptor/dpa2.py +++ b/deepmd/pt_expt/descriptor/dpa2.py @@ -234,6 +234,7 @@ def call( mapping: torch.Tensor | None = None, fparam: torch.Tensor | None = None, comm_dict: dict | None = None, + charge_spin: torch.Tensor | None = None, ) -> Any: if not self.compress: return DescrptDPA2DP.call.__wrapped__( diff --git a/deepmd/pt_expt/descriptor/se_e2_a.py b/deepmd/pt_expt/descriptor/se_e2_a.py index 45120c6d5d..5c9a8131e6 100644 --- a/deepmd/pt_expt/descriptor/se_e2_a.py +++ b/deepmd/pt_expt/descriptor/se_e2_a.py @@ -140,6 +140,7 @@ def call( mapping: torch.Tensor | None = None, fparam: torch.Tensor | None = None, comm_dict: dict | None = None, + charge_spin: torch.Tensor | None = None, ) -> Any: if not self.compress: return DescrptSeADP.call.__wrapped__( diff --git a/deepmd/pt_expt/descriptor/se_r.py b/deepmd/pt_expt/descriptor/se_r.py index ab32be1131..024c51fabb 100644 --- a/deepmd/pt_expt/descriptor/se_r.py +++ b/deepmd/pt_expt/descriptor/se_r.py @@ -129,6 +129,7 @@ def call( mapping: torch.Tensor | None = None, fparam: torch.Tensor | None = None, comm_dict: dict | None = None, + charge_spin: torch.Tensor | None = None, ) -> Any: if not self.compress: return DescrptSeRDP.call.__wrapped__( diff --git a/deepmd/pt_expt/descriptor/se_t.py b/deepmd/pt_expt/descriptor/se_t.py index 69d6183642..bc0414b6de 100644 --- a/deepmd/pt_expt/descriptor/se_t.py +++ b/deepmd/pt_expt/descriptor/se_t.py @@ -140,6 +140,7 @@ def call( mapping: torch.Tensor | None = None, fparam: torch.Tensor | None = None, comm_dict: dict | None = None, + charge_spin: torch.Tensor | None = None, ) -> Any: if not self.compress: return DescrptSeTDP.call.__wrapped__( diff --git a/deepmd/pt_expt/descriptor/se_t_tebd.py b/deepmd/pt_expt/descriptor/se_t_tebd.py index cbcaf3822c..9e358d64b6 100644 --- a/deepmd/pt_expt/descriptor/se_t_tebd.py +++ b/deepmd/pt_expt/descriptor/se_t_tebd.py @@ -167,6 +167,7 @@ def call( mapping: torch.Tensor | None = None, fparam: torch.Tensor | None = None, comm_dict: dict | None = None, + charge_spin: torch.Tensor | None = None, ) -> Any: if not self.compress: return DescrptSeTTebdDP.call.__wrapped__( diff --git a/deepmd/pt_expt/infer/deep_eval.py b/deepmd/pt_expt/infer/deep_eval.py index f2fe908297..8f40600ffc 100644 --- a/deepmd/pt_expt/infer/deep_eval.py +++ b/deepmd/pt_expt/infer/deep_eval.py @@ -59,6 +59,17 @@ import ase.neighborlist +def _reshape_charge_spin(charge_spin: np.ndarray, nframes: int) -> np.ndarray: + charge_spin_arr = np.asarray(charge_spin) + try: + return charge_spin_arr.reshape(nframes, 2) + except ValueError as err: + raise ValueError( + f"charge_spin must be reshape-compatible with ({nframes}, 2), " + f"got shape {charge_spin_arr.shape}." + ) from err + + class DeepEval(DeepEvalBackend): """PyTorch Exportable backend implementation of DeepEval. @@ -413,6 +424,21 @@ def _load_pt(self, model_file: str, head: str | None = None) -> None: "mixed_types": model.mixed_types(), "has_default_fparam": model.has_default_fparam(), "default_fparam": model.get_default_fparam(), + "has_chg_spin_ebd": ( + model.has_chg_spin_ebd() + if hasattr(model, "has_chg_spin_ebd") + else False + ), + "has_default_chg_spin": ( + model.has_default_chg_spin() + if hasattr(model, "has_default_chg_spin") + else False + ), + "default_chg_spin": ( + model.get_default_chg_spin() + if hasattr(model, "get_default_chg_spin") + else None + ), "is_spin": self._is_spin, } if self._is_spin: @@ -435,6 +461,7 @@ def _eager_runner_spin( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: ext_coord = ext_coord.detach().requires_grad_(True) return model.forward_common_lower( @@ -445,6 +472,7 @@ def _eager_runner_spin( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=True, ) @@ -458,6 +486,7 @@ def _eager_runner( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: ext_coord = ext_coord.detach().requires_grad_(True) return model.forward_common_lower( @@ -467,6 +496,7 @@ def _eager_runner( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=True, ) @@ -496,6 +526,18 @@ def get_dim_aparam(self) -> int: return self._dpmodel.get_dim_aparam() return int(self.metadata["dim_aparam"]) + def has_chg_spin_ebd(self) -> bool: + """Check whether the model uses a dedicated charge_spin input.""" + if self._dpmodel is not None and hasattr(self._dpmodel, "has_chg_spin_ebd"): + return bool(self._dpmodel.has_chg_spin_ebd()) + return bool(self.metadata.get("has_chg_spin_ebd", False)) + + def has_default_chg_spin(self) -> bool: + """Check whether the model has a default charge_spin fallback.""" + if self._dpmodel is not None and hasattr(self._dpmodel, "has_default_chg_spin"): + return bool(self._dpmodel.has_default_chg_spin()) + return bool(self.metadata.get("has_default_chg_spin", False)) + @property def model_type(self) -> type["DeepEvalWrapper"]: """The evaluator of the model type.""" @@ -568,6 +610,7 @@ def eval( atomic: bool = False, fparam: np.ndarray | None = None, aparam: np.ndarray | None = None, + charge_spin: np.ndarray | None = None, **kwargs: Any, ) -> dict[str, np.ndarray]: """Evaluate the energy, force and virial by using this DP. @@ -592,6 +635,13 @@ def eval( aparam The atomic parameter. The array should be of size nframes x natoms x dim_aparam. + charge_spin + The charge and spin values for each frame. + The array should be reshape-compatible with nframes x 2, where the first + column is charge and the second column is spin. If the model has + add_chg_spin_ebd=True and no default_chg_spin is set, this parameter is + required. If default_chg_spin is configured, this parameter is optional + and will override the default. **kwargs Other parameters @@ -623,11 +673,24 @@ def eval( if spins is not None: spins = np.array(spins) out = self._eval_func(self._eval_model_spin, numb_test, natoms)( - coords, cells, atom_types, spins, fparam, aparam, request_defs + coords, + cells, + atom_types, + spins, + fparam, + aparam, + request_defs, + charge_spin, ) else: out = self._eval_func(self._eval_model, numb_test, natoms)( - coords, cells, atom_types, fparam, aparam, request_defs + coords, + cells, + atom_types, + fparam, + aparam, + request_defs, + charge_spin, ) return dict( zip( @@ -917,6 +980,7 @@ def _prepare_inputs( atom_types: np.ndarray, fparam: np.ndarray | None, aparam: np.ndarray | None, + charge_spin: np.ndarray | None = None, ) -> tuple: """Prepare tensor inputs for model evaluation. @@ -924,7 +988,7 @@ def _prepare_inputs( ------- tuple (ext_coord_t, ext_atype_t, nlist_t, mapping_t, - fparam_t, aparam_t, nframes, natoms) + fparam_t, aparam_t, charge_spin_t, nframes, natoms) """ nframes = coords.shape[0] if len(atom_types.shape) == 1: @@ -1008,6 +1072,33 @@ def _prepare_inputs( else: aparam_t = None + # charge_spin handling: dedicated input, separate from fparam. + if charge_spin is not None: + charge_spin_arr = _reshape_charge_spin(charge_spin, nframes) + charge_spin_t = torch.tensor( + charge_spin_arr, + dtype=torch.float64, + device=DEVICE, + ) + elif self.metadata.get("has_chg_spin_ebd", False): + default_cs = self.metadata.get("default_chg_spin") + if default_cs is not None: + if hasattr(default_cs, "cpu"): + default_cs = default_cs.cpu().numpy() + charge_spin_t = ( + torch.tensor(default_cs, dtype=torch.float64, device=DEVICE) + .unsqueeze(0) + .expand(nframes, -1) + .contiguous() + ) + else: + raise ValueError( + "charge_spin is required for this model (add_chg_spin_ebd=True) " + "but was not provided, and no default_chg_spin is set." + ) + else: + charge_spin_t = None + return ( ext_coord_t, ext_atype_t, @@ -1015,6 +1106,7 @@ def _prepare_inputs( mapping_t, fparam_t, aparam_t, + charge_spin_t, nframes, natoms, ) @@ -1027,6 +1119,7 @@ def _eval_model( fparam: np.ndarray | None, aparam: np.ndarray | None, request_defs: list[OutputVariableDef], + charge_spin: np.ndarray | None = None, ) -> tuple[np.ndarray, ...]: ( ext_coord_t, @@ -1035,9 +1128,10 @@ def _eval_model( mapping_t, fparam_t, aparam_t, + charge_spin_t, nframes, natoms, - ) = self._prepare_inputs(coords, cells, atom_types, fparam, aparam) + ) = self._prepare_inputs(coords, cells, atom_types, fparam, aparam, charge_spin) # Call the model (forward_common_lower interface, internal keys) if self._is_pt2: @@ -1046,11 +1140,23 @@ def _eval_model( # It also filters non-tensor args automatically, matching the # export-time signature where None args were excluded. model_ret = self._pt2_runner( - ext_coord_t, ext_atype_t, nlist_t, mapping_t, fparam_t, aparam_t + ext_coord_t, + ext_atype_t, + nlist_t, + mapping_t, + fparam_t, + aparam_t, + charge_spin_t, ) else: model_ret = self.exported_module( - ext_coord_t, ext_atype_t, nlist_t, mapping_t, fparam_t, aparam_t + ext_coord_t, + ext_atype_t, + nlist_t, + mapping_t, + fparam_t, + aparam_t, + charge_spin_t, ) # Apply communicate_extended_output to map extended atoms → local atoms @@ -1093,6 +1199,7 @@ def _eval_model_spin( fparam: np.ndarray | None, aparam: np.ndarray | None, request_defs: list[OutputVariableDef], + charge_spin: np.ndarray | None = None, ) -> tuple[np.ndarray, ...]: nframes = coords.shape[0] if len(atom_types.shape) == 1: @@ -1182,6 +1289,33 @@ def _eval_model_spin( else: aparam_t = None + # charge_spin handling: dedicated input, separate from fparam. + if charge_spin is not None: + charge_spin_arr = _reshape_charge_spin(charge_spin, nframes) + charge_spin_t = torch.tensor( + charge_spin_arr, + dtype=torch.float64, + device=DEVICE, + ) + elif self.metadata.get("has_chg_spin_ebd", False): + default_cs = self.metadata.get("default_chg_spin") + if default_cs is not None: + if hasattr(default_cs, "cpu"): + default_cs = default_cs.cpu().numpy() + charge_spin_t = ( + torch.tensor(default_cs, dtype=torch.float64, device=DEVICE) + .unsqueeze(0) + .expand(nframes, -1) + .contiguous() + ) + else: + raise ValueError( + "charge_spin is required for this model (add_chg_spin_ebd=True) " + "but was not provided, and no default_chg_spin is set." + ) + else: + charge_spin_t = None + # Call the model with spin (7 args) if self._is_pt2: model_ret = self._pt2_runner( @@ -1192,6 +1326,7 @@ def _eval_model_spin( mapping_t, fparam_t, aparam_t, + charge_spin_t, ) else: model_ret = self.exported_module( @@ -1202,6 +1337,7 @@ def _eval_model_spin( mapping_t, fparam_t, aparam_t, + charge_spin_t, ) # Apply communicate_extended_output to map extended atoms → local atoms @@ -1357,6 +1493,7 @@ def eval_descriptor( atom_types: np.ndarray, fparam: np.ndarray | None = None, aparam: np.ndarray | None = None, + charge_spin: np.ndarray | None = None, **kwargs: Any, ) -> np.ndarray: """Evaluate descriptor. @@ -1402,19 +1539,19 @@ def eval_descriptor( mapping_t, fparam_t, _aparam_t, + charge_spin_t, _nframes, _natoms, - ) = self._prepare_inputs(coords, cells, atom_types, fparam, aparam) + ) = self._prepare_inputs(coords, cells, atom_types, fparam, aparam, charge_spin) with torch.no_grad(): - fparam_for_des = ( - fparam_t if getattr(dp_am, "add_chg_spin_ebd", False) else None - ) descriptor, *_ = dp_am.descriptor( ext_coord_t, ext_atype_t, nlist_t, mapping=mapping_t, - fparam=fparam_for_des, + charge_spin=charge_spin_t + if getattr(dp_am, "add_chg_spin_ebd", False) + else None, ) return descriptor.detach().cpu().numpy() @@ -1425,6 +1562,7 @@ def eval_fitting_last_layer( atom_types: np.ndarray, fparam: np.ndarray | None = None, aparam: np.ndarray | None = None, + charge_spin: np.ndarray | None = None, **kwargs: Any, ) -> np.ndarray: """Evaluate the last hidden layer of the fitting network. @@ -1470,19 +1608,19 @@ def eval_fitting_last_layer( mapping_t, fparam_t, aparam_t, + charge_spin_t, _nframes, natoms, - ) = self._prepare_inputs(coords, cells, atom_types, fparam, aparam) + ) = self._prepare_inputs(coords, cells, atom_types, fparam, aparam, charge_spin) with torch.no_grad(): - fparam_for_des = ( - fparam_t if getattr(dp_am, "add_chg_spin_ebd", False) else None - ) descriptor, rot_mat, g2, h2, _sw = dp_am.descriptor( ext_coord_t, ext_atype_t, nlist_t, mapping=mapping_t, - fparam=fparam_for_des, + charge_spin=charge_spin_t + if getattr(dp_am, "add_chg_spin_ebd", False) + else None, ) atype = ext_atype_t[:, :natoms] fitting_net = dp_am.fitting_net diff --git a/deepmd/pt_expt/model/dipole_model.py b/deepmd/pt_expt/model/dipole_model.py index 4b0e570ecb..4d664adb89 100644 --- a/deepmd/pt_expt/model/dipole_model.py +++ b/deepmd/pt_expt/model/dipole_model.py @@ -45,6 +45,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common( coord, @@ -52,6 +53,7 @@ def forward( box, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -76,6 +78,7 @@ def forward_lower( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common_lower( extended_coord, @@ -84,6 +87,7 @@ def forward_lower( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -126,6 +130,7 @@ def forward_lower_exportable( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, **make_fx_kwargs: Any, ) -> torch.nn.Module: model = self @@ -137,6 +142,7 @@ def fn( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, ) -> dict[str, torch.Tensor]: extended_coord = extended_coord.detach().requires_grad_(True) nlist = _pad_nlist_for_export(nlist) @@ -147,6 +153,7 @@ def fn( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) @@ -155,7 +162,13 @@ def fn( model.need_sorted_nlist_for_lower = types.MethodType(lambda self: True, model) try: traced = make_fx(fn, **make_fx_kwargs)( - extended_coord, extended_atype, nlist, mapping, fparam, aparam + extended_coord, + extended_atype, + nlist, + mapping, + fparam, + aparam, + charge_spin, ) finally: model.need_sorted_nlist_for_lower = _orig_need_sort diff --git a/deepmd/pt_expt/model/dos_model.py b/deepmd/pt_expt/model/dos_model.py index 219c22e753..125522b889 100644 --- a/deepmd/pt_expt/model/dos_model.py +++ b/deepmd/pt_expt/model/dos_model.py @@ -45,6 +45,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common( coord, @@ -52,6 +53,7 @@ def forward( box, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -70,6 +72,7 @@ def forward_lower( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common_lower( extended_coord, @@ -78,6 +81,7 @@ def forward_lower( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -106,6 +110,7 @@ def forward_lower_exportable( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, **make_fx_kwargs: Any, ) -> torch.nn.Module: model = self @@ -117,6 +122,7 @@ def fn( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, ) -> dict[str, torch.Tensor]: extended_coord = extended_coord.detach().requires_grad_(True) nlist = _pad_nlist_for_export(nlist) @@ -127,6 +133,7 @@ def fn( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) @@ -135,7 +142,13 @@ def fn( model.need_sorted_nlist_for_lower = types.MethodType(lambda self: True, model) try: traced = make_fx(fn, **make_fx_kwargs)( - extended_coord, extended_atype, nlist, mapping, fparam, aparam + extended_coord, + extended_atype, + nlist, + mapping, + fparam, + aparam, + charge_spin, ) finally: model.need_sorted_nlist_for_lower = _orig_need_sort diff --git a/deepmd/pt_expt/model/dp_linear_model.py b/deepmd/pt_expt/model/dp_linear_model.py index 0ac75659b0..4a29251932 100644 --- a/deepmd/pt_expt/model/dp_linear_model.py +++ b/deepmd/pt_expt/model/dp_linear_model.py @@ -48,6 +48,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common( coord, @@ -55,6 +56,7 @@ def forward( box, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -79,6 +81,7 @@ def forward_lower( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common_lower( extended_coord, @@ -87,6 +90,7 @@ def forward_lower( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -131,6 +135,7 @@ def forward_lower_exportable( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, **make_fx_kwargs: Any, ) -> torch.nn.Module: model = self @@ -142,6 +147,7 @@ def fn( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, ) -> dict[str, torch.Tensor]: extended_coord = extended_coord.detach().requires_grad_(True) nlist = _pad_nlist_for_export(nlist) @@ -152,6 +158,7 @@ def fn( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) @@ -160,7 +167,13 @@ def fn( model.need_sorted_nlist_for_lower = types.MethodType(lambda self: True, model) try: traced = make_fx(fn, **make_fx_kwargs)( - extended_coord, extended_atype, nlist, mapping, fparam, aparam + extended_coord, + extended_atype, + nlist, + mapping, + fparam, + aparam, + charge_spin, ) finally: model.need_sorted_nlist_for_lower = _orig_need_sort diff --git a/deepmd/pt_expt/model/dp_zbl_model.py b/deepmd/pt_expt/model/dp_zbl_model.py index baa30c4ce0..9ca9fe6fff 100644 --- a/deepmd/pt_expt/model/dp_zbl_model.py +++ b/deepmd/pt_expt/model/dp_zbl_model.py @@ -45,6 +45,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common( coord, @@ -52,6 +53,7 @@ def forward( box, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -76,6 +78,7 @@ def forward_lower( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common_lower( extended_coord, @@ -84,6 +87,7 @@ def forward_lower( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -128,6 +132,7 @@ def forward_lower_exportable( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, **make_fx_kwargs: Any, ) -> torch.nn.Module: model = self @@ -139,6 +144,7 @@ def fn( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, ) -> dict[str, torch.Tensor]: extended_coord = extended_coord.detach().requires_grad_(True) nlist = _pad_nlist_for_export(nlist) @@ -149,6 +155,7 @@ def fn( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) @@ -159,7 +166,13 @@ def fn( model.need_sorted_nlist_for_lower = types.MethodType(lambda self: True, model) try: traced = make_fx(fn, **make_fx_kwargs)( - extended_coord, extended_atype, nlist, mapping, fparam, aparam + extended_coord, + extended_atype, + nlist, + mapping, + fparam, + aparam, + charge_spin, ) finally: model.need_sorted_nlist_for_lower = _orig_need_sort diff --git a/deepmd/pt_expt/model/ener_model.py b/deepmd/pt_expt/model/ener_model.py index beb91c4ec4..1fdef5eaad 100644 --- a/deepmd/pt_expt/model/ener_model.py +++ b/deepmd/pt_expt/model/ener_model.py @@ -58,6 +58,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common( coord, @@ -65,6 +66,7 @@ def forward( box, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -91,6 +93,7 @@ def forward_lower( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common_lower( extended_coord, @@ -99,6 +102,7 @@ def forward_lower( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -145,6 +149,7 @@ def forward_lower_exportable( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, **make_fx_kwargs: Any, ) -> torch.nn.Module: """Trace ``forward_lower`` into an exportable module. @@ -175,6 +180,7 @@ def forward_lower_exportable( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, **make_fx_kwargs, ) @@ -191,9 +197,16 @@ def fn( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, ) -> dict[str, torch.Tensor]: model_ret = traced( - extended_coord, extended_atype, nlist, mapping, fparam, aparam + extended_coord, + extended_atype, + nlist, + mapping, + fparam, + aparam, + charge_spin, ) model_predict: dict[str, torch.Tensor] = {} model_predict["atom_energy"] = model_ret["energy"] @@ -211,5 +224,5 @@ def fn( return model_predict return make_fx(fn, **make_fx_kwargs)( - extended_coord, extended_atype, nlist, mapping, fparam, aparam + extended_coord, extended_atype, nlist, mapping, fparam, aparam, charge_spin ) diff --git a/deepmd/pt_expt/model/make_model.py b/deepmd/pt_expt/model/make_model.py index 0ef1f8c0b7..878ed21a38 100644 --- a/deepmd/pt_expt/model/make_model.py +++ b/deepmd/pt_expt/model/make_model.py @@ -62,6 +62,7 @@ def _cal_hessian_ext( fparam: torch.Tensor | None, aparam: torch.Tensor | None, create_graph: bool = False, + charge_spin: torch.Tensor | None = None, ) -> torch.Tensor: """Compute hessian of reduced output w.r.t. extended coordinates. @@ -112,6 +113,7 @@ def _cal_hessian_ext( mapping[ii] if mapping is not None else None, fparam[ii] if fparam is not None else None, aparam[ii] if aparam is not None else None, + charge_spin[ii] if charge_spin is not None else None, ) hess = torch.autograd.functional.hessian( wrapper, @@ -142,6 +144,7 @@ def __init__( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None = None, ) -> None: self.model = model self.kk = kk @@ -152,6 +155,7 @@ def __init__( self.mapping = mapping self.fparam = fparam self.aparam = aparam + self.charge_spin = charge_spin def __call__(self, coord_flat: torch.Tensor) -> torch.Tensor: """Compute scalar reduced energy for one frame, one component. @@ -174,6 +178,9 @@ def __call__(self, coord_flat: torch.Tensor) -> torch.Tensor: mapping=self.mapping.unsqueeze(0) if self.mapping is not None else None, fparam=self.fparam.unsqueeze(0) if self.fparam is not None else None, aparam=self.aparam.unsqueeze(0) if self.aparam is not None else None, + charge_spin=self.charge_spin.unsqueeze(0) + if self.charge_spin is not None + else None, ) # atomic_ret[kk]: [1, nloc, *def] atom_energy = atomic_ret[self.kk][0] # [nloc, *def] @@ -281,6 +288,7 @@ def forward_common_atomic( do_atomic_virial: bool = False, extended_coord_corr: torch.Tensor | None = None, comm_dict: dict | None = None, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: atomic_ret = self.atomic_model.forward_common_atomic( extended_coord, @@ -290,6 +298,7 @@ def forward_common_atomic( fparam=fparam, aparam=aparam, comm_dict=comm_dict, + charge_spin=charge_spin, ) model_ret = fit_output_to_model_output( atomic_ret, @@ -318,6 +327,7 @@ def forward_common_atomic( mapping, fparam, aparam, + charge_spin=charge_spin, create_graph=self.training, ) return model_ret @@ -331,6 +341,7 @@ def forward_common_lower_exportable( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, **make_fx_kwargs: Any, ) -> torch.nn.Module: """Trace ``forward_common_lower`` into an exportable module. @@ -369,6 +380,7 @@ def fn( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, ) -> dict[str, torch.Tensor]: extended_coord = extended_coord.detach().requires_grad_(True) nlist = _pad_nlist_for_export(nlist) @@ -379,6 +391,7 @@ def fn( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) @@ -399,6 +412,7 @@ def fn( mapping, fparam, aparam, + charge_spin, ) finally: model.need_sorted_nlist_for_lower = _orig_need_sort @@ -412,6 +426,7 @@ def forward_common_lower_exportable_with_comm( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, send_list: torch.Tensor, send_proc: torch.Tensor, recv_proc: torch.Tensor, @@ -447,6 +462,7 @@ def fn( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, send_list: torch.Tensor, send_proc: torch.Tensor, recv_proc: torch.Tensor, @@ -480,6 +496,7 @@ def fn( aparam=aparam, do_atomic_virial=do_atomic_virial, comm_dict=comm_dict, + charge_spin=charge_spin, ) # Force the sort branch in ``_format_nlist`` (mirrors the regular @@ -496,6 +513,7 @@ def fn( mapping, fparam, aparam, + charge_spin, send_list, send_proc, recv_proc, diff --git a/deepmd/pt_expt/model/polar_model.py b/deepmd/pt_expt/model/polar_model.py index dd6b1c5d0f..60fb004bd4 100644 --- a/deepmd/pt_expt/model/polar_model.py +++ b/deepmd/pt_expt/model/polar_model.py @@ -45,6 +45,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common( coord, @@ -52,6 +53,7 @@ def forward( box, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -70,6 +72,7 @@ def forward_lower( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common_lower( extended_coord, @@ -78,6 +81,7 @@ def forward_lower( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -106,6 +110,7 @@ def forward_lower_exportable( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, **make_fx_kwargs: Any, ) -> torch.nn.Module: model = self @@ -117,6 +122,7 @@ def fn( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, ) -> dict[str, torch.Tensor]: extended_coord = extended_coord.detach().requires_grad_(True) nlist = _pad_nlist_for_export(nlist) @@ -127,6 +133,7 @@ def fn( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) @@ -135,7 +142,13 @@ def fn( model.need_sorted_nlist_for_lower = types.MethodType(lambda self: True, model) try: traced = make_fx(fn, **make_fx_kwargs)( - extended_coord, extended_atype, nlist, mapping, fparam, aparam + extended_coord, + extended_atype, + nlist, + mapping, + fparam, + aparam, + charge_spin, ) finally: model.need_sorted_nlist_for_lower = _orig_need_sort diff --git a/deepmd/pt_expt/model/property_model.py b/deepmd/pt_expt/model/property_model.py index 223f8e5d78..6d8470f142 100644 --- a/deepmd/pt_expt/model/property_model.py +++ b/deepmd/pt_expt/model/property_model.py @@ -49,6 +49,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common( coord, @@ -56,6 +57,7 @@ def forward( box, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) var_name = self.get_var_name() @@ -75,6 +77,7 @@ def forward_lower( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common_lower( extended_coord, @@ -83,6 +86,7 @@ def forward_lower( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) var_name = self.get_var_name() @@ -113,6 +117,7 @@ def forward_lower_exportable( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, **make_fx_kwargs: Any, ) -> torch.nn.Module: model = self @@ -124,6 +129,7 @@ def fn( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, ) -> dict[str, torch.Tensor]: extended_coord = extended_coord.detach().requires_grad_(True) nlist = _pad_nlist_for_export(nlist) @@ -134,6 +140,7 @@ def fn( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) @@ -142,7 +149,13 @@ def fn( model.need_sorted_nlist_for_lower = types.MethodType(lambda self: True, model) try: traced = make_fx(fn, **make_fx_kwargs)( - extended_coord, extended_atype, nlist, mapping, fparam, aparam + extended_coord, + extended_atype, + nlist, + mapping, + fparam, + aparam, + charge_spin, ) finally: model.need_sorted_nlist_for_lower = _orig_need_sort diff --git a/deepmd/pt_expt/model/spin_ener_model.py b/deepmd/pt_expt/model/spin_ener_model.py index e96d0fbaf1..54d0cbb411 100644 --- a/deepmd/pt_expt/model/spin_ener_model.py +++ b/deepmd/pt_expt/model/spin_ener_model.py @@ -49,6 +49,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common( coord, @@ -57,6 +58,7 @@ def forward( box, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -82,6 +84,7 @@ def forward_lower( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: model_ret = self.call_common_lower( extended_coord, @@ -91,6 +94,7 @@ def forward_lower( mapping=mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) model_predict = {} @@ -120,6 +124,7 @@ def forward_lower_exportable( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, **make_fx_kwargs: Any, ) -> torch.nn.Module: """Trace ``forward_lower`` into an exportable module. @@ -151,6 +156,7 @@ def forward_lower_exportable( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, **make_fx_kwargs, ) @@ -168,6 +174,7 @@ def fn( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, ) -> dict[str, torch.Tensor]: model_ret = traced( extended_coord, @@ -177,6 +184,7 @@ def fn( mapping, fparam, aparam, + charge_spin, ) model_predict: dict[str, torch.Tensor] = {} model_predict["atom_energy"] = model_ret["energy"] @@ -203,4 +211,5 @@ def fn( mapping, fparam, aparam, + charge_spin, ) diff --git a/deepmd/pt_expt/model/spin_model.py b/deepmd/pt_expt/model/spin_model.py index 707d46f70e..83cc8ecba4 100644 --- a/deepmd/pt_expt/model/spin_model.py +++ b/deepmd/pt_expt/model/spin_model.py @@ -58,6 +58,7 @@ def forward_common_lower_exportable( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, **make_fx_kwargs: Any, ) -> torch.nn.Module: """Trace ``call_common_lower`` into an exportable module. @@ -96,6 +97,7 @@ def fn( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, ) -> dict[str, torch.Tensor]: extended_coord = extended_coord.detach().requires_grad_(True) nlist = _pad_nlist_for_export(nlist) @@ -107,6 +109,7 @@ def fn( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=do_atomic_virial, ) @@ -130,6 +133,7 @@ def fn( mapping, fparam, aparam, + charge_spin, ) finally: backbone.need_sorted_nlist_for_lower = _orig_need_sort @@ -144,6 +148,7 @@ def forward_common_lower_exportable_with_comm( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, send_list: torch.Tensor, send_proc: torch.Tensor, recv_proc: torch.Tensor, @@ -172,6 +177,7 @@ def fn( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, send_list: torch.Tensor, send_proc: torch.Tensor, recv_proc: torch.Tensor, @@ -211,6 +217,7 @@ def fn( aparam=aparam, do_atomic_virial=do_atomic_virial, comm_dict=comm_dict, + charge_spin=charge_spin, ) # Force the sort branch in ``_format_nlist`` so the compiled @@ -230,6 +237,7 @@ def fn( mapping, fparam, aparam, + charge_spin, send_list, send_proc, recv_proc, diff --git a/deepmd/pt_expt/train/training.py b/deepmd/pt_expt/train/training.py index 5692b019cd..1059af0be6 100644 --- a/deepmd/pt_expt/train/training.py +++ b/deepmd/pt_expt/train/training.py @@ -138,6 +138,25 @@ def get_additional_data_requirement(_model: Any) -> list[DataRequirementItem]: "aparam", _model.get_dim_aparam(), atomic=True, must=True ) ) + if _model.has_chg_spin_ebd(): + has_default_cs = _model.has_default_chg_spin() + if has_default_cs: + default_cs = _model.get_default_chg_spin() + if hasattr(default_cs, "cpu"): + default_cs = default_cs.cpu().numpy() + else: + default_cs = np.asarray(default_cs) + else: + default_cs = 0.0 + additional_data_requirement.append( + DataRequirementItem( + "charge_spin", + ndof=2, + atomic=False, + must=not has_default_cs, + default=default_cs, + ) + ) return additional_data_requirement @@ -194,6 +213,7 @@ def _trace_and_compile( fparam: torch.Tensor | None, aparam: torch.Tensor | None, compile_opts: dict[str, Any] | None = None, + charge_spin: torch.Tensor | None = None, ) -> torch.nn.Module: """Symbolic-trace ``forward_lower`` and compile with inductor + dynamic=True. @@ -231,6 +251,7 @@ def fn( mapping: torch.Tensor | None, fparam: torch.Tensor | None, aparam: torch.Tensor | None, + charge_spin: torch.Tensor | None, ) -> dict[str, torch.Tensor]: extended_coord = extended_coord.detach().requires_grad_(True) return model.forward_lower( @@ -240,6 +261,7 @@ def fn( mapping, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, ) # Pick a trace-time nframes that's unlikely to collide with any other @@ -270,6 +292,7 @@ def _expand(t: torch.Tensor | None) -> torch.Tensor | None: mapping = _expand(mapping) fparam = _expand(fparam) aparam = _expand(aparam) + charge_spin = _expand(charge_spin) # Decompose silu_backward into primitive ops (sigmoid + mul + ...) # so that inductor can compile the graph without requiring a @@ -286,7 +309,7 @@ def _expand(t: torch.Tensor | None) -> torch.Tensor | None: tracing_mode="symbolic", _allow_non_fake_inputs=True, decomposition_table=decomp_table, - )(ext_coord, ext_atype, nlist, mapping, fparam, aparam) + )(ext_coord, ext_atype, nlist, mapping, fparam, aparam, charge_spin) # make_fx inserts aten.detach.default for saved tensors used in the # decomposed autograd.grad backward ops. These detach nodes break @@ -344,6 +367,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: from deepmd.dpmodel.utils.nlist import ( build_neighbor_list, @@ -381,7 +405,7 @@ def forward( ext_coord = ext_coord.detach().requires_grad_(True) result = self.compiled_forward_lower( - ext_coord, ext_atype, nlist, mapping, fparam, aparam + ext_coord, ext_atype, nlist, mapping, fparam, aparam, charge_spin ) # Translate forward_lower keys -> forward keys. @@ -981,6 +1005,7 @@ def _compile_model(self, compile_opts: dict[str, Any]) -> None: fparam = inp.get("fparam") aparam = inp.get("aparam") + charge_spin = inp.get("charge_spin") compiled_lower = _trace_and_compile( model, @@ -990,7 +1015,8 @@ def _compile_model(self, compile_opts: dict[str, Any]) -> None: mapping, fparam, aparam, - compile_opts, + charge_spin=charge_spin, + compile_opts=compile_opts, ) wrapper_mod.model[task_key] = _CompiledModel(model, compiled_lower) @@ -1036,6 +1062,16 @@ def get_data( batch = normalize_batch(data_sys.get_batch()) input_dict, label_dict = split_batch(batch) + # Drop optional inputs whose find_* flag is False so the model sees None. + for opt_key in ("fparam", "charge_spin"): + find_key = f"find_{opt_key}" + if ( + opt_key in input_dict + and find_key in label_dict + and not bool(label_dict[find_key]) + ): + input_dict.pop(opt_key) + # Convert numpy values to torch tensors. for dd in (input_dict, label_dict): for key, val in dd.items(): diff --git a/deepmd/pt_expt/train/wrapper.py b/deepmd/pt_expt/train/wrapper.py index f67efe8a8e..6fd68b8edc 100644 --- a/deepmd/pt_expt/train/wrapper.py +++ b/deepmd/pt_expt/train/wrapper.py @@ -174,6 +174,7 @@ def forward( label: dict[str, torch.Tensor] | None = None, task_key: str | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> tuple[dict[str, torch.Tensor], torch.Tensor | None, dict | None]: if not self.multi_task: task_key = "Default" @@ -189,6 +190,7 @@ def forward( "do_atomic_virial": do_atomic_virial, "fparam": fparam, "aparam": aparam, + "charge_spin": charge_spin, } model_pred = self.model[task_key](**input_dict) diff --git a/deepmd/pt_expt/utils/serialization.py b/deepmd/pt_expt/utils/serialization.py index d85a334493..48d073ed99 100644 --- a/deepmd/pt_expt/utils/serialization.py +++ b/deepmd/pt_expt/utils/serialization.py @@ -1,6 +1,9 @@ # SPDX-License-Identifier: LGPL-3.0-or-later import ctypes import json +from typing import ( + Any, +) import numpy as np import torch @@ -98,6 +101,16 @@ def _json_to_numpy(model_obj: dict) -> dict: ) +def _metadata_value_to_json(value: Any) -> Any: + if value is None: + return None + if isinstance(value, torch.Tensor): + return value.detach().cpu().tolist() + if isinstance(value, np.ndarray): + return value.tolist() + return value + + def _needs_with_comm_artifact(model: torch.nn.Module) -> bool: """Return ``True`` if the model needs a "with-comm" AOTI artifact compiled. @@ -201,8 +214,9 @@ def _make_sample_inputs( Returns ------- tuple - (ext_coord, ext_atype, nlist, mapping, fparam, aparam) or - (ext_coord, ext_atype, ext_spin, nlist, mapping, fparam, aparam) when has_spin. + (ext_coord, ext_atype, nlist, mapping, fparam, aparam, charge_spin) or + (ext_coord, ext_atype, ext_spin, nlist, mapping, fparam, aparam, + charge_spin) when has_spin. """ rcut = model.get_rcut() sel = model.get_sel() @@ -269,14 +283,31 @@ def _make_sample_inputs( else: aparam = None + dim_chg_spin = model.get_dim_chg_spin() if hasattr(model, "get_dim_chg_spin") else 0 + if dim_chg_spin > 0: + charge_spin = torch.zeros( + nframes, dim_chg_spin, dtype=torch.float64, device=_env.DEVICE + ) + else: + charge_spin = None + if has_spin: nall = extended_coord.shape[1] ext_spin = torch.zeros( nframes, nall, 3, dtype=torch.float64, device=_env.DEVICE ) - return ext_coord, ext_atype, ext_spin, nlist_t, mapping_t, fparam, aparam + return ( + ext_coord, + ext_atype, + ext_spin, + nlist_t, + mapping_t, + fparam, + aparam, + charge_spin, + ) - return ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam + return ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam, charge_spin def _build_dynamic_shapes( @@ -332,9 +363,10 @@ def _build_dynamic_shapes( nnei_dim = torch.export.Dim("nnei", min=max(1, model_nnei)) if has_spin: - # (ext_coord, ext_atype, ext_spin, nlist, mapping, fparam, aparam) + # (ext_coord, ext_atype, ext_spin, nlist, mapping, fparam, aparam, charge_spin) fparam = sample_inputs[5] aparam = sample_inputs[6] + charge_spin = sample_inputs[7] base = ( {0: nframes_dim, 1: nall_dim}, # extended_coord: (nframes, nall, 3) {0: nframes_dim, 1: nall_dim}, # extended_atype: (nframes, nall) @@ -347,11 +379,13 @@ def _build_dynamic_shapes( {0: nframes_dim, 1: nall_dim}, # mapping: (nframes, nall) {0: nframes_dim} if fparam is not None else None, # fparam {0: nframes_dim, 1: nloc_dim} if aparam is not None else None, # aparam + {0: nframes_dim} if charge_spin is not None else None, # charge_spin ) else: - # (ext_coord, ext_atype, nlist, mapping, fparam, aparam) + # (ext_coord, ext_atype, nlist, mapping, fparam, aparam, charge_spin) fparam = sample_inputs[4] aparam = sample_inputs[5] + charge_spin = sample_inputs[6] base = ( {0: nframes_dim, 1: nall_dim}, # extended_coord: (nframes, nall, 3) {0: nframes_dim, 1: nall_dim}, # extended_atype: (nframes, nall) @@ -363,6 +397,7 @@ def _build_dynamic_shapes( {0: nframes_dim, 1: nall_dim}, # mapping: (nframes, nall) {0: nframes_dim} if fparam is not None else None, # fparam {0: nframes_dim, 1: nloc_dim} if aparam is not None else None, # aparam + {0: nframes_dim} if charge_spin is not None else None, # charge_spin ) if not with_comm_dict: @@ -428,6 +463,19 @@ def _collect_metadata(model: torch.nn.Module, is_spin: bool = False) -> dict: "mixed_types": model.mixed_types(), "has_default_fparam": model.has_default_fparam(), "default_fparam": model.get_default_fparam(), + "has_chg_spin_ebd": ( + model.has_chg_spin_ebd() if hasattr(model, "has_chg_spin_ebd") else False + ), + "has_default_chg_spin": ( + model.has_default_chg_spin() + if hasattr(model, "has_default_chg_spin") + else False + ), + "default_chg_spin": ( + _metadata_value_to_json(model.get_default_chg_spin()) + if hasattr(model, "get_default_chg_spin") + else None + ), "fitting_output_defs": fitting_output_defs, # sel_type enables `DeepEval.get_sel_type()` without a dpmodel # round-trip; required for dipole/polar/wfc models in metadata-only @@ -659,11 +707,26 @@ def _trace_and_export( _env.DEVICE = _orig_device if is_spin: - ext_coord, ext_atype, ext_spin, nlist_t, mapping_t, fparam, aparam = ( - sample_inputs - ) + ( + ext_coord, + ext_atype, + ext_spin, + nlist_t, + mapping_t, + fparam, + aparam, + charge_spin, + ) = sample_inputs else: - ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam = sample_inputs + ( + ext_coord, + ext_atype, + nlist_t, + mapping_t, + fparam, + aparam, + charge_spin, + ) = sample_inputs # 3b. Build comm-tensor sample inputs when tracing the with-comm # variant (only valid for GNN models). The actual values don't @@ -700,6 +763,7 @@ def _trace_and_export( mapping_t, fparam, aparam, + charge_spin, *comm_inputs, do_atomic_virial=do_atomic_virial, tracing_mode="symbolic", @@ -715,6 +779,7 @@ def _trace_and_export( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, tracing_mode="symbolic", _allow_non_fake_inputs=True, ) @@ -729,6 +794,7 @@ def _trace_and_export( mapping_t, fparam, aparam, + charge_spin, *comm_inputs, do_atomic_virial=do_atomic_virial, tracing_mode="symbolic", @@ -743,6 +809,7 @@ def _trace_and_export( fparam=fparam, aparam=aparam, do_atomic_virial=do_atomic_virial, + charge_spin=charge_spin, tracing_mode="symbolic", _allow_non_fake_inputs=True, ) diff --git a/deepmd/utils/argcheck.py b/deepmd/utils/argcheck.py index 0364b24695..060bb90524 100644 --- a/deepmd/utils/argcheck.py +++ b/deepmd/utils/argcheck.py @@ -1375,8 +1375,15 @@ def descrpt_dpa3_args() -> list[Argument]: ) doc_add_chg_spin_ebd = ( "Whether to add charge and spin embedding to the descriptor. " - "When enabled, fparam is expected to have 2 values (charge, spin) " - "which are embedded and added to the type embedding." + "When enabled, the dedicated `charge_spin` input (shape [nframes, 2], " + "[charge, spin]) is embedded and added to the type embedding. " + "When `charge_spin` is missing in the input data, `default_chg_spin` " + "is used as a fallback if provided." + ) + doc_default_chg_spin = ( + "Default charge and spin values used as fallback when `charge_spin` " + "is not provided in the input data. Must be a list of length 2 " + "[charge, spin]. Only used when `add_chg_spin_ebd` is True." ) doc_activation_function = f"The activation function in the embedding net. Supported activation functions are {list_to_doc(ACTIVATION_FN_DICT.keys())}." doc_precision = f"The precision of the embedding net parameters, supported options are {list_to_doc(PRECISION_DICT.keys())} Default follows the interface precision." @@ -1410,6 +1417,13 @@ def descrpt_dpa3_args() -> list[Argument]: default=False, doc=doc_add_chg_spin_ebd, ), + Argument( + "default_chg_spin", + list[float], + optional=True, + default=None, + doc=doc_default_chg_spin, + ), Argument( "activation_function", str, @@ -4624,6 +4638,53 @@ def gen_json_schema(multi_task: bool = False) -> str: return json.dumps(generate_json_schema(arg)) +def _check_dpa3_chg_spin_migration(data: dict[str, Any]) -> None: + """Warn on likely legacy DPA3 configs that packed charge/spin into fparam. + + Before the charge_spin decoupling, enabling ``add_chg_spin_ebd`` on DPA3 + required ``numb_fparam=2`` on the fitting net so that charge/spin could be + carried via ``fparam``. After the decoupling, ``charge_spin`` is a + first-class input that is fully independent of ``fparam``, so users may + legitimately combine ``add_chg_spin_ebd`` with any ``numb_fparam`` for + genuine frame parameters. + + We cannot determine from the config alone whether a user's ``numb_fparam`` + is legacy (charge/spin in disguise) or genuine (real frame parameters). + But the combination ``add_chg_spin_ebd=True`` together with + ``numb_fparam=2`` is the strongest heuristic for the legacy pattern, since + that is exactly what the old code required. Emit a warning — not an error + — so users can audit their setup without breaking legitimate combinations. + """ + model = data.get("model", {}) if isinstance(data, dict) else {} + if not isinstance(model, dict): + return + submodels = ( + [model] if "descriptor" in model else list(model.get("model_dict", {}).values()) + ) + for m in submodels: + if not isinstance(m, dict): + continue + desc = m.get("descriptor", {}) + fitting = m.get("fitting_net", {}) + if not isinstance(desc, dict) or not isinstance(fitting, dict): + continue + if desc.get("type") != "dpa3": + continue + if not desc.get("add_chg_spin_ebd", False): + continue + if fitting.get("numb_fparam", 0) == 2: + warnings.warn( + "DPA3 `add_chg_spin_ebd=True` with `numb_fparam=2` matches the " + "pre-decoupling pattern where charge/spin was carried via " + "`fparam`. `charge_spin` is now an independent input, so " + "`numb_fparam=2` will be treated as two genuine frame " + "parameters. If you intended to feed charge/spin, remove the " + "charge/spin part of `fparam` and use the `charge_spin` input " + "or the descriptor's `default_chg_spin` instead.", + stacklevel=2, + ) + + def normalize( data: dict[str, Any], multi_task: bool = False, *, check: bool = True ) -> dict[str, Any]: @@ -4633,6 +4694,7 @@ def normalize( if check: base.check_value(data, strict=True) validate_full_validation_config(data, multi_task=multi_task) + _check_dpa3_chg_spin_migration(data) return data diff --git a/source/tests/consistent/descriptor/common.py b/source/tests/consistent/descriptor/common.py index 33bf7312de..078db4829f 100644 --- a/source/tests/consistent/descriptor/common.py +++ b/source/tests/consistent/descriptor/common.py @@ -103,6 +103,7 @@ def eval_dp_descriptor( box: np.ndarray, mixed_types: bool = False, fparam: np.ndarray | None = None, + charge_spin: np.ndarray | None = None, ) -> Any: ext_coords, ext_atype, mapping = extend_coord_with_ghosts( coords.reshape(1, -1, 3), @@ -118,9 +119,10 @@ def eval_dp_descriptor( dp_obj.get_sel(), distinguish_types=(not mixed_types), ) - return dp_obj( - ext_coords, ext_atype, nlist=nlist, mapping=mapping, fparam=fparam - ) + kwargs = {"nlist": nlist, "mapping": mapping, "fparam": fparam} + if hasattr(dp_obj, "get_dim_chg_spin") and dp_obj.get_dim_chg_spin() > 0: + kwargs["charge_spin"] = charge_spin + return dp_obj(ext_coords, ext_atype, **kwargs) def eval_pt_descriptor( self, @@ -131,6 +133,7 @@ def eval_pt_descriptor( box: np.ndarray, mixed_types: bool = False, fparam: np.ndarray | None = None, + charge_spin: np.ndarray | None = None, ) -> Any: ext_coords, ext_atype, mapping = extend_coord_with_ghosts_pt( torch.from_numpy(coords).to(PT_DEVICE).reshape(1, -1, 3), @@ -149,11 +152,17 @@ def eval_pt_descriptor( fparam_pt = ( torch.from_numpy(fparam).to(PT_DEVICE) if fparam is not None else None ) + charge_spin_pt = ( + torch.from_numpy(charge_spin).to(PT_DEVICE) + if charge_spin is not None + else None + ) + kwargs = {"nlist": nlist, "mapping": mapping, "fparam": fparam_pt} + if hasattr(pt_obj, "get_dim_chg_spin") and pt_obj.get_dim_chg_spin() > 0: + kwargs["charge_spin"] = charge_spin_pt return [ x.detach().cpu().numpy() if torch.is_tensor(x) else x - for x in pt_obj( - ext_coords, ext_atype, nlist=nlist, mapping=mapping, fparam=fparam_pt - ) + for x in pt_obj(ext_coords, ext_atype, **kwargs) ] def eval_pt_expt_descriptor( @@ -165,6 +174,7 @@ def eval_pt_expt_descriptor( box: np.ndarray, mixed_types: bool = False, fparam: np.ndarray | None = None, + charge_spin: np.ndarray | None = None, ) -> Any: ext_coords, ext_atype, mapping = extend_coord_with_ghosts( torch.from_numpy(coords).to(PT_DEVICE).reshape(1, -1, 3), @@ -183,11 +193,20 @@ def eval_pt_expt_descriptor( fparam_pt = ( torch.from_numpy(fparam).to(PT_DEVICE) if fparam is not None else None ) + charge_spin_pt = ( + torch.from_numpy(charge_spin).to(PT_DEVICE) + if charge_spin is not None + else None + ) + kwargs = {"nlist": nlist, "mapping": mapping, "fparam": fparam_pt} + if ( + hasattr(pt_expt_obj, "get_dim_chg_spin") + and pt_expt_obj.get_dim_chg_spin() > 0 + ): + kwargs["charge_spin"] = charge_spin_pt return [ x.detach().cpu().numpy() if torch.is_tensor(x) else x - for x in pt_expt_obj( - ext_coords, ext_atype, nlist=nlist, mapping=mapping, fparam=fparam_pt - ) + for x in pt_expt_obj(ext_coords, ext_atype, **kwargs) ] def eval_jax_descriptor( @@ -199,6 +218,7 @@ def eval_jax_descriptor( box: np.ndarray, mixed_types: bool = False, fparam: np.ndarray | None = None, + charge_spin: np.ndarray | None = None, ) -> Any: ext_coords, ext_atype, mapping = extend_coord_with_ghosts( jnp.array(coords).reshape(1, -1, 3), @@ -215,11 +235,13 @@ def eval_jax_descriptor( distinguish_types=(not mixed_types), ) fparam_jax = jnp.array(fparam) if fparam is not None else None + charge_spin_jax = jnp.array(charge_spin) if charge_spin is not None else None + kwargs = {"nlist": nlist, "mapping": mapping, "fparam": fparam_jax} + if hasattr(jax_obj, "get_dim_chg_spin") and jax_obj.get_dim_chg_spin() > 0: + kwargs["charge_spin"] = charge_spin_jax return [ np.asarray(x) if isinstance(x, jnp.ndarray) else x - for x in jax_obj( - ext_coords, ext_atype, nlist=nlist, mapping=mapping, fparam=fparam_jax - ) + for x in jax_obj(ext_coords, ext_atype, **kwargs) ] def eval_pd_descriptor( @@ -231,6 +253,7 @@ def eval_pd_descriptor( box: np.ndarray, mixed_types: bool = False, fparam: np.ndarray | None = None, + charge_spin: np.ndarray | None = None, ) -> Any: ext_coords, ext_atype, mapping = extend_coord_with_ghosts_pd( paddle.to_tensor(coords).to(PD_DEVICE).reshape([1, -1, 3]), @@ -265,6 +288,7 @@ def eval_array_api_strict_descriptor( box: np.ndarray, mixed_types: bool = False, fparam: np.ndarray | None = None, + charge_spin: np.ndarray | None = None, ) -> Any: ext_coords, ext_atype, mapping = extend_coord_with_ghosts( array_api_strict.asarray(coords.reshape(1, -1, 3)), @@ -283,15 +307,18 @@ def eval_array_api_strict_descriptor( fparam_array_api = ( array_api_strict.asarray(fparam) if fparam is not None else None ) + charge_spin_array_api = ( + array_api_strict.asarray(charge_spin) if charge_spin is not None else None + ) + kwargs = {"nlist": nlist, "mapping": mapping, "fparam": fparam_array_api} + if ( + hasattr(array_api_strict_obj, "get_dim_chg_spin") + and array_api_strict_obj.get_dim_chg_spin() > 0 + ): + kwargs["charge_spin"] = charge_spin_array_api return [ to_numpy_array(x) if hasattr(x, "__array_namespace__") else x - for x in array_api_strict_obj( - ext_coords, - ext_atype, - nlist=nlist, - mapping=mapping, - fparam=fparam_array_api, - ) + for x in array_api_strict_obj(ext_coords, ext_atype, **kwargs) ] diff --git a/source/tests/consistent/descriptor/test_dpa3.py b/source/tests/consistent/descriptor/test_dpa3.py index 2aa0fd931b..3f30d59435 100644 --- a/source/tests/consistent/descriptor/test_dpa3.py +++ b/source/tests/consistent/descriptor/test_dpa3.py @@ -81,6 +81,7 @@ "n_multi_edge_message", "precision", "add_chg_spin_ebd", + "default_chg_spin", "sequential_update", ) @@ -101,6 +102,7 @@ "n_multi_edge_message": 1, "precision": "float64", "add_chg_spin_ebd": False, + "default_chg_spin": None, "sequential_update": False, } @@ -125,6 +127,7 @@ def dpa3_case(**overrides: Any) -> tuple: dpa3_case(exclude_types=[[0, 1]]), dpa3_case(use_loc_mapping=False), dpa3_case(add_chg_spin_ebd=True), + dpa3_case(add_chg_spin_ebd=True, default_chg_spin=[5.0, 1.0]), # Repflow compression branches. dpa3_case(a_compress_rate=1), dpa3_case(a_compress_e_rate=2), @@ -165,6 +168,7 @@ def dpa3_descriptor_api_case(**overrides: Any) -> tuple: dpa3_descriptor_api_case(use_loc_mapping=False), dpa3_descriptor_api_case(fix_stat_std=0.0), dpa3_descriptor_api_case(add_chg_spin_ebd=True), + dpa3_descriptor_api_case(add_chg_spin_ebd=True, default_chg_spin=[5.0, 1.0]), # Repflow compression branches. dpa3_descriptor_api_case(a_compress_rate=1), dpa3_descriptor_api_case(a_compress_e_rate=2), @@ -211,6 +215,7 @@ def data(self) -> dict: n_multi_edge_message, precision, add_chg_spin_ebd, + default_chg_spin, sequential_update, ) = self.param return { @@ -254,6 +259,7 @@ def data(self) -> dict: "use_loc_mapping": use_loc_mapping, "trainable": False, "add_chg_spin_ebd": add_chg_spin_ebd, + "default_chg_spin": default_chg_spin, } @property @@ -274,6 +280,7 @@ def skip_pt(self) -> bool: _n_multi_edge_message, _precision, _add_chg_spin_ebd, + _default_chg_spin, _sequential_update, ) = self.param return CommonTest.skip_pt @@ -296,6 +303,7 @@ def skip_pd(self) -> bool: _n_multi_edge_message, _precision, add_chg_spin_ebd, + _default_chg_spin, _sequential_update, ) = self.param return True if add_chg_spin_ebd else CommonTest.skip_pd @@ -318,6 +326,7 @@ def skip_dp(self) -> bool: _n_multi_edge_message, _precision, _add_chg_spin_ebd, + _default_chg_spin, _sequential_update, ) = self.param return CommonTest.skip_dp @@ -340,6 +349,7 @@ def skip_tf(self) -> bool: _n_multi_edge_message, _precision, _add_chg_spin_ebd, + _default_chg_spin, _sequential_update, ) = self.param return True @@ -406,10 +416,10 @@ def setUp(self) -> None: _n_multi_edge_message, _precision, add_chg_spin_ebd, + _default_chg_spin, _sequential_update, ) = self.param - # fparam for charge=5, spin=1 when add_chg_spin_ebd is True - self.fparam = ( + self.charge_spin = ( np.array([[5, 1]], dtype=GLOBAL_NP_FLOAT_PRECISION) if add_chg_spin_ebd else None @@ -433,7 +443,7 @@ def eval_dp(self, dp_obj: Any) -> Any: self.atype, self.box, mixed_types=True, - fparam=self.fparam, + charge_spin=self.charge_spin, ) def eval_pt(self, pt_obj: Any) -> Any: @@ -444,7 +454,7 @@ def eval_pt(self, pt_obj: Any) -> Any: self.atype, self.box, mixed_types=True, - fparam=self.fparam, + charge_spin=self.charge_spin, ) def eval_pd(self, pd_obj: Any) -> Any: @@ -455,7 +465,7 @@ def eval_pd(self, pd_obj: Any) -> Any: self.atype, self.box, mixed_types=True, - fparam=self.fparam, + charge_spin=self.charge_spin, ) def eval_jax(self, jax_obj: Any) -> Any: @@ -466,7 +476,7 @@ def eval_jax(self, jax_obj: Any) -> Any: self.atype, self.box, mixed_types=True, - fparam=self.fparam, + charge_spin=self.charge_spin, ) def eval_pt_expt(self, pt_expt_obj: Any) -> Any: @@ -477,7 +487,7 @@ def eval_pt_expt(self, pt_expt_obj: Any) -> Any: self.atype, self.box, mixed_types=True, - fparam=self.fparam, + charge_spin=self.charge_spin, ) def eval_array_api_strict(self, array_api_strict_obj: Any) -> Any: @@ -488,7 +498,7 @@ def eval_array_api_strict(self, array_api_strict_obj: Any) -> Any: self.atype, self.box, mixed_types=True, - fparam=self.fparam, + charge_spin=self.charge_spin, ) def extract_ret(self, ret: Any, backend) -> tuple[np.ndarray, ...]: @@ -513,6 +523,7 @@ def rtol(self) -> float: _n_multi_edge_message, precision, _add_chg_spin_ebd, + _default_chg_spin, _sequential_update, ) = self.param if precision == "float64": @@ -541,6 +552,7 @@ def atol(self) -> float: _n_multi_edge_message, precision, _add_chg_spin_ebd, + _default_chg_spin, _sequential_update, ) = self.param if precision == "float64": @@ -578,6 +590,7 @@ def data(self) -> dict: n_multi_edge_message, precision, add_chg_spin_ebd, + default_chg_spin, sequential_update, ) = self.param return { @@ -621,4 +634,5 @@ def data(self) -> dict: "use_loc_mapping": use_loc_mapping, "trainable": False, "add_chg_spin_ebd": add_chg_spin_ebd, + "default_chg_spin": default_chg_spin, } diff --git a/source/tests/consistent/model/test_ener.py b/source/tests/consistent/model/test_ener.py index def9f67f32..d62f84bea8 100644 --- a/source/tests/consistent/model/test_ener.py +++ b/source/tests/consistent/model/test_ener.py @@ -2016,57 +2016,56 @@ def raise_error(): @parameterized( - ("no_fparam", "explicit_fparam", "default_fparam"), # fparam_mode + ("no_chg_spin", "explicit_chg_spin", "default_chg_spin"), # cs_mode ) @unittest.skipUnless(INSTALLED_PT and INSTALLED_PT_EXPT, "PT and PT_EXPT are required") class TestEnerChgSpinEbdFparam(unittest.TestCase): - """Test dp/pt/pt_expt model forward consistency for add_chg_spin_ebd with three fparam modes. + """Test dp/pt/pt_expt model forward consistency for add_chg_spin_ebd with three modes. - - no_fparam: numb_fparam=0, add_chg_spin_ebd=False (baseline) - - explicit_fparam: numb_fparam=2, add_chg_spin_ebd=True, fparam provided - - default_fparam: numb_fparam=2, default_fparam set, add_chg_spin_ebd=True, fparam=None + - no_chg_spin: add_chg_spin_ebd=False (baseline) + - explicit_chg_spin: add_chg_spin_ebd=True, charge_spin provided + - default_chg_spin: add_chg_spin_ebd=True, default_chg_spin=[5,1], charge_spin=None """ def setUp(self) -> None: - (self.fparam_mode,) = self.param + (self.cs_mode,) = self.param - add_chg_spin_ebd = self.fparam_mode != "no_fparam" + add_chg_spin_ebd = self.cs_mode != "no_chg_spin" fitting_cfg: dict[str, Any] = { "neuron": [10, 10], "precision": "float64", "seed": 1, } - if self.fparam_mode != "no_fparam": - fitting_cfg["numb_fparam"] = 2 - if self.fparam_mode == "default_fparam": - fitting_cfg["default_fparam"] = [5, 1] + descriptor_cfg: dict[str, Any] = { + "type": "dpa3", + "repflow": { + "n_dim": 20, + "e_dim": 10, + "a_dim": 8, + "nlayers": 3, + "e_rcut": 6.0, + "e_rcut_smth": 5.0, + "e_sel": 10, + "a_rcut": 4.0, + "a_rcut_smth": 3.5, + "a_sel": 8, + "axis_neuron": 4, + "update_angle": True, + "update_style": "res_residual", + "update_residual": 0.1, + "update_residual_init": "const", + }, + "precision": "float64", + "seed": 1, + "add_chg_spin_ebd": add_chg_spin_ebd, + } + if self.cs_mode == "default_chg_spin": + descriptor_cfg["default_chg_spin"] = [5.0, 1.0] data = model_args().normalize_value( { "type_map": ["O", "H"], - "descriptor": { - "type": "dpa3", - "repflow": { - "n_dim": 20, - "e_dim": 10, - "a_dim": 8, - "nlayers": 3, - "e_rcut": 6.0, - "e_rcut_smth": 5.0, - "e_sel": 10, - "a_rcut": 4.0, - "a_rcut_smth": 3.5, - "a_sel": 8, - "axis_neuron": 4, - "update_angle": True, - "update_style": "res_residual", - "update_residual": 0.1, - "update_residual_init": "const", - }, - "precision": "float64", - "seed": 1, - "add_chg_spin_ebd": add_chg_spin_ebd, - }, + "descriptor": descriptor_cfg, "fitting_net": fitting_cfg, }, trim_pattern="_*", @@ -2106,15 +2105,15 @@ def setUp(self) -> None: dtype=GLOBAL_NP_FLOAT_PRECISION, ).reshape(1, 9) - # fparam: charge=5, spin=1 - if self.fparam_mode == "explicit_fparam": - self.fparam_np = np.array([[5, 1]], dtype=GLOBAL_NP_FLOAT_PRECISION) + # charge_spin: charge=5, spin=1; only set in explicit mode. + if self.cs_mode == "explicit_chg_spin": + self.charge_spin_np = np.array([[5, 1]], dtype=GLOBAL_NP_FLOAT_PRECISION) else: - self.fparam_np = None + self.charge_spin_np = None def test_forward_consistency(self) -> None: dp_ret = self.dp_model( - self.coords, self.atype, box=self.box, fparam=self.fparam_np + self.coords, self.atype, box=self.box, charge_spin=self.charge_spin_np ) pt_ret = { kk: torch_to_numpy(vv) @@ -2122,7 +2121,7 @@ def test_forward_consistency(self) -> None: numpy_to_torch(self.coords), numpy_to_torch(self.atype), box=numpy_to_torch(self.box), - fparam=numpy_to_torch(self.fparam_np), + charge_spin=numpy_to_torch(self.charge_spin_np), do_atomic_virial=True, ).items() } @@ -2134,7 +2133,7 @@ def test_forward_consistency(self) -> None: coord_t, pt_expt_numpy_to_torch(self.atype), box=pt_expt_numpy_to_torch(self.box), - fparam=pt_expt_numpy_to_torch(self.fparam_np), + charge_spin=pt_expt_numpy_to_torch(self.charge_spin_np), do_atomic_virial=True, ).items() } @@ -2144,12 +2143,12 @@ def test_forward_consistency(self) -> None: pt_ret[key], rtol=1e-10, atol=1e-10, - err_msg=f"dp vs pt mismatch in {key} (mode={self.fparam_mode})", + err_msg=f"dp vs pt mismatch in {key} (mode={self.cs_mode})", ) np.testing.assert_allclose( dp_ret[key], pe_ret[key], rtol=1e-10, atol=1e-10, - err_msg=f"dp vs pt_expt mismatch in {key} (mode={self.fparam_mode})", + err_msg=f"dp vs pt_expt mismatch in {key} (mode={self.cs_mode})", ) diff --git a/source/tests/pd/model/test_dpa3.py b/source/tests/pd/model/test_dpa3.py index 2294b1810e..a582181085 100644 --- a/source/tests/pd/model/test_dpa3.py +++ b/source/tests/pd/model/test_dpa3.py @@ -66,7 +66,7 @@ def test_consistency( [1, 2], # n_multi_edge_message ["float64"], # precision [False], # use_econf_tebd - [False, True], # add_chg_spin_ebd + [False], # add_chg_spin_ebd (PD backend does not support charge_spin) ): dtype = PRECISION_DICT[prec] rtol, atol = get_tols(prec) diff --git a/source/tests/pt/model/test_dpa3.py b/source/tests/pt/model/test_dpa3.py index d66eab9dea..f99111b8d7 100644 --- a/source/tests/pt/model/test_dpa3.py +++ b/source/tests/pt/model/test_dpa3.py @@ -5,12 +5,12 @@ import numpy as np import torch -from deepmd.dpmodel.descriptor.dpa3 import DescrptDPA3 as DPDescrptDPA3 from deepmd.dpmodel.descriptor.dpa3 import ( RepFlowArgs, ) from deepmd.pt.model.descriptor import ( DescrptDPA3, + DescrptHybrid, ) from deepmd.pt.utils import ( env, @@ -32,6 +32,23 @@ dtype = env.GLOBAL_PT_FLOAT_PRECISION +def _repflow_args() -> RepFlowArgs: + return RepFlowArgs( + n_dim=8, + e_dim=6, + a_dim=4, + nlayers=1, + e_rcut=4.0, + e_rcut_smth=0.5, + e_sel=12, + a_rcut=3.5, + a_rcut_smth=0.5, + a_sel=8, + axis_neuron=4, + update_angle=False, + ) + + class TestDescrptDPA3(unittest.TestCase, TestCaseSingleFrameWithNlist): def setUp(self) -> None: TestCaseSingleFrameWithNlist.setUp(self) @@ -55,7 +72,7 @@ def test_consistency( nme, prec, ect, - add_chg_spin, + cs_mode, seq_upd, ) in itertools.product( [True, False], # update_angle @@ -67,7 +84,7 @@ def test_consistency( [1, 2], # n_multi_edge_message ["float64"], # precision [False], # use_econf_tebd - [False, True], # add_chg_spin_ebd + ["no_chg_spin", "explicit_chg_spin", "default_chg_spin"], [False, True], # sequential_update ): # sequential_update only works with update_angle=True @@ -78,6 +95,13 @@ def test_consistency( if prec == "float64": atol = 1e-8 # marginal GPU test cases... + add_chg_spin = cs_mode != "no_chg_spin" + default_chg_spin = [5.0, 1.0] if cs_mode == "default_chg_spin" else None + # Descriptor.forward does not apply default_chg_spin fallback + # (that lives in dp_atomic_model). When add_chg_spin_ebd is on, + # tests must always pass an explicit charge_spin tensor. + need_cs_input = add_chg_spin + repflow = RepFlowArgs( n_dim=20, e_dim=10, @@ -111,27 +135,26 @@ def test_consistency( use_econf_tebd=ect, type_map=["O", "H"] if ect else None, add_chg_spin_ebd=add_chg_spin, + default_chg_spin=default_chg_spin, seed=GLOBAL_SEED, ).to(env.DEVICE) dd0.repflows.mean = torch.tensor(davg, dtype=dtype, device=env.DEVICE) dd0.repflows.stddev = torch.tensor(dstd, dtype=dtype, device=env.DEVICE) - # Prepare fparam if needed - fparam = None - fparam_np = None - if add_chg_spin: - fparam = torch.tensor([[5, 1]], dtype=dtype, device=env.DEVICE).expand( - nf, -1 - ) - fparam_np = np.array([[5, 1]], dtype=np.float64).repeat(nf, axis=0) + # Prepare charge_spin per mode. + charge_spin = None + if need_cs_input: + charge_spin = torch.tensor( + [[5, 1]], dtype=dtype, device=env.DEVICE + ).expand(nf, -1) rd0, _, _, _, _ = dd0( torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), torch.tensor(self.atype_ext, dtype=int, device=env.DEVICE), torch.tensor(self.nlist, dtype=int, device=env.DEVICE), torch.tensor(self.mapping, dtype=int, device=env.DEVICE), - fparam=fparam, + charge_spin=charge_spin, ) # serialization dd1 = DescrptDPA3.deserialize(dd0.serialize()) @@ -140,7 +163,7 @@ def test_consistency( torch.tensor(self.atype_ext, dtype=int, device=env.DEVICE), torch.tensor(self.nlist, dtype=int, device=env.DEVICE), torch.tensor(self.mapping, dtype=int, device=env.DEVICE), - fparam=fparam, + charge_spin=charge_spin, ) np.testing.assert_allclose( rd0.detach().cpu().numpy(), @@ -148,21 +171,75 @@ def test_consistency( rtol=rtol, atol=atol, ) - # dp impl - dd2 = DPDescrptDPA3.deserialize(dd0.serialize()) - rd2, _, _, _, _ = dd2.call( - self.coord_ext, - self.atype_ext, - self.nlist, - self.mapping, - fparam=fparam_np, - ) - np.testing.assert_allclose( - rd0.detach().cpu().numpy(), - rd2, - rtol=rtol, - atol=atol, - ) + # Cross-backend (dpmodel vs pt) numeric consistency for + # add_chg_spin_ebd is covered by + # source/tests/consistent/descriptor/test_dpa3.py. + + # default_chg_spin should match explicit when value is the same. + if cs_mode == "default_chg_spin": + dd_explicit = DescrptDPA3( + self.nt, + repflow=repflow, + exclude_types=[], + precision=prec, + use_econf_tebd=ect, + type_map=["O", "H"] if ect else None, + add_chg_spin_ebd=True, + default_chg_spin=None, + seed=GLOBAL_SEED, + ).to(env.DEVICE) + dd_explicit.repflows.mean = torch.tensor( + davg, dtype=dtype, device=env.DEVICE + ) + dd_explicit.repflows.stddev = torch.tensor( + dstd, dtype=dtype, device=env.DEVICE + ) + cs = torch.tensor([[5, 1]], dtype=dtype, device=env.DEVICE).expand( + nf, -1 + ) + rd_explicit, _, _, _, _ = dd_explicit( + torch.tensor(self.coord_ext, dtype=dtype, device=env.DEVICE), + torch.tensor(self.atype_ext, dtype=int, device=env.DEVICE), + torch.tensor(self.nlist, dtype=int, device=env.DEVICE), + torch.tensor(self.mapping, dtype=int, device=env.DEVICE), + charge_spin=cs, + ) + np.testing.assert_allclose( + rd0.detach().cpu().numpy(), + rd_explicit.detach().cpu().numpy(), + rtol=rtol, + atol=atol, + ) + + def test_hybrid_default_chg_spin_semantics(self) -> None: + def make_dpa3(default_chg_spin: list[float] | None) -> DescrptDPA3: + return DescrptDPA3( + self.nt, + repflow=_repflow_args(), + precision="float64", + add_chg_spin_ebd=True, + default_chg_spin=default_chg_spin, + seed=GLOBAL_SEED, + ).to(env.DEVICE) + + shared_default = DescrptHybrid( + list=[make_dpa3([5.0, 1.0]), make_dpa3([5.0, 1.0])] + ) + self.assertTrue(shared_default.has_default_chg_spin()) + torch.testing.assert_close( + shared_default.get_default_chg_spin(), + torch.tensor([5.0, 1.0], dtype=torch.float64, device=env.DEVICE), + ) + + missing_default = DescrptHybrid(list=[make_dpa3([5.0, 1.0]), make_dpa3(None)]) + self.assertFalse(missing_default.has_default_chg_spin()) + self.assertIsNone(missing_default.get_default_chg_spin()) + + mismatched_default = DescrptHybrid( + list=[make_dpa3([5.0, 1.0]), make_dpa3([6.0, 1.0])] + ) + self.assertFalse(mismatched_default.has_default_chg_spin()) + self.assertIsNone(mismatched_default.get_default_chg_spin()) def test_jit( self, diff --git a/source/tests/pt/test_data_modifier.py b/source/tests/pt/test_data_modifier.py index 18d66ef2ff..7b8f8096ce 100644 --- a/source/tests/pt/test_data_modifier.py +++ b/source/tests/pt/test_data_modifier.py @@ -116,6 +116,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: """Implementation of abstractmethod.""" return {} @@ -158,6 +159,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: """Implementation of abstractmethod.""" return {} @@ -205,6 +207,7 @@ def forward( fparam: torch.Tensor | None = None, aparam: torch.Tensor | None = None, do_atomic_virial: bool = False, + charge_spin: torch.Tensor | None = None, ) -> dict[str, torch.Tensor]: """Take scaled model prediction as data modification.""" model_pred = self.model( diff --git a/source/tests/pt_expt/descriptor/test_dpa3.py b/source/tests/pt_expt/descriptor/test_dpa3.py index 3013f5cc65..87f31d62e2 100644 --- a/source/tests/pt_expt/descriptor/test_dpa3.py +++ b/source/tests/pt_expt/descriptor/test_dpa3.py @@ -128,6 +128,89 @@ def test_consistency(self, ua, ruri, acr, acer, acus, nme) -> None: atol=atol, ) + @pytest.mark.parametrize("cs_mode", ["explicit_chg_spin", "default_chg_spin"]) + def test_consistency_chg_spin(self, cs_mode) -> None: + rng = np.random.default_rng(GLOBAL_SEED) + nf, nloc, nnei = self.nlist.shape + davg = rng.normal(size=(self.nt, nnei, 4)) + dstd = 0.1 + np.abs(rng.normal(size=(self.nt, nnei, 4))) + + prec = "float64" + dtype = PRECISION_DICT[prec] + rtol, atol = get_tols(prec) + atol = 1e-8 + + default_chg_spin = [5.0, 1.0] if cs_mode == "default_chg_spin" else None + + repflow = RepFlowArgs( + n_dim=20, + e_dim=10, + a_dim=8, + nlayers=3, + e_rcut=self.rcut, + e_rcut_smth=self.rcut_smth, + e_sel=nnei, + a_rcut=self.rcut - 0.1, + a_rcut_smth=self.rcut_smth, + a_sel=nnei - 1, + axis_neuron=4, + update_angle=True, + update_style="res_residual", + update_residual_init="const", + smooth_edge_update=True, + ) + + dd0 = DescrptDPA3( + self.nt, + repflow=repflow, + exclude_types=[], + precision=prec, + add_chg_spin_ebd=True, + default_chg_spin=default_chg_spin, + seed=GLOBAL_SEED, + ).to(self.device) + dd0.repflows.mean = torch.tensor(davg, dtype=dtype, device=self.device) + dd0.repflows.stddev = torch.tensor(dstd, dtype=dtype, device=self.device) + + # descriptor.forward does not apply default_chg_spin fallback; + # always pass an explicit charge_spin tensor here. + charge_spin = torch.tensor([[5, 1]], dtype=dtype, device=self.device).expand( + nf, -1 + ) + charge_spin_np = np.array([[5, 1]], dtype=np.float64).repeat(nf, axis=0) + + coord_ext = torch.tensor(self.coord_ext, dtype=dtype, device=self.device) + atype_ext = torch.tensor(self.atype_ext, dtype=int, device=self.device) + nlist_t = torch.tensor(self.nlist, dtype=int, device=self.device) + mapping_t = torch.tensor(self.mapping, dtype=int, device=self.device) + + rd0, _, _, _, _ = dd0( + coord_ext, atype_ext, nlist_t, mapping_t, charge_spin=charge_spin + ) + # serialization round-trip preserves default_chg_spin + dd1 = DescrptDPA3.deserialize(dd0.serialize()) + rd1, _, _, _, _ = dd1( + coord_ext, atype_ext, nlist_t, mapping_t, charge_spin=charge_spin + ) + np.testing.assert_allclose( + rd0.detach().cpu().numpy(), + rd1.detach().cpu().numpy(), + rtol=rtol, + atol=atol, + ) + # vs dpmodel + dd2 = DPDescrptDPA3.deserialize(dd0.serialize()) + rd2, _, _, _, _ = dd2.call( + self.coord_ext, + self.atype_ext, + self.nlist, + self.mapping, + charge_spin=charge_spin_np, + ) + np.testing.assert_allclose( + rd0.detach().cpu().numpy(), rd2, rtol=rtol, atol=atol + ) + @pytest.mark.parametrize("prec", ["float64", "float32"]) # precision def test_exportable(self, prec) -> None: rng = np.random.default_rng(GLOBAL_SEED) diff --git a/source/tests/pt_expt/export_helpers.py b/source/tests/pt_expt/export_helpers.py index ae4db82ddb..3f97192a67 100644 --- a/source/tests/pt_expt/export_helpers.py +++ b/source/tests/pt_expt/export_helpers.py @@ -118,6 +118,7 @@ def model_forward_lower_export_round_trip( fparam, aparam, output_keys: tuple[str, ...], + charge_spin=None, rtol: float = 1e-10, atol: float = 1e-10, ): @@ -141,6 +142,9 @@ def model_forward_lower_export_round_trip( Frame and atom parameters. output_keys : tuple of str Output dictionary keys to verify. + charge_spin : torch.Tensor or None + Charge/spin parameter for descriptors that consume it (e.g. DPA3 + with ``add_chg_spin_ebd=True``). rtol, atol : float Tolerances for np.testing.assert_allclose. """ @@ -156,6 +160,7 @@ def model_forward_lower_export_round_trip( mapping_t, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, ) # 2. Concrete trace @@ -166,21 +171,24 @@ def model_forward_lower_export_round_trip( mapping_t, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, ) assert isinstance(traced, torch.nn.Module) # 3. Basic export (no dynamic shapes) exported = torch.export.export( traced, - (ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam), + (ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam, charge_spin), strict=False, ) assert exported is not None # 4. Compare traced and exported vs eager - ret_traced = traced(ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam) + ret_traced = traced( + ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam, charge_spin + ) ret_exported = exported.module()( - ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam + ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam, charge_spin ) for key in output_keys: np.testing.assert_allclose( @@ -201,7 +209,15 @@ def model_forward_lower_export_round_trip( # 5. Symbolic trace + dynamic shapes + .pte round-trip inputs_2f = tuple( torch.cat([t, t], dim=0) if t is not None else None - for t in (ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam) + for t in ( + ext_coord, + ext_atype, + nlist_t, + mapping_t, + fparam, + aparam, + charge_spin, + ) ) traced_sym = md_pt.forward_lower_exportable( inputs_2f[0], @@ -210,6 +226,7 @@ def model_forward_lower_export_round_trip( inputs_2f[3], fparam=inputs_2f[4], aparam=inputs_2f[5], + charge_spin=inputs_2f[6], tracing_mode="symbolic", _allow_non_fake_inputs=True, ) @@ -226,7 +243,9 @@ def model_forward_lower_export_round_trip( loaded = torch.export.load(f.name).module() # 6. Compare loaded vs eager (nf=1 — different shapes) - ret_loaded_1f = loaded(ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam) + ret_loaded_1f = loaded( + ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam, charge_spin + ) for key in output_keys: np.testing.assert_allclose( ret_eager[key].detach().cpu().numpy(), diff --git a/source/tests/pt_expt/infer/test_deep_eval.py b/source/tests/pt_expt/infer/test_deep_eval.py index f77b882b7c..7537575f1a 100644 --- a/source/tests/pt_expt/infer/test_deep_eval.py +++ b/source/tests/pt_expt/infer/test_deep_eval.py @@ -244,12 +244,18 @@ def test_dynamic_shapes(self) -> None: exported_mod = exported.module() for nloc in [2, 5, 10]: - ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam = ( + ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam, charge_spin = ( _make_sample_inputs(self.model, nloc=nloc) ) pte_ret = exported_mod( - ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam + ext_coord, + ext_atype, + nlist_t, + mapping_t, + fparam, + aparam, + charge_spin, ) ec = ext_coord.detach().requires_grad_(True) @@ -261,6 +267,7 @@ def test_dynamic_shapes(self) -> None: fparam=fparam, aparam=aparam, do_atomic_virial=True, + charge_spin=charge_spin, ) for key in ("energy", "energy_redu", "energy_derv_r", "energy_derv_c"): @@ -296,8 +303,8 @@ def test_oversized_nlist(self) -> None: nnei = sum(self.sel) # model's expected neighbor count nloc = 5 - ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam = _make_sample_inputs( - self.model, nloc=nloc + ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam, charge_spin = ( + _make_sample_inputs(self.model, nloc=nloc) ) # Pad nlist with -1 columns, then shuffle column order so real @@ -331,11 +338,18 @@ def test_oversized_nlist(self) -> None: fparam=fparam, aparam=aparam, do_atomic_virial=True, + charge_spin=charge_spin, ) # Exported model with same shuffled oversized nlist pte_ret = exported_mod( - ext_coord, ext_atype, nlist_shuffled, mapping_t, fparam, aparam + ext_coord, + ext_atype, + nlist_shuffled, + mapping_t, + fparam, + aparam, + charge_spin, ) for key in ("energy", "energy_redu", "energy_derv_r", "energy_derv_c"): @@ -362,6 +376,7 @@ def test_oversized_nlist(self) -> None: fparam=fparam, aparam=aparam, do_atomic_virial=True, + charge_spin=charge_spin, ) # The truncated result MUST differ from the correctly sorted result, # proving that naive truncation discards real neighbors. @@ -382,7 +397,7 @@ def test_serialize_round_trip(self) -> None: model2.eval() for nloc in [3, 7]: - ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam = ( + ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam, charge_spin = ( _make_sample_inputs(self.model, nloc=nloc) ) ec1 = ext_coord.detach().requires_grad_(True) @@ -396,6 +411,7 @@ def test_serialize_round_trip(self) -> None: fparam=fparam, aparam=aparam, do_atomic_virial=True, + charge_spin=charge_spin, ) ret2 = model2.forward_common_lower( ec2, @@ -405,6 +421,7 @@ def test_serialize_round_trip(self) -> None: fparam=fparam, aparam=aparam, do_atomic_virial=True, + charge_spin=charge_spin, ) for key in ("energy", "energy_redu", "energy_derv_r", "energy_derv_c"): @@ -943,8 +960,8 @@ def test_oversized_nlist(self) -> None: nnei = sum(self.sel) # model's expected neighbor count nloc = 5 - ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam = _make_sample_inputs( - self.model, nloc=nloc + ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam, charge_spin = ( + _make_sample_inputs(self.model, nloc=nloc) ) # Pad nlist with -1 columns, then shuffle column order so real @@ -977,10 +994,17 @@ def test_oversized_nlist(self) -> None: fparam=fparam, aparam=aparam, do_atomic_virial=True, + charge_spin=charge_spin, ) pte_ret = exported_mod( - ext_coord, ext_atype, nlist_shuffled, mapping_t, fparam, aparam + ext_coord, + ext_atype, + nlist_shuffled, + mapping_t, + fparam, + aparam, + charge_spin, ) for key in ("energy", "energy_redu", "energy_derv_r", "energy_derv_c"): @@ -1004,6 +1028,7 @@ def test_oversized_nlist(self) -> None: fparam=fparam, aparam=aparam, do_atomic_virial=True, + charge_spin=charge_spin, ) e_ref = ref_ret["energy_redu"].detach().cpu().numpy() e_trunc = trunc_ret["energy_redu"].detach().cpu().numpy() @@ -1022,7 +1047,7 @@ def test_serialize_round_trip(self) -> None: model2.eval() for nloc in [3, 7]: - ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam = ( + ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam, charge_spin = ( _make_sample_inputs(self.model, nloc=nloc) ) ec1 = ext_coord.detach().requires_grad_(True) @@ -1036,6 +1061,7 @@ def test_serialize_round_trip(self) -> None: fparam=fparam, aparam=aparam, do_atomic_virial=True, + charge_spin=charge_spin, ) ret2 = model2.forward_common_lower( ec2, @@ -1045,6 +1071,7 @@ def test_serialize_round_trip(self) -> None: fparam=fparam, aparam=aparam, do_atomic_virial=True, + charge_spin=charge_spin, ) for key in ("energy", "energy_redu", "energy_derv_r", "energy_derv_c"): diff --git a/source/tests/pt_expt/model/test_ener_model.py b/source/tests/pt_expt/model/test_ener_model.py index b91653a260..79946221af 100644 --- a/source/tests/pt_expt/model/test_ener_model.py +++ b/source/tests/pt_expt/model/test_ener_model.py @@ -182,6 +182,12 @@ def test_forward_lower_exportable(self) -> None: dtype=torch.float64, device=self.device, ) + charge_spin_zero = torch.zeros( + nframes, + 2, + dtype=torch.float64, + device=self.device, + ) # --- eager reference with zero params --- ret_eager_zero = md.forward_lower( @@ -204,13 +210,22 @@ def test_forward_lower_exportable(self) -> None: mapping_t, fparam=fparam_zero, aparam=aparam_zero, + charge_spin=charge_spin_zero, do_atomic_virial=True, ) self.assertIsInstance(traced, torch.nn.Module) exported = torch.export.export( traced, - (ext_coord, ext_atype, nlist_t, mapping_t, fparam_zero, aparam_zero), + ( + ext_coord, + ext_atype, + nlist_t, + mapping_t, + fparam_zero, + aparam_zero, + charge_spin_zero, + ), strict=False, ) self.assertIsNotNone(exported) @@ -223,6 +238,7 @@ def test_forward_lower_exportable(self) -> None: mapping_t, fparam_zero, aparam_zero, + charge_spin_zero, ) ret_exported_zero = exported.module()( ext_coord, @@ -231,6 +247,7 @@ def test_forward_lower_exportable(self) -> None: mapping_t, fparam_zero, aparam_zero, + charge_spin_zero, ) for key in output_keys: np.testing.assert_allclose( @@ -278,6 +295,7 @@ def test_forward_lower_exportable(self) -> None: mapping_t, fparam_nz, aparam_nz, + charge_spin_zero, ) ret_exported_nz = exported.module()( ext_coord, @@ -286,6 +304,7 @@ def test_forward_lower_exportable(self) -> None: mapping_t, fparam_nz, aparam_nz, + charge_spin_zero, ) for key in output_keys: np.testing.assert_allclose( @@ -321,6 +340,7 @@ def test_forward_lower_exportable(self) -> None: mapping_t, fparam_zero, aparam_nz, + charge_spin_zero, ) self.assertFalse( np.allclose( @@ -351,6 +371,7 @@ def test_forward_lower_exportable(self) -> None: mapping_t, fparam_zero, aparam_zero, + charge_spin_zero, ) ) @@ -361,6 +382,7 @@ def test_forward_lower_exportable(self) -> None: inputs_5f[3], fparam=inputs_5f[4], aparam=inputs_5f[5], + charge_spin=inputs_5f[6], do_atomic_virial=True, tracing_mode="symbolic", _allow_non_fake_inputs=True, @@ -390,7 +412,13 @@ def test_forward_lower_exportable(self) -> None: do_atomic_virial=True, ) ret_loaded_1f = loaded( - ext_coord, ext_atype, nlist_t, mapping_t, fparam_zero, aparam_zero + ext_coord, + ext_atype, + nlist_t, + mapping_t, + fparam_zero, + aparam_zero, + charge_spin_zero, ) for key in ret_common: np.testing.assert_allclose( diff --git a/source/tests/pt_expt/model/test_export_pipeline.py b/source/tests/pt_expt/model/test_export_pipeline.py index 23e0a62a98..478298c92f 100644 --- a/source/tests/pt_expt/model/test_export_pipeline.py +++ b/source/tests/pt_expt/model/test_export_pipeline.py @@ -121,7 +121,9 @@ def test_export_pipeline(self, descriptor_type, with_fparam) -> None: inputs_trace = _make_sample_inputs(model2, nframes=5, nloc=7) finally: _env.DEVICE = orig_device - ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam = inputs_trace + ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam, charge_spin = ( + inputs_trace + ) # 4. Eager reference eager_out = model2.forward_common_lower( @@ -132,6 +134,7 @@ def test_export_pipeline(self, descriptor_type, with_fparam) -> None: fparam=fparam, aparam=aparam, do_atomic_virial=True, + charge_spin=charge_spin, ) # 5. Trace with symbolic mode (same as dp freeze) @@ -142,6 +145,7 @@ def test_export_pipeline(self, descriptor_type, with_fparam) -> None: mapping_t, fparam=fparam, aparam=aparam, + charge_spin=charge_spin, do_atomic_virial=True, tracing_mode="symbolic", _allow_non_fake_inputs=True, @@ -155,11 +159,12 @@ def test_export_pipeline(self, descriptor_type, with_fparam) -> None: mapping_t, fparam, aparam, + charge_spin, model_nnei=sum(model2.get_sel()), ) exported = torch.export.export( traced, - (ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam), + (ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam, charge_spin), dynamic_shapes=dynamic_shapes, strict=False, prefer_deferred_runtime_asserts_over_guards=True, @@ -171,7 +176,9 @@ def test_export_pipeline(self, descriptor_type, with_fparam) -> None: loaded = torch.export.load(tmp.name).module() # 8. Verify: traced output matches eager (same shapes as trace) - traced_out = traced(ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam) + traced_out = traced( + ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam, charge_spin + ) for key in eager_out: np.testing.assert_allclose( eager_out[key].detach().cpu().numpy(), @@ -182,7 +189,9 @@ def test_export_pipeline(self, descriptor_type, with_fparam) -> None: ) # 9. Verify: loaded (.pte) output matches eager (same shapes) - loaded_out = loaded(ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam) + loaded_out = loaded( + ext_coord, ext_atype, nlist_t, mapping_t, fparam, aparam, charge_spin + ) for key in eager_out: np.testing.assert_allclose( eager_out[key].detach().cpu().numpy(), @@ -206,6 +215,7 @@ def test_export_pipeline(self, descriptor_type, with_fparam) -> None: mapping_t2, fparam2, aparam2, + charge_spin2, ) = inputs_infer eager_out2 = model2.forward_common_lower( @@ -216,9 +226,16 @@ def test_export_pipeline(self, descriptor_type, with_fparam) -> None: fparam=fparam2, aparam=aparam2, do_atomic_virial=True, + charge_spin=charge_spin2, ) loaded_out2 = loaded( - ext_coord2, ext_atype2, nlist_t2, mapping_t2, fparam2, aparam2 + ext_coord2, + ext_atype2, + nlist_t2, + mapping_t2, + fparam2, + aparam2, + charge_spin2, ) for key in eager_out2: np.testing.assert_allclose( @@ -248,9 +265,16 @@ def test_export_pipeline(self, descriptor_type, with_fparam) -> None: fparam=fparam_ones, aparam=aparam, do_atomic_virial=True, + charge_spin=charge_spin, ) loaded_out_fp1 = loaded( - ext_coord, ext_atype, nlist_t, mapping_t, fparam_ones, aparam + ext_coord, + ext_atype, + nlist_t, + mapping_t, + fparam_ones, + aparam, + charge_spin, ) # Loaded with fparam=1 should match eager with fparam=1 for key in eager_out_fp1: diff --git a/source/tests/pt_expt/model/test_export_with_comm.py b/source/tests/pt_expt/model/test_export_with_comm.py index dcbc628e53..ec305f2ed0 100644 --- a/source/tests/pt_expt/model/test_export_with_comm.py +++ b/source/tests/pt_expt/model/test_export_with_comm.py @@ -349,9 +349,11 @@ def test_pte_with_comm_dict_traces_and_loads(tmp_path) -> None: assert os.path.exists(pte_path) loaded = torch.export.load(pte_path) # Sanity: the loaded program has the expected number of inputs - # (6 base + 8 comm = 14). + # (7 base + 8 comm = 15): extended_coord, extended_atype, nlist, + # mapping, fparam, aparam, charge_spin (added in 0505_reformat_chg_spin) + # + the 8 comm tensors. spec = loaded.module().graph.find_nodes(op="placeholder") - assert len(spec) == 14, ( - f"with-comm exported program must accept 14 positional inputs " - f"(6 base + 8 comm); got {len(spec)}" + assert len(spec) == 15, ( + f"with-comm exported program must accept 15 positional inputs " + f"(7 base + 8 comm); got {len(spec)}" ) diff --git a/source/tests/pt_expt/model/test_spin_ener_model.py b/source/tests/pt_expt/model/test_spin_ener_model.py index f7f96392d3..1f600934b5 100644 --- a/source/tests/pt_expt/model/test_spin_ener_model.py +++ b/source/tests/pt_expt/model/test_spin_ener_model.py @@ -495,14 +495,23 @@ def test_forward_lower_exportable(self) -> None: # --- export with torch.export --- exported = torch.export.export( traced, - (ext_coord_t, ext_atype_t, ext_spin_t, nlist_t, mapping_t, None, None), + ( + ext_coord_t, + ext_atype_t, + ext_spin_t, + nlist_t, + mapping_t, + None, + None, + None, + ), strict=False, ) self.assertIsNotNone(exported) # --- verify traced matches eager --- ret_traced = traced( - ext_coord_t, ext_atype_t, ext_spin_t, nlist_t, mapping_t, None, None + ext_coord_t, ext_atype_t, ext_spin_t, nlist_t, mapping_t, None, None, None ) for key in output_keys: np.testing.assert_allclose( @@ -515,7 +524,7 @@ def test_forward_lower_exportable(self) -> None: # --- verify exported matches eager --- ret_exported = exported.module()( - ext_coord_t, ext_atype_t, ext_spin_t, nlist_t, mapping_t, None, None + ext_coord_t, ext_atype_t, ext_spin_t, nlist_t, mapping_t, None, None, None ) for key in output_keys: np.testing.assert_allclose( @@ -538,6 +547,7 @@ def test_forward_lower_exportable(self) -> None: torch.cat([mapping_t, mapping_t], dim=0), None, None, + None, ) traced_sym = model.forward_lower_exportable( @@ -551,7 +561,7 @@ def test_forward_lower_exportable(self) -> None: ) # Build dynamic shapes for spin model - # (ext_coord, ext_atype, ext_spin, nlist, mapping, fparam, aparam) + # (ext_coord, ext_atype, ext_spin, nlist, mapping, fparam, aparam, charge_spin) nframes_dim = torch.export.Dim("nframes", min=1) nall_dim = torch.export.Dim("nall", min=1) nloc_dim = torch.export.Dim("nloc", min=1) @@ -563,6 +573,7 @@ def test_forward_lower_exportable(self) -> None: {0: nframes_dim, 1: nall_dim}, # mapping None, # fparam None, # aparam + None, # charge_spin ) exported_dyn = torch.export.export( traced_sym, @@ -577,7 +588,7 @@ def test_forward_lower_exportable(self) -> None: loaded = torch.export.load(f.name).module() ret_loaded_1f = loaded( - ext_coord_t, ext_atype_t, ext_spin_t, nlist_t, mapping_t, None, None + ext_coord_t, ext_atype_t, ext_spin_t, nlist_t, mapping_t, None, None, None ) for key in output_keys: np.testing.assert_allclose( @@ -632,7 +643,14 @@ def test_oversized_nlist(self) -> None: mapping_t, ) ret_traced = traced( - ext_coord_t, ext_atype_t, ext_spin_t, nlist_shuffled, mapping_t, None, None + ext_coord_t, + ext_atype_t, + ext_spin_t, + nlist_shuffled, + mapping_t, + None, + None, + None, ) ec = ext_coord_t.detach().requires_grad_(True) diff --git a/source/tests/pt_expt/model/test_spin_export_with_comm.py b/source/tests/pt_expt/model/test_spin_export_with_comm.py index 0e403d2b42..971a96623a 100644 --- a/source/tests/pt_expt/model/test_spin_export_with_comm.py +++ b/source/tests/pt_expt/model/test_spin_export_with_comm.py @@ -108,6 +108,7 @@ def test_spin_forward_common_lower_exportable_with_comm_traces() -> None: mapping = torch.zeros(1, nall, dtype=torch.int64) fparam = None aparam = None + charge_spin = None comm_inputs, _keepalive = _build_self_comm_inputs(nloc=nloc, nghost=nall - nloc) @@ -125,6 +126,7 @@ def test_spin_forward_common_lower_exportable_with_comm_traces() -> None: mapping, fparam, aparam, + charge_spin, *comm_inputs, do_atomic_virial=True, tracing_mode="symbolic", @@ -142,6 +144,7 @@ def test_spin_forward_common_lower_exportable_with_comm_traces() -> None: mapping, fparam, aparam, + charge_spin, *comm_inputs, ) assert isinstance(out, dict) diff --git a/source/tests/pt_expt/test_training.py b/source/tests/pt_expt/test_training.py index bb3123b1ed..07bbf2c06a 100644 --- a/source/tests/pt_expt/test_training.py +++ b/source/tests/pt_expt/test_training.py @@ -574,6 +574,9 @@ def has_default_fparam(self) -> bool: def get_default_fparam(self) -> list[float]: return [0.0, 1.0] + def has_chg_spin_ebd(self) -> bool: + return False + reqs = get_additional_data_requirement(_M()) self.assertEqual(len(reqs), 1) fparam_req = reqs[0] @@ -605,6 +608,9 @@ def has_default_fparam(self) -> bool: def get_default_fparam(self) -> None: return None + def has_chg_spin_ebd(self) -> bool: + return False + reqs = get_additional_data_requirement(_M()) self.assertEqual(len(reqs), 1) fparam_req = reqs[0] diff --git a/source/tests/universal/dpmodel/descriptor/test_descriptor.py b/source/tests/universal/dpmodel/descriptor/test_descriptor.py index 6f5a337b4d..010cf4dcd6 100644 --- a/source/tests/universal/dpmodel/descriptor/test_descriptor.py +++ b/source/tests/universal/dpmodel/descriptor/test_descriptor.py @@ -489,6 +489,7 @@ def DescriptorParamDPA3( precision="float64", use_loc_mapping=True, add_chg_spin_ebd=False, + default_chg_spin=None, ): input_dict = { # kwargs for repformer @@ -537,6 +538,7 @@ def DescriptorParamDPA3( "use_tebd_bias": False, "use_loc_mapping": use_loc_mapping, "add_chg_spin_ebd": add_chg_spin_ebd, + "default_chg_spin": default_chg_spin, "type_map": type_map, "seed": GLOBAL_SEED, } @@ -563,10 +565,24 @@ def DescriptorParamDPA3( "env_protection": (0.0, 1e-8), "precision": ("float64",), "use_loc_mapping": (True, False), - "add_chg_spin_ebd": (False, True), } ), ) + + +def DescriptorParamDPA3DefaultChgSpin(ntypes, rcut, rcut_smth, sel, type_map, **kwargs): + return DescriptorParamDPA3( + ntypes, + rcut, + rcut_smth, + sel, + type_map, + **kwargs, + add_chg_spin_ebd=True, + default_chg_spin=[5.0, 1.0], + ) + + # to get name for the default function DescriptorParamDPA3 = DescriptorParamDPA3List[0] @@ -642,3 +658,37 @@ def setUp(self) -> None: self.nt, self.rcut, self.rcut_smth, self.sel, ["O", "H"] ) self.module = Descrpt(**self.input_dict) + + +class TestHybridChgSpinDefaultDP(unittest.TestCase): + def _make_dpa3(self, default_chg_spin: list[float] | None) -> DescrptDPA3: + return DescrptDPA3( + **DescriptorParamDPA3( + 2, + 4.0, + 0.5, + [6, 6], + ["O", "H"], + add_chg_spin_ebd=True, + default_chg_spin=default_chg_spin, + ) + ) + + def test_shared_default_required_for_hybrid_default(self) -> None: + shared_default = DescrptHybrid( + list=[self._make_dpa3([5.0, 1.0]), self._make_dpa3([5.0, 1.0])] + ) + self.assertTrue(shared_default.has_default_chg_spin()) + self.assertEqual(shared_default.get_default_chg_spin(), [5.0, 1.0]) + + missing_default = DescrptHybrid( + list=[self._make_dpa3([5.0, 1.0]), self._make_dpa3(None)] + ) + self.assertFalse(missing_default.has_default_chg_spin()) + self.assertIsNone(missing_default.get_default_chg_spin()) + + mismatched_default = DescrptHybrid( + list=[self._make_dpa3([5.0, 1.0]), self._make_dpa3([6.0, 1.0])] + ) + self.assertFalse(mismatched_default.has_default_chg_spin()) + self.assertIsNone(mismatched_default.get_default_chg_spin()) diff --git a/source/tests/universal/dpmodel/model/test_model.py b/source/tests/universal/dpmodel/model/test_model.py index b5c6bd82ee..65ac116807 100644 --- a/source/tests/universal/dpmodel/model/test_model.py +++ b/source/tests/universal/dpmodel/model/test_model.py @@ -63,13 +63,10 @@ def skip_model_tests(test_obj): if test_obj.input_dict_ds.get("add_chg_spin_ebd", False): - import inspect - - (FittingParam, _) = test_obj.param[1] - sig = inspect.signature(FittingParam) - numb_param = sig.parameters.get("numb_param") - if numb_param is None or numb_param.default != 2: - return True, "add_chg_spin_ebd requires numb_fparam=2" + # The universal model driver does not feed `charge_spin` directly; + # rely on `default_chg_spin` fallback inside dp_atomic_model. + if test_obj.input_dict_ds.get("default_chg_spin") is None: + return True, "add_chg_spin_ebd requires default_chg_spin in universal tests" if not test_obj.input_dict_ds.get( "smooth_type_embedding", True ) or not test_obj.input_dict_ds.get("smooth", True):