Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9ad31f7
feat(pt): add charge density prediction support
YuzhiLiu-ai Aug 24, 2026
ba7ce74
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 25, 2026
1172f9b
Merge branch 'master' into density-for-pr
YuzhiLiu-ai Sep 8, 2026
a2c294e
ci: rerun checks
YuzhiLiu-ai Sep 10, 2026
f6a514f
fix(infer): remove type-incoherent grid branch from DeepPot.eval
YuzhiLiu-ai Sep 11, 2026
4ddc097
fix(pt): harden density model evaluation and inputs
YuzhiLiu-ai Sep 13, 2026
83a753c
refactor(pt): derive density model from make_model and assert shuffle…
YuzhiLiu-ai Sep 13, 2026
e894ce8
ci: rerun checks
YuzhiLiu-ai Sep 13, 2026
d01f459
fix(pt): address review on density model interfaces, loss, data, and …
YuzhiLiu-ai Sep 14, 2026
3aef44c
docs: add density fitting guide and align argcheck and example configs
YuzhiLiu-ai Sep 14, 2026
9d98f65
fix(pt): address review on density signatures, statistics cache, and …
YuzhiLiu-ai Sep 16, 2026
dec47b2
Merge remote-tracking branch 'upstream/master' into density-for-pr
YuzhiLiu-ai Sep 17, 2026
aa566fb
fix(pt): address review round on density dispatch, loss, exclusions, …
YuzhiLiu-ai Sep 18, 2026
b75d383
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 18, 2026
ff7fc05
Merge branch 'master' into density-for-pr
YuzhiLiu-ai Sep 20, 2026
6145ae1
fix(pt): close review round on density eval, statistics, and loader c…
YuzhiLiu-ai Sep 23, 2026
284f62e
Merge remote-tracking branch 'density/density-for-pr' into density-fo…
YuzhiLiu-ai Sep 23, 2026
e23e953
test: avoid subclass __init__ bypass flagged by CodeQL in data test d…
YuzhiLiu-ai Sep 23, 2026
bbbcec8
test: avoid subclass __init__ bypass flagged by CodeQL in data test d…
YuzhiLiu-ai Sep 23, 2026
4f83b59
fix(data): require exact ndof for 3D frame_major arrays
YuzhiLiu-ai Sep 23, 2026
3e7c259
fix(pt): align density data layout, forward charge_spin, and loader c…
YuzhiLiu-ai Oct 3, 2026
7a4c466
fix(data): guard frame-major extents across sets and align the densit…
YuzhiLiu-ai Oct 3, 2026
99c2b81
Merge remote-tracking branch 'upstream/master' into density-for-pr
YuzhiLiu-ai Oct 3, 2026
094a19f
fix(pt): address density model review on serialization, charge_spin, …
YuzhiLiu-ai Oct 13, 2026
26c0eed
Merge remote-tracking branch 'upstream/master' into density-for-pr
YuzhiLiu-ai Oct 13, 2026
5cf0090
Merge remote-tracking branch 'refs/remotes/upstream/master' into dens…
YuzhiLiu-ai Oct 15, 2026
873b182
Merge remote-tracking branch 'upstream/master' into density-for-pr
Oct 19, 2026
e9fb537
fix(pt): address density model review feedback
Oct 22, 2026
7bf3e6d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 22, 2026
9449d6e
chore(docs): retrigger Read the Docs build
Oct 22, 2026
94e0a1b
test(pt): avoid ambiguous parent stat call
Oct 22, 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
167 changes: 167 additions & 0 deletions deepmd/infer/deep_density.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
from typing import (
Any,
)

import numpy as np

from deepmd.dpmodel.output_def import (
FittingOutputDef,
ModelOutputDef,
OutputVariableDef,
)

from .deep_eval import (
DeepEval,
)


class DeepDensity(DeepEval):
"""Charge density evaluated on grid points.

Parameters
----------
model_file : Path
The name of the frozen model file.
*args : list
Positional arguments.
auto_batch_size : bool or int or AutoBatchSize, default: True
If True, automatic batch size will be used. If int, it will be used
as the initial batch size.
neighbor_list : ase.neighborlist.NewPrimitiveNeighborList, optional
The ASE neighbor list class to produce the neighbor list. If None, the
neighbor list will be built natively in the model.
**kwargs : dict
Keyword arguments.
"""

@property
def output_def(self) -> ModelOutputDef:
"""Get the output definition of this model.

The output definition is identical to that of the density
fitting net of the model: the density is predicted on grid
points, and is neither reducible nor differentiable.
"""
return ModelOutputDef(
FittingOutputDef(
[
OutputVariableDef(
"density",
[1],
reducible=False,
r_differentiable=False,
c_differentiable=False,
),
]
)
)

