Skip to content

Commit e76b702

Browse files
author
Han Wang
committed
feat: add stat for dpmodel's atomic model. implement atomic model for pt_expt
1 parent 165d1df commit e76b702

9 files changed

Lines changed: 1833 additions & 1 deletion

File tree

deepmd/dpmodel/atomic_model/base_atomic_model.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# SPDX-License-Identifier: LGPL-3.0-or-later
22
import math
3+
from collections.abc import (
4+
Callable,
5+
)
36
from typing import (
47
Any,
58
)
@@ -30,6 +33,9 @@
3033
map_atom_exclude_types,
3134
map_pair_exclude_types,
3235
)
36+
from deepmd.utils.path import (
37+
DPPath,
38+
)
3339

3440
from .make_base_atomic_model import (
3541
make_base_atomic_model,
@@ -246,6 +252,196 @@ def call(
246252
aparam=aparam,
247253
)
248254

255+
def get_intensive(self) -> bool:
256+
"""Whether the fitting property is intensive."""
257+
return False
258+
259+
def get_compute_stats_distinguish_types(self) -> bool:
260+
"""Get whether the fitting net computes stats which are not distinguished between different types of atoms."""
261+
return True
262+
263+
def compute_or_load_out_stat(
264+
self,
265+
merged: Callable[[], list[dict]] | list[dict],
266+
stat_file_path: DPPath | None = None,
267+
) -> None:
268+
"""
269+
Compute the output statistics (e.g. energy bias) for the fitting net from packed data.
270+
271+
Parameters
272+
----------
273+
merged : Union[Callable[[], list[dict]], list[dict]]
274+
- list[dict]: A list of data samples from various data systems.
275+
Each element, `merged[i]`, is a data dictionary containing `keys`: `np.ndarray`
276+
originating from the `i`-th data system.
277+
- Callable[[], list[dict]]: A lazy function that returns data samples in the above format
278+
only when needed. Since the sampling process can be slow and memory-intensive,
279+
the lazy function helps by only sampling once.
280+
stat_file_path : Optional[DPPath]
281+
The path to the stat file.
282+
283+
"""
284+
self.change_out_bias(
285+
merged,
286+
stat_file_path=stat_file_path,
287+
bias_adjust_mode="set-by-statistic",
288+
)
289+
290+
def change_out_bias(
291+
self,
292+
sample_merged: Callable[[], list[dict]] | list[dict],
293+
stat_file_path: DPPath | None = None,
294+
bias_adjust_mode: str = "change-by-statistic",
295+
) -> None:
296+
"""Change the output bias according to the input data and the pretrained model.
297+
298+
Parameters
299+
----------
300+
sample_merged : Union[Callable[[], list[dict]], list[dict]]
301+
- list[dict]: A list of data samples from various data systems.
302+
Each element, `merged[i]`, is a data dictionary containing `keys`: `np.ndarray`
303+
originating from the `i`-th data system.
304+
- Callable[[], list[dict]]: A lazy function that returns data samples in the above format
305+
only when needed. Since the sampling process can be slow and memory-intensive,
306+
the lazy function helps by only sampling once.
307+
bias_adjust_mode : str
308+
The mode for changing output bias : ['change-by-statistic', 'set-by-statistic']
309+
'change-by-statistic' : perform predictions on labels of target dataset,
310+
and do least square on the errors to obtain the target shift as bias.
311+
'set-by-statistic' : directly use the statistic output bias in the target dataset.
312+
stat_file_path : Optional[DPPath]
313+
The path to the stat file.
314+
"""
315+
from deepmd.dpmodel.utils.stat import (
316+
compute_output_stats,
317+
)
318+
319+
if bias_adjust_mode == "change-by-statistic":
320+
delta_bias, out_std = compute_output_stats(
321+
sample_merged,
322+
self.get_ntypes(),
323+
keys=list(self.atomic_output_def().keys()),
324+
stat_file_path=stat_file_path,
325+
model_forward=self._get_forward_wrapper_func(),
326+
rcond=self.rcond,
327+
preset_bias=self.preset_out_bias,
328+
stats_distinguish_types=self.get_compute_stats_distinguish_types(),
329+
intensive=self.get_intensive(),
330+
)
331+
self._store_out_stat(delta_bias, out_std, add=True)
332+
elif bias_adjust_mode == "set-by-statistic":
333+
bias_out, std_out = compute_output_stats(
334+
sample_merged,
335+
self.get_ntypes(),
336+
keys=list(self.atomic_output_def().keys()),
337+
stat_file_path=stat_file_path,
338+
rcond=self.rcond,
339+
preset_bias=self.preset_out_bias,
340+
stats_distinguish_types=self.get_compute_stats_distinguish_types(),
341+
intensive=self.get_intensive(),
342+
)
343+
self._store_out_stat(bias_out, std_out)
344+
else:
345+
raise RuntimeError("Unknown bias_adjust_mode mode: " + bias_adjust_mode)
346+
347+
def _store_out_stat(
348+
self,
349+
out_bias: dict[str, np.ndarray],
350+
out_std: dict[str, np.ndarray],
351+
add: bool = False,
352+
) -> None:
353+
"""Store output bias and std into the model."""
354+
ntypes = self.get_ntypes()
355+
out_bias_data = np.copy(self.out_bias)
356+
out_std_data = np.copy(self.out_std)
357+
for kk in out_bias.keys():
358+
assert kk in out_std.keys()
359+
idx = self._get_bias_index(kk)
360+
size = self._varsize(self.atomic_output_def()[kk].shape)
361+
if not add:
362+
out_bias_data[idx, :, :size] = out_bias[kk].reshape(ntypes, size)
363+
else:
364+
out_bias_data[idx, :, :size] += out_bias[kk].reshape(ntypes, size)
365+
out_std_data[idx, :, :size] = out_std[kk].reshape(ntypes, size)
366+
self.out_bias = out_bias_data
367+
self.out_std = out_std_data
368+
369+
def _get_forward_wrapper_func(self) -> Callable[..., dict[str, np.ndarray]]:
370+
"""Get a forward wrapper of the atomic model for output bias calculation."""
371+
import array_api_compat
372+
373+
from deepmd.dpmodel.utils.nlist import (
374+
extend_input_and_build_neighbor_list,
375+
)
376+
377+
def model_forward(
378+
coord: np.ndarray,
379+
atype: np.ndarray,
380+
box: np.ndarray | None,
381+
fparam: np.ndarray | None = None,
382+
aparam: np.ndarray | None = None,
383+
) -> dict[str, np.ndarray]:
384+
# Get reference array to determine the target array type and device
385+
# Use out_bias as reference since it's always present
386+
ref_array = self.out_bias
387+
xp = array_api_compat.array_namespace(ref_array)
388+
389+
# Convert numpy inputs to the model's array type with correct device
390+
device = getattr(ref_array, "device", None)
391+
if device is not None:
392+
# For torch tensors
393+
coord = xp.asarray(coord, device=device)
394+
atype = xp.asarray(atype, device=device)
395+
if box is not None:
396+
# Check if box is all zeros before converting
397+
if np.allclose(box, 0.0):
398+
box = None
399+
else:
400+
box = xp.asarray(box, device=device)
401+
if fparam is not None:
402+
fparam = xp.asarray(fparam, device=device)
403+
if aparam is not None:
404+
aparam = xp.asarray(aparam, device=device)
405+
else:
406+
# For numpy arrays
407+
coord = xp.asarray(coord)
408+
atype = xp.asarray(atype)
409+
if box is not None:
410+
if np.allclose(box, 0.0):
411+
box = None
412+
else:
413+
box = xp.asarray(box)
414+
if fparam is not None:
415+
fparam = xp.asarray(fparam)
416+
if aparam is not None:
417+
aparam = xp.asarray(aparam)
418+
419+
(
420+
extended_coord,
421+
extended_atype,
422+
mapping,
423+
nlist,
424+
) = extend_input_and_build_neighbor_list(
425+
coord,
426+
atype,
427+
self.get_rcut(),
428+
self.get_sel(),
429+
mixed_types=self.mixed_types(),
430+
box=box,
431+
)
432+
atomic_ret = self.forward_common_atomic(
433+
extended_coord,
434+
extended_atype,
435+
nlist,
436+
mapping=mapping,
437+
fparam=fparam,
438+
aparam=aparam,
439+
)
440+
# Convert outputs back to numpy arrays
441+
return {kk: to_numpy_array(vv) for kk, vv in atomic_ret.items()}
442+
443+
return model_forward
444+
249445
def serialize(self) -> dict:
250446
return {
251447
"type_map": self.type_map,

deepmd/dpmodel/common.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ def to_numpy_array(x: Optional["Array"]) -> np.ndarray | None:
121121
try:
122122
# asarray is not within Array API standard, so may fail
123123
return np.asarray(x)
124-
except (ValueError, AttributeError, TypeError):
124+
except (ValueError, AttributeError, TypeError, RuntimeError):
125+
# RuntimeError: handles torch tensors with requires_grad=True
125126
xp = array_api_compat.array_namespace(x)
126127
# to fix BufferError: Cannot export readonly array since signalling readonly is unsupported by DLPack.
127128
# Move to CPU device to ensure numpy compatibility
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# SPDX-License-Identifier: LGPL-3.0-or-later
2+
from .dp_atomic_model import (
3+
DPAtomicModel,
4+
)
5+
from .energy_atomic_model import (
6+
DPEnergyAtomicModel,
7+
)
8+
9+
__all__ = [
10+
"DPAtomicModel",
11+
"DPEnergyAtomicModel",
12+
]
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# SPDX-License-Identifier: LGPL-3.0-or-later
2+
from typing import (
3+
Any,
4+
)
5+
6+
import torch
7+
8+
from deepmd.dpmodel.atomic_model.dp_atomic_model import DPAtomicModel as DPAtomicModelDP
9+
from deepmd.pt_expt.common import (
10+
dpmodel_setattr,
11+
register_dpmodel_mapping,
12+
)
13+
14+
15+
class DPAtomicModel(DPAtomicModelDP, torch.nn.Module):
16+
# Import at class level to set base classes for deserialization
17+
# These will be used by the dpmodel deserialize method to create pt_expt instances
18+
from deepmd.pt_expt.descriptor.base_descriptor import (
19+
BaseDescriptor,
20+
)
21+
from deepmd.pt_expt.fitting.base_fitting import (
22+
BaseFitting,
23+
)
24+
25+
base_descriptor_cls = BaseDescriptor
26+
base_fitting_cls = BaseFitting
27+
28+
def __init__(
29+
self, descriptor: Any, fitting: Any, *args: Any, **kwargs: Any
30+
) -> None:
31+
torch.nn.Module.__init__(self)
32+
# Convert descriptor and fitting to pt_expt versions if they are dpmodel instances
33+
# The dpmodel_setattr mechanism will handle this automatically via registry
34+
from deepmd.pt_expt.common import (
35+
try_convert_module,
36+
)
37+
38+
descriptor_pt = try_convert_module(descriptor)
39+
fitting_pt = try_convert_module(fitting)
40+
# If conversion failed (not registered), use original (assume already pt_expt)
41+
if descriptor_pt is None:
42+
descriptor_pt = descriptor
43+
if fitting_pt is None:
44+
fitting_pt = fitting
45+
DPAtomicModelDP.__init__(self, descriptor_pt, fitting_pt, *args, **kwargs)
46+
47+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
48+
# Ensure torch.nn.Module.__call__ drives forward() for export/tracing.
49+
return torch.nn.Module.__call__(self, *args, **kwargs)
50+
51+
def __setattr__(self, name: str, value: Any) -> None:
52+
handled, value = dpmodel_setattr(self, name, value)
53+
if not handled:
54+
super().__setattr__(name, value)
55+
56+
def forward(
57+
self,
58+
extended_coord: torch.Tensor,
59+
extended_atype: torch.Tensor,
60+
nlist: torch.Tensor,
61+
mapping: torch.Tensor | None = None,
62+
fparam: torch.Tensor | None = None,
63+
aparam: torch.Tensor | None = None,
64+
) -> dict[str, torch.Tensor]:
65+
return self.forward_atomic(
66+
extended_coord,
67+
extended_atype,
68+
nlist,
69+
mapping=mapping,
70+
fparam=fparam,
71+
aparam=aparam,
72+
)
73+
74+
75+
register_dpmodel_mapping(
76+
DPAtomicModelDP,
77+
lambda v: DPAtomicModel.deserialize(v.serialize()),
78+
)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# SPDX-License-Identifier: LGPL-3.0-or-later
2+
from deepmd.dpmodel.atomic_model.energy_atomic_model import (
3+
DPEnergyAtomicModel as DPEnergyAtomicModelDP,
4+
)
5+
from deepmd.pt_expt.common import (
6+
register_dpmodel_mapping,
7+
)
8+
9+
from .dp_atomic_model import (
10+
DPAtomicModel,
11+
)
12+
13+
14+
class DPEnergyAtomicModel(DPAtomicModel):
15+
"""Energy atomic model for pt_expt backend.
16+
17+
This is a thin wrapper around DPAtomicModel that validates
18+
the fitting is an EnergyFittingNet or InvarFitting.
19+
"""
20+
21+
pass
22+
23+
24+
register_dpmodel_mapping(
25+
DPEnergyAtomicModelDP,
26+
lambda v: DPEnergyAtomicModel.deserialize(v.serialize()),
27+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# SPDX-License-Identifier: LGPL-3.0-or-later

0 commit comments

Comments
 (0)