Skip to content
12 changes: 12 additions & 0 deletions deepmd/dpmodel/model/base_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ def get_model_def_script(self) -> str:
"""Get the model definition script."""
pass

def get_observed_type_list(self) -> list[str]:
Comment thread Fixed
"""Get observed types from model metadata.

Returns empty list if not available.
"""
if self.model_def_script:
params = json.loads(self.model_def_script)
observed = params.get("info", {}).get("observed_type")
if observed is not None:
return observed
return []

Comment thread
iProzd marked this conversation as resolved.
Outdated
def get_min_nbor_dist(self) -> float | None:
"""Get the minimum distance between two atoms."""
return self.min_nbor_dist
Expand Down
56 changes: 56 additions & 0 deletions deepmd/dpmodel/utils/stat.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,62 @@
log = logging.getLogger(__name__)


def collect_observed_types(sampled: list[dict], type_map: list[str]) -> list[str]:
"""Collect observed element types from sampled training data.

Parameters
----------
sampled : list[dict]
Sampled data from different data systems. Each dict must contain
``"atype"`` with shape ``[nframes, natoms]``.
type_map : list[str]
Mapping from type index to element symbol.

Returns
-------
list[str]
Sorted list of observed element symbols.
"""
from deepmd.utils.econf_embd import (
sort_element_type,
)

observed_indices: set[int] = set()
for system in sampled:
atype = to_numpy_array(system["atype"]) # shape: [nframes, natoms]
observed_indices.update(np.unique(atype).tolist())
observed_types = [
type_map[i] for i in sorted(observed_indices) if i < len(type_map)
]
return sort_element_type(observed_types)


def _restore_observed_type_from_file(
stat_file_path: DPPath | None,
) -> list[str] | None:
"""Try to load observed_type from stat file."""
if stat_file_path is None:
return None
fp = stat_file_path / "observed_type"
if fp.is_file():
arr = fp.load_numpy()
# Decode bytes back to str if stored as bytes (for h5py compatibility)
return [x.decode() if isinstance(x, bytes) else x for x in arr.tolist()]
return None


def _save_observed_type_to_file(
stat_file_path: DPPath | None, observed_type: list[str]
) -> None:
"""Save observed_type to stat file."""
if stat_file_path is None:
return
stat_file_path.mkdir(exist_ok=True, parents=True)
fp = stat_file_path / "observed_type"
# Use bytes dtype for h5py compatibility (h5py cannot store Unicode strings)
fp.save_numpy(np.array(observed_type, dtype="S"))