def eval(
self,
coords: np.ndarray,
cells: np.ndarray | None,
atom_types: list[int] | np.ndarray,
grid: np.ndarray,
fparam: np.ndarray | None = None,
aparam: np.ndarray | None = None,
mixed_type: bool = False,
grid_batch_size: int | None = 4096,
**kwargs: Any,
) -> np.ndarray:
"""Evaluate the density on grid points.

Parameters
----------
coords : np.ndarray
The coordinates of the atoms, in shape (nframes, natoms, 3).
cells : np.ndarray
The cell vectors of the system, in shape (nframes, 9). If the system
is not periodic, set it to None.
atom_types : list[int] or np.ndarray
The types of the atoms. If mixed_type is False, the shape is (natoms,);
otherwise, the shape is (nframes, natoms).
grid : np.ndarray
The coordinates of the grid points, in shape (nframes, ngrid, 3).
fparam : np.ndarray, optional
The frame parameters, by default None.
aparam : np.ndarray, optional
The atomic parameters, by default None.
mixed_type : bool, optional
Whether the atom_types is mixed type, by default False.
grid_batch_size : int or None, optional
Maximum number of grid points evaluated in one model call. Grid
chunks are concatenated in their original order. The default is
4096; set to None to evaluate the full grid in one call.
**kwargs : dict[str, Any]
Keyword arguments. ``charge_spin`` (nframes, 2) is forwarded to
models with charge spin embedding; when omitted, such models use
their ``default_chg_spin`` if set.

Returns
-------
density
The density on the grid points, in shape (nframes, ngrid).
"""
(
coords,
cells,
atom_types,
fparam,
aparam,
nframes,
_,
) = self._standard_input(coords, cells, atom_types, fparam, aparam, mixed_type)
Comment thread
YuzhiLiu-ai marked this conversation as resolved.
grid = self._standard_grid(grid, nframes)
if grid_batch_size is not None and grid_batch_size <= 0:
raise ValueError("grid_batch_size must be positive or None")
if grid_batch_size is None or grid.shape[1] <= grid_batch_size:
grid_slices = (grid,)
else:
grid_slices = tuple(
grid[:, begin : begin + grid_batch_size]
for begin in range(0, grid.shape[1], grid_batch_size)
)
density_chunks = []
for grid_chunk in grid_slices:
results = self.deep_eval.eval(
coords,
cells,
atom_types,
False,
fparam=fparam,
aparam=aparam,
grid=grid_chunk,
**kwargs,
)
density_chunks.append(results["density"].reshape(nframes, -1))
return np.concatenate(density_chunks, axis=1)

@staticmethod
def _standard_grid(grid: np.ndarray, nframes: int) -> np.ndarray:
"""Normalise the grid coordinates to ``(nframes, ngrid, 3)``.

The auto batcher slices every argument with ``ndim > 1`` along axis
0, so a natural single-frame ``(ngrid, 3)`` input would silently be
truncated to one grid point; carry the frame dimension explicitly.
"""
arr = np.asarray(grid)
if arr.ndim == 2:
Comment thread
YuzhiLiu-ai marked this conversation as resolved.
if nframes != 1:
raise ValueError(
f"grid of shape {arr.shape} is ambiguous for {nframes} "

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: the two validation branches in _standard_grid — this 'ambiguous for N frames' error (2D grid with nframes > 1) and the ndim != 3 / trailing-dim != 3 error below — have no test coverage. Only the (ngrid,3) happy path and the 3D frame-count-mismatch error are tested. These are new user-facing validation paths; please cover them.

"frames; pass grid with shape (nframes, ngrid, 3)"
)
arr = arr[None, ...]
if arr.ndim != 3 or arr.shape[-1] != 3:
raise ValueError(
f"grid must have shape (nframes, ngrid, 3), got {arr.shape}"
)
if arr.shape[0] != nframes:
raise ValueError(f"grid has {arr.shape[0]} frames but coord has {nframes}")
if arr.shape[1] == 0:
raise ValueError("grid must contain at least one point per frame")
return arr


__all__ = ["DeepDensity"]
1 change: 1 addition & 0 deletions deepmd/infer/deep_pot.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ def eval(
aparam=aparam,
**kwargs,
)

energy = results["energy_redu"].reshape(nframes, 1)
force = results["energy_derv_r"].reshape(nframes, natoms, 3)
virial = results["energy_derv_c_redu"].reshape(nframes, 9)
Expand Down
9 changes: 9 additions & 0 deletions deepmd/infer/model_test/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
Any,
)

