Skip to content

Commit 56fabc9

Browse files
committed
feat(pt): add charge density prediction support
Add a grid-based charge density predi ction task for the PyTorch backend: - add DensityFittingNet, DPDensityAtomicModel and GridDensityModel (fitting type "density", model type "grid_density") - add GridDensityLoss ("grid_density") for grid density training - support loading grid.npy/density.npy in the data system - support DeepEval/DeepPot inference with grid= input, returning density - add QM9 charge density training example under examples/density/
1 parent 8cfd46e commit 56fabc9

29 files changed

Lines changed: 1728 additions & 0 deletions

File tree

deepmd/infer/deep_pot.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,11 @@ def eval(
212212
aparam=aparam,
213213
**kwargs,
214214
)
215+
# TODO: if the grid is requested, we can directly return it without reshaping to energy, force and virial. We can also consider to return the grid in a separate key in the results dict, instead of reshaping it to energy, force and virial.
216+
if "grid" in kwargs:
217+
result = results["density"].reshape(nframes, -1)
218+
return result
219+
215220
energy = results["energy_redu"].reshape(nframes, 1)
216221
force = results["energy_derv_r"].reshape(nframes, natoms, 3)
217222
virial = results["energy_derv_c_redu"].reshape(nframes, 9)

deepmd/pt/infer/deep_eval.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,17 @@ def eval(
552552
coords, atom_types, len(atom_types.shape) > 1
553553
)
554554
request_defs = self._get_request_defs(atomic)
555+
if "grid" in kwargs and kwargs["grid"] is not None:
556+
out = self._eval_func(self._eval_model_density, numb_test, natoms)(
557+
coords,
558+
cells,
559+
atom_types,
560+
np.array(kwargs["grid"]),
561+
fparam,
562+
aparam,
563+
request_defs,
564+
)
565+
return {"density": out}
555566
if "spin" not in kwargs or kwargs["spin"] is None:
556567
out = self._eval_func(self._eval_model, numb_test, natoms)(
557568
coords, cells, atom_types, fparam, aparam, request_defs, charge_spin
@@ -916,6 +927,80 @@ def _eval_model_spin(
916927
) # this is kinda hacky
917928
return tuple(results)
918929

930+
def _eval_model_density(
931+
self,
932+
coords: np.ndarray,
933+
cells: np.ndarray | None,
934+
atom_types: np.ndarray,
935+
grid: np.ndarray,
936+
fparam: np.ndarray | None,
937+
aparam: np.ndarray | None,
938+
request_defs: list[OutputVariableDef],
939+
) -> tuple[np.ndarray, ...]:
940+
model = self.dp.to(DEVICE)
941+
942+
nframes = coords.shape[0]
943+
if len(atom_types.shape) == 1:
944+
natoms = len(atom_types)
945+
atom_types = np.tile(atom_types, nframes).reshape(nframes, -1)
946+
else:
947+
natoms = len(atom_types[0])
948+
949+
coord_input = torch.tensor(
950+
coords.reshape([nframes, natoms, 3]),
951+
dtype=GLOBAL_PT_FLOAT_PRECISION,
952+
device=DEVICE,
953+
)
954+
type_input = torch.tensor(atom_types, dtype=torch.long, device=DEVICE)
955+
grid_input = torch.tensor(
956+
grid.reshape([nframes, -1, 3]),
957+
dtype=GLOBAL_PT_FLOAT_PRECISION,
958+
device=DEVICE,
959+
)
960+
ngrid = grid_input.shape[1]
961+
if cells is not None:
962+
box_input = torch.tensor(
963+
cells.reshape([nframes, 3, 3]),
964+
dtype=GLOBAL_PT_FLOAT_PRECISION,
965+
device=DEVICE,
966+
)
967+
else:
968+
box_input = None
969+
if fparam is not None:
970+
fparam_input = to_torch_tensor(
971+
fparam.reshape(nframes, self.get_dim_fparam())
972+
)
973+
else:
974+
fparam_input = None
975+
if aparam is not None:
976+
aparam_input = to_torch_tensor(
977+
aparam.reshape(nframes, natoms, self.get_dim_aparam())
978+
)
979+
else:
980+
aparam_input = None
981+
982+
do_atomic_virial = any(
983+
x.category == OutputVariableCategory.DERV_C_REDU for x in request_defs
984+
)
985+
batch_output = model(
986+
coord_input,
987+
type_input,
988+
grid=grid_input,
989+
box=box_input,
990+
do_atomic_virial=do_atomic_virial,
991+
fparam=fparam_input,
992+
aparam=aparam_input,
993+
)
994+
if isinstance(batch_output, tuple):
995+
batch_output = batch_output[0]
996+
997+
results = []
998+
pt_name = "density"
999+
density_shape = [nframes, ngrid]
1000+
out = batch_output[pt_name].reshape(density_shape).detach().cpu().numpy()
1001+
results.append(out)
1002+
return tuple(results)
1003+
9191004
def _get_output_shape(
9201005
self, odef: OutputVariableDef, nframes: int, natoms: int
9211006
) -> list[int]:

deepmd/pt/loss/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
# SPDX-License-Identifier: LGPL-3.0-or-later
2+
from .charge import (
3+
GridDensityLoss,
4+
)
25
from .denoise import (
36
DenoiseLoss,
47
)
@@ -35,6 +38,7 @@
3538
"EnergyHessianStdLoss",
3639
"EnergySpinLoss",
3740
"EnergyStdLoss",
41+
"GridDensityLoss",
3842
"PopulationLoss",
3943
"PropertyLoss",
4044
"TaskLoss",

deepmd/pt/loss/charge.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# SPDX-License-Identifier: LGPL-3.0-or-later
2+
from typing import (
3+
Any,
4+
)
5+
6+
import torch
7+
8+
from deepmd.pt.loss.loss import (
9+
TaskLoss,
10+
)
11+
from deepmd.pt.utils import (
12+
env,
13+
)
14+
from deepmd.pt.utils.env import (
15+
GLOBAL_PT_FLOAT_PRECISION,
16+
)
17+
from deepmd.utils.data import (
18+
DataRequirementItem,
19+
)
20+
21+
22+
class GridDensityLoss(TaskLoss):
23+
def __init__(
24+
self,
25+
starter_learning_rate: float = 1.0,
26+
start_pref_d: float = 0.0,
27+
limit_pref_d: float = 0.0,
28+
inference: bool = False,
29+
**kwargs: Any,
30+
) -> None:
31+
r"""Construct a layer to compute loss on grid density.
32+
33+
Parameters
34+
----------
35+
starter_learning_rate : float
36+
The learning rate at the start of the training.
37+
start_pref_d : float
38+
The prefactor of charge density loss at the start of the training.
39+
limit_pref_d : float
40+
The prefactor of charge density loss at the end of the training.
41+
inference : bool
42+
If true, it will output all losses found in output, ignoring the pre-factors.
43+
**kwargs
44+
Other keyword arguments.
45+
"""
46+
super().__init__()
47+
self.starter_learning_rate = starter_learning_rate
48+
self.has_d = (start_pref_d != 0.0 and limit_pref_d != 0.0) or inference
49+
50+
self.start_pref_d = start_pref_d
51+
self.limit_pref_d = limit_pref_d
52+
self.inference = inference
53+
54+
def forward(
55+
self,
56+
input_dict: dict[str, torch.Tensor],
57+
model: torch.nn.Module,
58+
label: dict[str, torch.Tensor],
59+
natoms: int,
60+
learning_rate: float,
61+
mae: bool = False,
62+
) -> tuple[dict[str, torch.Tensor], torch.Tensor, dict[str, torch.Tensor]]:
63+
"""Return loss on energy and force.
64+
65+
Parameters
66+
----------
67+
input_dict : dict[str, torch.Tensor]
68+
Model inputs.
69+
model : torch.nn.Module
70+
Model to be used to output the predictions.
71+
label : dict[str, torch.Tensor]
72+
Labels.
73+
natoms : int
74+
The local atom number.
75+
76+
Returns
77+
-------
78+
model_pred: dict[str, torch.Tensor]
79+
Model predictions.
80+
loss: torch.Tensor
81+
Loss for model to minimize.
82+
more_loss: dict[str, torch.Tensor]
83+
Other losses for display.
84+
"""
85+
model_pred = model(**input_dict)
86+
coef = learning_rate / self.starter_learning_rate
87+
pref_d = self.limit_pref_d + (self.start_pref_d - self.limit_pref_d) * coef
88+
89+
loss = torch.zeros(1, dtype=env.GLOBAL_PT_FLOAT_PRECISION, device=env.DEVICE)[0]
90+
more_loss = {}
91+
# more_loss['log_keys'] = [] # showed when validation on the fly
92+
# more_loss['test_keys'] = [] # showed when doing dp test
93+
atom_norm = 1.0 / natoms
94+
if self.has_d and "density" in model_pred and "density" in label:
95+
density_pred = model_pred["density"]
96+
density_label = label["density"]
97+
find_density = label.get("find_density", 0.0)
98+
pref_d = pref_d * find_density
99+
density_pred_reshape = density_pred.reshape(-1)
100+
density_label_reshape = density_label.reshape(-1)
101+
l2_density_loss = torch.square(
102+
density_label_reshape - density_pred_reshape
103+
).mean()
104+
rmse_d = l2_density_loss.sqrt()
105+
more_loss["rmse_d"] = self.display_if_exist(rmse_d.detach(), find_density)
106+
l1_density_loss = torch.abs(
107+
density_label_reshape - density_pred_reshape
108+
).mean()
109+
loss += (pref_d * l1_density_loss).to(GLOBAL_PT_FLOAT_PRECISION)
110+
mae_d = l1_density_loss
111+
more_loss["mae_d"] = self.display_if_exist(mae_d.detach(), find_density)
112+
return model_pred, loss, more_loss
113+
114+
@property
115+
def label_requirement(self) -> list[DataRequirementItem]:
116+
"""Return data label requirements needed for this loss calculation."""
117+
label_requirement = []
118+
label_requirement.append(
119+
DataRequirementItem(
120+
"grid",
121+
ndof=3,
122+
atomic=True, # the grid is defined for each atom, so it is atomic
123+
must=True,
124+
high_prec=True,
125+
)
126+
)
127+
if self.has_d:
128+
label_requirement.append(
129+
DataRequirementItem(
130+
"density",
131+
ndof=1,
132+
atomic=True,
133+
must=False,
134+
high_prec=True,
135+
)
136+
)
137+
return label_requirement

deepmd/pt/model/atomic_model/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
from .base_atomic_model import (
1818
BaseAtomicModel,
1919
)
20+
from .density_atomic_model import (
21+
DPDensityAtomicModel,
22+
)
2023
from .dipole_atomic_model import (
2124
DPDipoleAtomicModel,
2225
)
@@ -53,6 +56,7 @@
5356
"BaseAtomicModel",
5457
"DPAtomicModel",
5558
"DPDOSAtomicModel",
59+
"DPDensityAtomicModel",
5660
"DPDipoleAtomicModel",
5761
"DPEnergyAtomicModel",
5862
"DPPolarAtomicModel",

0 commit comments

Comments
 (0)