def _restore_from_file(
stat_file_path: DPPath,
keys: list[str],
Expand Down
9 changes: 8 additions & 1 deletion deepmd/entrypoints/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ def show(
)
else:
log.info("The observed types for this model: ")
observed_types = model.get_observed_types()
observed_type_list = model_params.get("info", {}).get("observed_type")
if observed_type_list is not None:
observed_types = {
"type_num": len(observed_type_list),
"observed_type": observed_type_list,
}
else:
observed_types = model.get_observed_types()
log.info(f"Number of observed types: {observed_types['type_num']} ")
log.info(f"Observed types: {observed_types['observed_type']} ")
8 changes: 8 additions & 0 deletions deepmd/pt/infer/deep_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,14 @@ def get_observed_types(self) -> dict:
- 'type_num': the total number of observed types in this model.
- 'observed_type': a list of the observed types in this model.
"""
# Try metadata first (from model_def_script, already a dict)
observed_type_list = self.model_def_script.get("info", {}).get("observed_type")
if observed_type_list is not None:
return {
"type_num": len(observed_type_list),
"observed_type": observed_type_list,
}
# Fallback: bias-based approach for old models
observed_type_list = self.dp.model["Default"].get_observed_type_list()
return {
"type_num": len(observed_type_list),
Expand Down
2 changes: 2 additions & 0 deletions deepmd/pt/model/atomic_model/base_atomic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def __init__(
self.rcond = rcond
self.preset_out_bias = preset_out_bias
self.data_stat_protect = data_stat_protect
self._observed_type: list[str] | None = None

def init_out_stat(self) -> None:
"""Initialize the output bias."""
Expand Down Expand Up @@ -371,6 +372,7 @@ def compute_or_load_stat(
merged: Callable[[], list[dict]] | list[dict],
stat_file_path: DPPath | None = None,
compute_or_load_out_stat: bool = True,
preset_observed_type: list[str] | None = None,
) -> NoReturn:
"""
Compute or load the statistics parameters of the model,
Expand Down
17 changes: 17 additions & 0 deletions deepmd/pt/model/atomic_model/dp_atomic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
from deepmd.pt.model.task.base_fitting import (
BaseFitting,
)
from deepmd.pt.utils.stat import (
_restore_observed_type_from_file,
_save_observed_type_to_file,
collect_observed_types,
)
from deepmd.utils.path import (
DPPath,
)
Expand Down Expand Up @@ -307,6 +312,7 @@ def compute_or_load_stat(
sampled_func: Callable[[], list[dict]],
stat_file_path: DPPath | None = None,
compute_or_load_out_stat: bool = True,
preset_observed_type: list[str] | None = None,
) -> None:
"""
Compute or load the statistics parameters of the model,
Expand Down Expand Up @@ -358,6 +364,17 @@ def wrapped_sampler() -> list[dict]:
if compute_or_load_out_stat:
self.compute_or_load_out_stat(wrapped_sampler, stat_file_path)

# Collect observed types with priority: preset > stat_file > compute
if preset_observed_type is not None:
self._observed_type = preset_observed_type
else:
observed = _restore_observed_type_from_file(stat_file_path)
if observed is None:
sampled = wrapped_sampler()
observed = collect_observed_types(sampled, self.type_map)
_save_observed_type_to_file(stat_file_path, observed)
self._observed_type = observed

Comment thread
iProzd marked this conversation as resolved.
Outdated
def compute_fitting_input_stat(
self,
sample_merged: Callable[[], list[dict]] | list[dict],
Expand Down
18 changes: 18 additions & 0 deletions deepmd/pt/model/atomic_model/linear_atomic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ def compute_or_load_stat(
sampled_func: Callable[[], list[dict[str, Any]]],
stat_file_path: DPPath | None = None,
compute_or_load_out_stat: bool = True,
preset_observed_type: list[str] | None = None,
) -> None:
"""
Compute or load the statistics parameters of the model,
Expand Down Expand Up @@ -523,6 +524,23 @@ def wrapped_sampler() -> list[dict[str, Any]]:

self.compute_or_load_out_stat(wrapped_sampler, stat_file_path)

# Collect observed types with priority: preset > stat_file > compute
from deepmd.dpmodel.utils.stat import (
_restore_observed_type_from_file,
_save_observed_type_to_file,
collect_observed_types,
)

if preset_observed_type is not None:
self._observed_type = preset_observed_type
else:
observed = _restore_observed_type_from_file(stat_file_path)
if observed is None:
sampled = wrapped_sampler()
observed = collect_observed_types(sampled, self.type_map)
_save_observed_type_to_file(stat_file_path, observed)
self._observed_type = observed


class DPZBLLinearEnergyAtomicModel(LinearEnergyAtomicModel):
"""Model linearly combine a list of AtomicModels.
Expand Down
1 change: 1 addition & 0 deletions deepmd/pt/model/atomic_model/pairtab_atomic_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ def compute_or_load_stat(
sampled_func: Callable[[], list[dict]] | list[dict],
stat_file_path: DPPath | None = None,
compute_or_load_out_stat: bool = True,
preset_observed_type: list[str] | None = None,
Comment thread
iProzd marked this conversation as resolved.
) -> None:
"""
Compute or load the statistics parameters of the model,
Expand Down
7 changes: 6 additions & 1 deletion deepmd/pt/model/model/make_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -587,9 +587,14 @@ def compute_or_load_stat(
self,
sampled_func: Callable[[], Any],
stat_file_path: DPPath | None = None,
preset_observed_type: list[str] | None = None,
) -> None:
"""Compute or load the statistics."""
return self.atomic_model.compute_or_load_stat(sampled_func, stat_file_path)
return self.atomic_model.compute_or_load_stat(
sampled_func,
stat_file_path,
preset_observed_type=preset_observed_type,
)

def get_sel(self) -> list[int]:
"""Returns the number of selected atoms for each type."""
Expand Down
1 change: 1 addition & 0 deletions deepmd/pt/model/model/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def compute_or_load_stat(
self,
sampled_func: Any,
stat_file_path: DPPath | None = None,
preset_observed_type: list[str] | None = None,
) -> NoReturn:
"""
Compute or load the statistics parameters of the model,
Expand Down
7 changes: 6 additions & 1 deletion deepmd/pt/model/model/spin_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ def compute_or_load_stat(
self,
sampled_func: Callable[[], list[dict[str, Any]]],
stat_file_path: DPPath | None = None,
preset_observed_type: list[str] | None = None,
) -> None:
"""
Compute or load the statistics parameters of the model,
Expand Down Expand Up @@ -394,7 +395,11 @@ def spin_sampled_func() -> list[dict[str, Any]]:
spin_sampled.append(tmp_dict)
return spin_sampled

self.backbone_model.compute_or_load_stat(spin_sampled_func, stat_file_path)
self.backbone_model.compute_or_load_stat(
spin_sampled_func,
stat_file_path,
preset_observed_type=preset_observed_type,
)

def forward_common(
self,
Expand Down
30 changes: 30 additions & 0 deletions deepmd/pt/train/training.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
import functools
import json
import logging
import time
from collections.abc import (
Expand Down Expand Up @@ -288,6 +289,7 @@ def single_model_stat(
_training_data: DpLoaderSet,
_stat_file_path: str | None,
finetune_has_new_type: bool = False,
preset_observed_type: list[str] | None = None,
) -> Callable[[], Any]:
@functools.lru_cache
def get_sample() -> Any:
Expand All @@ -302,6 +304,7 @@ def get_sample() -> Any:
_model.compute_or_load_stat(
sampled_func=get_sample,
stat_file_path=_stat_file_path,
preset_observed_type=preset_observed_type,
)
if isinstance(_stat_file_path, DPH5Path):
_stat_file_path.root.close()
Expand Down Expand Up @@ -394,7 +397,14 @@ def get_lr(lr_params: dict[str, Any]) -> BaseLR:
finetune_has_new_type=self.finetune_links["Default"].get_has_new_type()
if self.finetune_links is not None
else False,
preset_observed_type=model_params.get("info", {}).get("observed_type"),
)
# Persist observed_type from stat into model_params and model_def_script
if not resuming and self.rank == 0:
observed = getattr(self.model.atomic_model, "_observed_type", None)
Comment thread
iProzd marked this conversation as resolved.
Outdated
if observed is not None:
model_params.setdefault("info", {})["observed_type"] = observed
self.model.model_def_script = json.dumps(model_params)
(
self.training_dataloader,
self.training_data,
Expand Down Expand Up @@ -432,6 +442,11 @@ def get_lr(lr_params: dict[str, Any]) -> BaseLR:
training_data[model_key].preload_and_modify_all_data_torch()
if validation_data[model_key] is not None:
validation_data[model_key].preload_and_modify_all_data_torch()
_mt_user_observed = (
model_params["model_dict"][model_key]
.get("info", {})
.get("observed_type")
)
self.get_sample_func[model_key] = single_model_stat(
self.model[model_key],
model_params["model_dict"][model_key].get("data_stat_nbatch", 10),
Expand All @@ -442,7 +457,22 @@ def get_lr(lr_params: dict[str, Any]) -> BaseLR:
].get_has_new_type()
if self.finetune_links is not None
else False,
preset_observed_type=_mt_user_observed,
)
# Persist observed_type into model_params and model_def_script
if not resuming and self.rank == 0:
observed = getattr(
self.model[model_key].atomic_model,
"_observed_type",
None,
)
if observed is not None:
model_params["model_dict"][model_key].setdefault("info", {})[
"observed_type"
] = observed
self.model[model_key].model_def_script = json.dumps(
model_params["model_dict"][model_key]
)

(
self.training_dataloader[model_key],
Expand Down
13 changes: 13 additions & 0 deletions deepmd/pt/utils/stat.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@

log = logging.getLogger(__name__)

# Re-export from dpmodel (backend-agnostic implementations)
from deepmd.dpmodel.utils.stat import (
_restore_observed_type_from_file,
_save_observed_type_to_file,
collect_observed_types,
)

__all__ = [
"_restore_observed_type_from_file",
"_save_observed_type_to_file",
"collect_observed_types",
]
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def make_stat_input(
datasets: list[Any], dataloaders: list[Any], nbatches: int
Expand Down
Loading