from deepmd.infer.deep_density import (
DeepDensity,
)
from deepmd.infer.deep_dipole import (
DeepDipole,
)
Expand All @@ -36,6 +39,9 @@
save_txt_file,
test_chunk_atoms,
)
from deepmd.infer.model_test.density import (
DensityTester,
)
from deepmd.infer.model_test.dos import (
DosTester,
)
Expand All @@ -56,6 +62,7 @@

__all__ = [
"ChunkContext",
"DensityTester",
"DipoleTester",
"DosTester",
"EnerTester",
Expand Down Expand Up @@ -96,6 +103,8 @@ def build_tester(dp: Any, *, atomic: bool) -> ModelTester:
return tester(dp, atomic=atomic)
if isinstance(dp, DeepDOS):
return DosTester(dp, atomic=atomic)
if isinstance(dp, DeepDensity):
return DensityTester(dp, atomic=atomic)
if isinstance(dp, DeepProperty):
return PropertyTester(dp, atomic=atomic)
if isinstance(dp, DeepGlobalPolar):
Expand Down
123 changes: 123 additions & 0 deletions deepmd/infer/model_test/density.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
"""Testing of models predicting charge density on grid points."""

from deepmd.infer.model_test.base import (
ChunkContext,
ModelTester,
_write_per_frame_details,
)
from deepmd.utils.data import (
DeepmdData,
)
from deepmd.utils.eval_metrics import (
mae,
rmse,
)

__all__ = ["DensityTester"]


class DensityTester(ModelTester):
"""Test a model of charge density on grid points."""

report = (
("mae_density", "DENSITY MAE : {} units"),
("rmse_density", "DENSITY RMSE : {} units"),
)

def add_data_requirements(self, data: DeepmdData) -> None:
"""Declare the labels a density test consumes."""
dp = self.dp
# The grid and the density are defined on grid points rather than on
# atoms, and their extent (ngrid) is not known until the data is
# loaded. They are declared frame-major so the loader keeps the
# (nframes, ngrid, ndof) layout without reshaping to natoms.
data.add(
"grid",
3,
atomic=False,
must=True,
high_prec=True,
special_shape="frame_major",
)
data.add(
"density",
1,
atomic=False,
must=True,
high_prec=True,
special_shape="frame_major",
)
if dp.get_dim_fparam() > 0:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: DensityFittingNet accepts numb_fparam > 0, so this branch (and the forwarding at line 85) is reachable, but no density test configures fparam — the True side of this conditional never runs. Please add a density dp test case with fparam set.

data.add(
"fparam", dp.get_dim_fparam(), atomic=False, must=True, high_prec=False
)
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,
)

def evaluate_chunk(
Comment thread
YuzhiLiu-ai marked this conversation as resolved.
self,
data: DeepmdData,
test_data: dict,
context: ChunkContext,
) -> dict[str, tuple[float, float]]:
"""Evaluate one chunk of a density test."""
dp = self.dp
mixed_type = data.mixed_type
nframes = test_data["box"].shape[0]

coord = test_data["coord"].reshape([nframes, -1])
box = test_data["box"] if data.pbc else None
if mixed_type:
atype = test_data["type"].reshape([nframes, -1])
else:
atype = test_data["type"][0]
fparam = test_data["fparam"] if dp.get_dim_fparam() > 0 else None
aparam = test_data["aparam"] if dp.get_dim_aparam() > 0 else None
charge_spin = (
test_data["charge_spin"]
if dp.has_chg_spin_ebd() and test_data.get("find_charge_spin", 0.0) != 0.0
else None
)
# the loader accepts both (nframes, ngrid, 3) and the flattened
# (nframes, ngrid*3) layout; DeepDensity.eval only accepts the
# former, so normalize here and keep one contract at the eval call
grid = test_data["grid"].reshape(nframes, -1, 3)

prediction = dp.eval(
coord,
box,
atype,
grid,
fparam=fparam,
aparam=aparam,
charge_spin=charge_spin,
mixed_type=mixed_type,
).reshape(nframes, -1)
label = test_data["density"].reshape(nframes, -1)

diff = prediction - label
errors: dict[str, tuple[float, float]] = {
"mae_density": (mae(diff), diff.size),
"rmse_density": (rmse(diff), diff.size),
}

if context.detail_path is not None:
Comment thread
YuzhiLiu-ai marked this conversation as resolved.
_write_per_frame_details(
context,
suffix="density",
reference=label,
prediction=prediction,
)

return errors
1 change: 1 addition & 0 deletions deepmd/pd/utils/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,5 @@ def add_data_requirement(self, data_requirement: list[DataRequirementItem]) -> N
default=data_item["default"],
dtype=data_item["dtype"],
output_natoms_for_type_sel=data_item["output_natoms_for_type_sel"],
special_shape=getattr(data_item, "special_shape", None),
)
Loading
Loading