Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2f601e2
refactor: merge has_default_chg_spin into get_default_chg_spin; drop …
Aug 11, 2026
92431ed
refactor: declare property-model queries on make_base_model with conc…
Aug 11, 2026
11372dc
refactor: convert jax2tf property probe + refresh stale docstring (#5…
Aug 11, 2026
83e2cfe
refactor: add get_geo_compress() base accessor; drop consumer getattr…
Aug 11, 2026
f38e793
test: pin get_geo_compress() override/default branches in compression…
Aug 11, 2026
13b0aff
refactor: concrete reinit_exclude default on make_base_fitting (#5897)
Aug 11, 2026
61253d3
refactor: get_pair_exclude_types() accessor; pin pair_excl as direct-…
Aug 11, 2026
cc54052
refactor: pin pair_excl direct access in SpinModel.forward_common_low…
Aug 11, 2026
2865922
refactor: class-default stat flags on DescriptorBlock bases (#5897)
Aug 11, 2026
9659d4b
refactor: drop dead-defensive probes of base-declared members (#5897)
Aug 11, 2026
1072bb2
refactor: declare tebd compression slots in family __init__ (#5897)
Aug 11, 2026
cca4eb9
refactor: defuse dpa2 register_buffer trap for tebd compression slot
Aug 11, 2026
39a13d5
fix(dpmodel): move get_pair_exclude_types() to shared atomic-model base
Aug 12, 2026
7576d85
test: re-pin has_default_chg_spin absence as dpmodel-only, not shared
Aug 12, 2026
32416d4
fix(tf2): gate get_var_name export block on actual property-model check
Aug 12, 2026
3c38089
refactor(pd): complete has_default_chg_spin -> get_default_chg_spin m…
Aug 12, 2026
169cba7
refactor: direct pair_excl access + hoist get_var_name in pt_expt dee…
Aug 12, 2026
2a3fa71
test: pin BD-base chg-spin stat-flag defaults + merge_env_stat on bar…
Aug 12, 2026
d2b1c7c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 12, 2026
910a1e0
test: pin default_chg_spin shape, not container type, in shared capab…
Aug 12, 2026
eef5ab1
test: align test doubles with the promoted capability contracts
Aug 12, 2026
7ac154b
fix(jax),test: address capability-probe review round 2
Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions deepmd/dpmodel/atomic_model/base_atomic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,6 @@ 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
Expand Down
15 changes: 3 additions & 12 deletions deepmd/dpmodel/atomic_model/dp_atomic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,9 @@ def __init__(
super().__init__(type_map, **kwargs)
self.descriptor = descriptor
self.fitting_net = fitting
if hasattr(self.fitting_net, "reinit_exclude"):
self.fitting_net.reinit_exclude(self.atom_exclude_types)
self.fitting_net.reinit_exclude(self.atom_exclude_types)
self.type_map = type_map
self.add_chg_spin_ebd: bool = getattr(
self.descriptor, "add_chg_spin_ebd", False
)
self.add_chg_spin_ebd: bool = self.descriptor.get_dim_chg_spin() > 0
# Structural capability: only descriptors with a native spin
# conditioning mechanism (currently DPA4) accept a ``spin`` kwarg on
# ``call_graph`` at all -- unlike ``charge_spin``, which every
Expand Down Expand Up @@ -151,15 +148,9 @@ def get_dim_chg_spin(self) -> int:
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():
if self.add_chg_spin_ebd:
return self.descriptor.get_default_chg_spin()
return None

Expand Down
10 changes: 1 addition & 9 deletions deepmd/dpmodel/atomic_model/linear_atomic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -723,19 +723,11 @@ def _fparam_consumers(self) -> list:
"""Children that actually consume ``fparam``."""
return [m for m in self.models if m.get_dim_fparam() > 0]

def has_default_chg_spin(self) -> bool:
"""Whether every active child shares one default charge/spin."""
return self._agreed_default(
self._chg_spin_consumers(),
lambda m: m.has_default_chg_spin(),
lambda m: m.get_default_chg_spin(),
)[0]

def get_default_chg_spin(self) -> "Array | None":
"""The shared default charge/spin conditions, if the children agree."""
return self._agreed_default(
self._chg_spin_consumers(),
lambda m: m.has_default_chg_spin(),
lambda m: m.get_default_chg_spin() is not None,
lambda m: m.get_default_chg_spin(),
)[1]

Expand Down
8 changes: 8 additions & 0 deletions deepmd/dpmodel/atomic_model/make_base_atomic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ def get_nnei(self) -> int:
"""Returns the total number of selected neighboring atoms in the cut-off radius."""
return self.get_nsel()

def get_pair_exclude_types(self) -> list[tuple[int, int]]:
"""Return the excluded atom-type pairs of this atomic model.

Always set by ``__init__`` (empty list when no exclusion is
configured); an empty return means the pair-exclusion mask is off.
"""
return self.pair_exclude_types

@abstractmethod
def get_dim_fparam(self) -> int:
"""Get the number (dimension) of frame parameters of this atomic model."""
Expand Down
6 changes: 6 additions & 0 deletions deepmd/dpmodel/descriptor/descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ class DescriptorBlock(ABC, make_plugin_registry("DescriptorBlock")):

local_cluster = False

# Stat-behavior flags with concrete defaults so stat machinery can read
# them on any block without getattr probes; blocks that configure them
# assign instance attributes in __init__ (issue #5897).
set_davg_zero: bool = False
set_stddev_constant: bool = False

def __new__(cls, *args: Any, **kwargs: Any) -> Any:
if cls is DescriptorBlock:
try:
Expand Down
13 changes: 12 additions & 1 deletion deepmd/dpmodel/descriptor/dpa1.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,10 @@ def __init__(
self.concat_output_tebd = concat_output_tebd
self.trainable = trainable
self.precision = precision
# tebd-compression slots: declared here so presence is a class
# property, not a runtime accident (issue #5897); populated by
# enable_compression()/deserialize().
self.type_embd_data = None
self.tebd_compress = False
self.geo_compress = False
self.compress = False
Expand Down Expand Up @@ -1113,6 +1117,10 @@ def enable_compression(
stacklevel=2,
)

def get_geo_compress(self) -> bool:
"""Return whether geometric tabulated compression is active."""
return self.geo_compress

def serialize(self) -> dict:
"""Serialize the descriptor to dict."""
obj = self.se_atten
Expand Down Expand Up @@ -1172,7 +1180,7 @@ def serialize(self) -> dict:
if self.compress:
type_embd_data = (
self.type_embd_data
if hasattr(self, "type_embd_data")
if self.type_embd_data is not None
else obj.type_embd_data
)
compress_dict: dict = {
Expand Down Expand Up @@ -1514,6 +1522,9 @@ def __init__(
self.mean = np.zeros(wanted_shape, dtype=PRECISION_DICT[self.precision])
self.stddev = np.ones(wanted_shape, dtype=PRECISION_DICT[self.precision])
self.orig_sel = self.sel
# tebd-compression slots: declared here so presence is a class
# property, not a runtime accident (issue #5897); populated by
# type_embedding_compression()/enable_compression().
self.tebd_compress = False
self.geo_compress = False
self.is_sorted = len(self.exclude_types) == 0
Expand Down
4 changes: 4 additions & 0 deletions deepmd/dpmodel/descriptor/dpa2.py
Original file line number Diff line number Diff line change
Expand Up @@ -1421,6 +1421,10 @@ def _call_dense(
g1 = xp.concat([g1, g1_inp], axis=-1)
return g1, rot_mat, g2, h2, sw

def get_geo_compress(self) -> bool:
"""Return whether geometric tabulated compression is active."""
return self.geo_compress

def serialize(self) -> dict:
repinit = self.repinit
repformers = self.repformers
Expand Down
4 changes: 0 additions & 4 deletions deepmd/dpmodel/descriptor/dpa3.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,10 +543,6 @@ 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
Expand Down
4 changes: 0 additions & 4 deletions deepmd/dpmodel/descriptor/dpa4.py
Original file line number Diff line number Diff line change
Expand Up @@ -2286,10 +2286,6 @@ def get_dim_chg_spin(self) -> int:
"""Return the charge/spin condition width."""
return 2 if self.add_chg_spin_ebd else 0

def has_default_chg_spin(self) -> bool:
"""Return whether default charge/spin conditions are configured."""
return self.default_chg_spin is not None

def get_default_chg_spin(self) -> list[float] | None:
"""Return default charge/spin conditions."""
return self.default_chg_spin
Expand Down
25 changes: 9 additions & 16 deletions deepmd/dpmodel/descriptor/hybrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,33 +129,26 @@ def get_dim_chg_spin(self) -> int:
(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."""
def get_default_chg_spin(self) -> list[float] | None:
"""Returns the default charge_spin value, or None.

``None`` unless every sub-descriptor that supports charge_spin
(``get_dim_chg_spin() > 0``) agrees on the same default 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
return None
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
return None
return default_chg_spin if found_chg_spin else None

def get_rcut_smth(self) -> float:
"""Returns the radius where the neighbor information starts to smoothly decay to 0."""
Expand Down
21 changes: 17 additions & 4 deletions deepmd/dpmodel/descriptor/make_base_descriptor.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ def make_base_descriptor(
class BD(ABC, PluginVariant, make_plugin_registry("descriptor")):
"""Base descriptor provides the interfaces of descriptor."""

# Stat-behavior flags with concrete defaults so stat machinery (e.g.
# ``merge_env_stat``, which accepts either a ``Descriptor`` or a
# ``DescriptorBlock``) can read them on any descriptor without
# getattr probes; descriptors that configure them assign instance
# attributes in __init__ (issue #5897). Mirrors the same defaults on
# ``DescriptorBlock``.
set_davg_zero: bool = False
set_stddev_constant: bool = False

def __new__(cls, *args: Any, **kwargs: Any) -> Any:
if cls is BD:
cls = cls.get_class_by_type(j_get_type(kwargs, cls.__name__))
Expand Down Expand Up @@ -100,14 +109,18 @@ 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

def get_geo_compress(self) -> bool:
"""Return whether geometric tabulated compression is active.

Concrete default ``False``; descriptor families with a
geometric compression path override from their own state.
"""
return False

@abstractmethod
def mixed_types(self) -> bool:
"""Returns if the descriptor requires a neighbor list that distinguish different
Expand Down
2 changes: 1 addition & 1 deletion deepmd/dpmodel/descriptor/se_atten_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def serialize(self) -> dict:
if self.compress:
type_embd_data = (
self.type_embd_data
if hasattr(self, "type_embd_data")
if self.type_embd_data is not None
else obj.type_embd_data
)
compress_dict: dict = {
Expand Down
7 changes: 6 additions & 1 deletion deepmd/dpmodel/descriptor/se_t_tebd.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ def __init__(
self.trainable = trainable
self.precision = precision
self.compress = False
# tebd-compression slot: declared here so presence is a class
# property, not a runtime accident (issue #5897); optionally
# populated by deserialize() (only present in compressed models
# that carry type embedding compression data).
self.type_embd_data = None

def get_rcut(self) -> float:
"""Returns the cut-off radius."""
Expand Down Expand Up @@ -463,7 +468,7 @@ def serialize(self) -> dict:
"compress_info": [to_numpy_array(i) for i in self.compress_info],
},
}
if hasattr(self, "type_embd_data"):
if self.type_embd_data is not None:
compress_dict["@variables"]["type_embd_data"] = to_numpy_array(
self.type_embd_data
)
Expand Down
24 changes: 24 additions & 0 deletions deepmd/dpmodel/fitting/make_base_fitting.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,30 @@ def compute_output_stats(self, merged: Any) -> NoReturn:
"""Update the output bias for fitting net."""
raise NotImplementedError

def reinit_exclude(self, exclude_types: list[int] = []) -> None:
"""Reinitialize the per-type output exclusion list.

Concrete default for fittings without exclusion support: an
empty list is a no-op; a non-empty list raises, because
silently ignoring a requested exclusion would degrade the
model without any signal.

Parameters
----------
exclude_types
Atom types whose fitting output is excluded.

Raises
------
NotImplementedError
If ``exclude_types`` is non-empty and this fitting does
not support atom-type exclusion.
"""
if exclude_types:
raise NotImplementedError(
"this fitting does not support atom-type exclusion"
)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
@abstractmethod
def get_type_map(self) -> list[str]:
"""Get the name to each type of atoms."""
Expand Down
39 changes: 39 additions & 0 deletions deepmd/dpmodel/model/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,45 @@ def has_spin(self) -> bool:
"""
return False

def has_chg_spin_ebd(self) -> bool:
"""Return whether the model conditions on charge/spin embedding.

Concrete default ``False``; models wrapping an atomic model
override to delegate.
"""
return False

def get_dim_chg_spin(self) -> int:
"""Return the charge/spin condition width (0 if unsupported)."""
return 0

def get_default_chg_spin(self) -> list | None:
"""Return default charge/spin conditions, or ``None`` if none
are configured. ``is not None`` is the support predicate.
"""
return None

def get_var_name(self) -> str | None:
"""Return the fitted property's variable name, or ``None`` if
this is not a property model. ``is not None`` is the support
predicate.
"""
return None

def get_task_dim(self) -> int:
"""Return the property output dimension (property models only).

Raises
------
NotImplementedError
If the model is not a property model.
"""
raise NotImplementedError("get_task_dim: property models only")

def get_intensive(self) -> bool:
"""Return whether the fitted property is intensive."""
return False

@abstractmethod
def serialize(self) -> dict:
"""Serialize the model.
Expand Down
8 changes: 2 additions & 6 deletions deepmd/dpmodel/model/make_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ def call_common(
charge_spin=cs,
neighbor_list=neighbor_list,
# exclusion is a nlist-BUILD transform (decision #18/A4)
pair_excl=getattr(self.atomic_model, "pair_excl", None),
pair_excl=self.atomic_model.pair_excl,
)
model_predict = self._output_type_cast(model_predict, input_prec)
return model_predict
Expand Down Expand Up @@ -527,7 +527,7 @@ def _call_common_graph(
# is constructed, so the graph lower / exported ``.pt2`` consumes an
# already-excluded ``edge_mask`` and never re-applies it. Mirrors the
# pt_expt eager path and the C++ ``applyPairExclusion`` at build.
pair_excl = getattr(self.atomic_model, "pair_excl", None)
pair_excl = self.atomic_model.pair_excl
if method == "dense":
ng = build_neighbor_graph(
cc, atype, bb, self.get_rcut(), pair_excl=pair_excl
Expand Down Expand Up @@ -1131,10 +1131,6 @@ 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()
Expand Down
Loading
Loading