Skip to content

Commit e26c742

Browse files
committed
Refactor SOGKSpace and SOG spline definitions for improved clarity and organization
- Updated copyright and author information in sog.h - Reorganized member variables in SOGKSpace for better readability - Introduced new parameters for SOG kernel and spline/grid method selection - Enhanced method definitions and added new methods for kernel parameter finalization - Refactored spline weight functions and node definitions in sog_spline.h - Improved naming conventions for clarity and consistency
1 parent e3dbad5 commit e26c742

11 files changed

Lines changed: 1314 additions & 1488 deletions

File tree

deepmd/pt/model/atomic_model/sog_atomic_model.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,16 @@ def __init__(
5151

5252
self.descriptor = descriptor
5353
self.fitting_net = sog_energy_fitting
54-
# self.sog_energy_fitting = self.fitting_net
5554
self.type_map = type_map
5655
self.ntypes = len(type_map)
5756
self.rcut = self.descriptor.get_rcut()
5857
self.sel = self.descriptor.get_sel()
5958

59+
# Apply SOG library defaults: if sigma was not explicitly set by user,
60+
# compute it from the descriptor's r_cut (sigma = r_cut * nlayers / RCUT_TO_SIGMA).
61+
nlayers_sog = getattr(self.descriptor, "nlayers", 1)
62+
self.fitting_net.recompute_from_rcut(self.rcut, nlayers_sog)
63+
6064
super().init_out_stat()
6165

6266
self.enable_eval_descriptor_hook = False

deepmd/pt/model/model/sog_model.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,14 @@ def _build_sog_lib_direct_kernel(
114114
)
115115

116116
nlayers = getattr(self.atomic_model.descriptor, "nlayers", 1) if hasattr(self.atomic_model, "descriptor") else 1
117-
# Build sog_arguments dict — prefer cubes2_phi_max, fall back to n_dl
117+
# Auto-detect FFT vs direct:
118+
# Explicit n_dl without cubes2_phi_max → direct k-sum (training/validation).
119+
_has_n_dl = getattr(fitting, "n_dl", None) is not None
120+
_has_phi = getattr(fitting, "cubes2_phi_max", None) is not None
121+
_use_fft = bool(getattr(fitting, "use_cubes2_fft", False))
122+
if _has_n_dl and not _has_phi:
123+
_use_fft = False
124+
# Build sog_arguments dict
118125
sog_args: dict = {
119126
"use_atomwise": False,
120127
"amp": amp_internal_runtime,
@@ -124,17 +131,21 @@ def _build_sog_lib_direct_kernel(
124131
"remove_self_interaction": bool(fitting.remove_self_interaction),
125132
"nufft": False,
126133
"use_nufft": False,
127-
"use_cubes2_fft": True,
134+
"use_cubes2_fft": _use_fft,
128135
"nlayers": nlayers,
129136
"norm_factor": E2_PER_ANGSTROM_TO_EV,
130137
"trainable_kernel": False,
138+
"b": float(fitting.b),
131139
}
132140
# Prefer cubes2_phi_max (new API), fall back to n_dl (legacy)
133141
if getattr(fitting, "cubes2_phi_max", None) is not None:
134142
sog_args["cubes2_phi_max"] = float(fitting.cubes2_phi_max)
135143
elif getattr(fitting, "n_dl", None) is not None:
136144
sog_args["n_dl"] = float(fitting.n_dl)
137145
# else: auto-default from SOG lib's Table III
146+
# Optional charge neutrality penalty (None = disabled, use physical k=0 instead)
147+
if getattr(fitting, "charge_neutral_lambda", None) is not None:
148+
sog_args["charge_neutral_lambda"] = float(fitting.charge_neutral_lambda)
138149

139150
kernel = sog_lib.Sog(
140151
sog_arguments=sog_args,

deepmd/pt/model/task/sog_energy_fitting.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@
3131
LRFittingNet,
3232
)
3333

34-
SOG_DEFAULT_B = to_numpy_array(np.array(1.62976708826776469))
34+
SOG_DEFAULT_B = to_numpy_array(np.array(2.0))
3535
SOG_DEFAULT_SIGMA = to_numpy_array(np.array(2.180230445405648))
3636
SOG_DEFAULT_M = int(12)
37+
# r_cut / sigma = RCUT_TO_SIGMA for C¹ continuity at the cutoff.
38+
# Equivalent to the parameter in sog.module.gaussian.
39+
RCUT_TO_SIGMA = 1.9892536839080267
3740

3841

3942
@LRFittingNet.register("sog_energy")
@@ -111,6 +114,10 @@ class SOGEnergyFittingNet(LRFittingNet):
111114
external_kspace : bool
112115
If True, long-range correction is handled externally (e.g. kspace),
113116
and the model only provides latent charges.
117+
use_cubes2_fft : bool
118+
If True, use CubeS₂ + FFT for long-range computation (fast, default).
119+
If False, use direct k-space summation (exact but slower for large systems).
120+
The grid resolution is controlled by n_dl (k-space cutoff) when FFT is off.
114121
"""
115122

116123
def __init__(
@@ -147,7 +154,9 @@ def __init__(
147154
n_dl: float | int | None = None,
148155
cubes2_phi_max: float | None = None,
149156
remove_self_interaction: bool = False,
157+
charge_neutral_lambda: float | None = None,
150158
external_kspace: bool = False,
159+
use_cubes2_fft: bool = False,
151160
**kwargs: Any,
152161
) -> None:
153162
super().__init__(
@@ -185,8 +194,11 @@ def __init__(
185194
if b_value <= 0.0:
186195
raise ValueError("`b` should be positive.")
187196

197+
self._sigma_user_set = sigma is not None
198+
self.use_cubes2_fft = bool(use_cubes2_fft)
199+
self.charge_neutral_lambda = charge_neutral_lambda
188200
if sigma is None:
189-
sigma_value = SOG_DEFAULT_SIGMA # will be overridden by sog lib via rcut
201+
sigma_value = float(SOG_DEFAULT_SIGMA) # placeholder, may be recomputed via rcut
190202
else:
191203
sigma_tensor = torch.as_tensor(sigma, dtype=dtype, device=device)
192204
sigma_value = float(sigma_tensor.reshape(-1)[0].item())
@@ -297,6 +309,9 @@ def serialize(self) -> dict:
297309
data["n_dl"] = self.n_dl # legacy
298310
data["remove_self_interaction"] = bool(self.remove_self_interaction)
299311
data["external_kspace"] = bool(self.external_kspace)
312+
data["use_cubes2_fft"] = bool(self.use_cubes2_fft)
313+
if self.charge_neutral_lambda is not None:
314+
data["charge_neutral_lambda"] = self.charge_neutral_lambda
300315
return data
301316

302317
@classmethod
@@ -311,6 +326,8 @@ def deserialize(cls, data: dict) -> "SOGEnergyFittingNet":
311326

312327
obj = super().deserialize(data)
313328

329+
obj.charge_neutral_lambda = data.get("charge_neutral_lambda", None)
330+
314331
with torch.no_grad():
315332
if bandwidth_tensor is not None:
316333
bw = bandwidth_tensor.to(
@@ -364,6 +381,39 @@ def deserialize(cls, data: dict) -> "SOGEnergyFittingNet":
364381
def _kernel_params(self) -> tuple[torch.Tensor, torch.Tensor]:
365382
return self.amp, self.bandwidth
366383

384+
def recompute_from_rcut(self, rcut: float, nlayers: int = 1) -> None:
385+
"""Recompute sigma, amp, bandwidth from the descriptor's r_cut.
386+
387+
This implements the SOG library's default sigma formula:
388+
sigma = r_cut * nlayers / RCUT_TO_SIGMA
389+
390+
Only recomputes when sigma was NOT explicitly set by the user,
391+
so explicit sigma in the config is always respected.
392+
"""
393+
if self._sigma_user_set:
394+
return # user explicitly set sigma — don't override
395+
396+
new_sigma = rcut * nlayers / RCUT_TO_SIGMA
397+
b_base = torch.tensor(self.b, dtype=self.amp.dtype, device=self.amp.device)
398+
bw_tensor = new_sigma * torch.pow(
399+
b_base,
400+
torch.arange(self.M, dtype=self.amp.dtype, device=self.amp.device),
401+
)
402+
new_bandwidth = bw_tensor.square()
403+
coef1 = float(4.0 * np.pi * np.log(self.b))
404+
new_amp = torch.full_like(new_bandwidth, coef1)
405+
new_amp *= new_bandwidth # convert to sog-lib internal amplitude
406+
407+
self.sigma = new_sigma
408+
self.amp = torch.nn.Parameter(
409+
new_amp.to(device=self.amp.device, dtype=self.amp.dtype),
410+
requires_grad=bool(self.trainable),
411+
)
412+
self.bandwidth = torch.nn.Parameter(
413+
new_bandwidth.to(device=self.bandwidth.device, dtype=self.bandwidth.dtype),
414+
requires_grad=bool(self.trainable),
415+
)
416+
367417
def forward(
368418
self,
369419
descriptor: torch.Tensor,

deepmd/utils/argcheck.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,6 +2284,25 @@ def fitting_sog_energy() -> list[Argument]:
22842284
+ "φ = Δ/r_c grid control for CubeS₂ FFT solver. "
22852285
+ "Auto-defaults from Predescu 2020 Table III when not set.",
22862286
),
2287+
Argument(
2288+
"n_dl",
2289+
[float, int, type(None)],
2290+
optional=True,
2291+
default=None,
2292+
doc=doc_only_pt_supported
2293+
+ "Legacy k-space grid density control. When set without cubes2_phi_max, "
2294+
+ "triggers direct k-space summation (no FFT). "
2295+
+ "Compute from ε via n_dl = 2π / sqrt(2·ln(1/ε) / bw[0]).",
2296+
),
2297+
Argument(
2298+
"use_cubes2_fft",
2299+
bool,
2300+
optional=True,
2301+
default=False,
2302+
doc=doc_only_pt_supported
2303+
+ "If True, use CubeS₂ + FFT solver. "
2304+
+ "If False (default), use direct k-space summation (fully autograd-compatible).",
2305+
),
22872306
Argument(
22882307
"remove_self_interaction",
22892308
bool,

log.lammps

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,83 @@
1-
LAMMPS (22 Jul 2025 - Update 4)
2-
units metal
3-
atom_style atomic
4-
lattice fcc 3.5
5-
Lattice spacing in x,y,z = 3.5 3.5 3.5
6-
region box block 0 1 0 1 0 1
7-
create_box 1 box
8-
Created orthogonal box = (0 0 0) to (3.5 3.5 3.5)
1+
LAMMPS (30 Mar 2026 - Development - 7f680de296)
2+
OMP_NUM_THREADS environment is not set. Defaulting to 1 thread.
3+
using 1 OpenMP thread(s) per MPI task
4+
units real
5+
atom_style charge
6+
plugin load /data/home/public/jiangzhen/dp/deepmd-kit/build/sog_lmp_develop_plugin/lmp/plugin/libdeepmd_lmp.so
7+
Loading plugin: deepmd pair style by Han Wang
8+
Loading plugin: deepspin pair style by Duo Zhang
9+
Loading plugin: compute deeptensor/atom by Han Wang
10+
Loading plugin: fix dplr by Han Wang
11+
Loading plugin: kspace pppm/dplr by Han Wang
12+
Loading plugin: kspace les by DeepMD contributors
13+
Loading plugin: kspace sog by DeepMD contributors
14+
pair_style coul/long 10.0
15+
kspace_style sog 1e-5 2.0 5.027010924194599 12 spline cubes2_4 remove_self_interaction no
16+
read_data /tmp/water_clean.data
17+
Reading data file ...
18+
orthogonal box = (7.9947316 0 0) to (52.005268 44.121 54.219)
919
1 by 1 by 1 MPI processor grid
10-
create_atoms 1 box
11-
Created 4 atoms
12-
using lattice units in orthogonal box = (0 0 0) to (3.5 3.5 3.5)
13-
create_atoms CPU = 0.000 seconds
14-
pair_style deepmd /tmp/nonexistent.pth
15-
ERROR: Unrecognized pair style 'deepmd' (src/force.cpp:275)
16-
Last input line: pair_style deepmd /tmp/nonexistent.pth
20+
reading atoms ...
21+
10368 atoms
22+
read_data CPU = 0.023 seconds
23+
pair_coeff * *
24+
thermo 1
25+
thermo_style custom step pe ke etotal pxx pyy pzz pxy pxz pyz vol
26+
run 0
27+
WARNING: No fixes with time integration, atoms won't move
28+
For more information see https://docs.lammps.org/err0028 (src/verlet.cpp:60)
29+
SOG initialization ...
30+
b = 2, sigma = 5.02701, M = 12
31+
n_dl = 6.58236, accuracy = 1e-05
32+
w0 = 0.99444649, rcut = 10
33+
SOG-bandwidth grid: phi_max=0.2300 delta=2.3 -> 20x20x24
34+
CubeS2 influence: grid 20x20x24 ngrid=9600
35+
CubeS2 monomials built
36+
CubeS2 1D integrals done, starting 3D loop
37+
CubeS2 influence done
38+
Generated 0 of 1 mixed pair_coeff terms from geometric mixing rule
39+
Neighbor list info ...
40+
update: every = 1 steps, delay = 0 steps, check = yes
41+
max neighbors/atom: 2000, page size: 100000
42+
master list distance cutoff = 12
43+
ghost atom cutoff = 12
44+
binsize = 6, bins = 8 8 10
45+
1 neighbor lists, perpetual/occasional/extra = 1 0 0
46+
(1) pair coul/long, perpetual
47+
attributes: half, newton on
48+
pair build: half/bin/atomonly/newton
49+
stencil: half/bin/3d
50+
bin: standard
51+
SOG virial: max |force·r - Fourier|/max = 1.887629
52+
force·r: 0.1158 0.0227 -0.0662 -0.0541 0.6101 1.1526
53+
Fourier: -0.2882 -0.6333 -0.7060 0.0480 0.1175 0.0562
54+
Per MPI rank memory allocation (min/avg/max) = 24.09 | 24.09 | 24.09 Mbytes
55+
Step PotEng KinEng TotEng Pxx Pyy Pzz Pxy Pxz Pyz Volume
56+
0 -1484147 0 -1484147 -325151.18 -322689.17 -323022.85 1245.8592 623.70609 -62.88239 105281.85
57+
Loop time of 5.4e-07 on 1 procs for 0 steps with 10368 atoms
58+
59+
185.2% CPU use with 1 MPI tasks x 1 OpenMP threads
60+
61+
MPI task timing breakdown:
62+
Section | min time | avg time | max time |%varavg| %total
63+
---------------------------------------------------------------
64+
Pair | 0 | 0 | 0 | 0.0 | 0.00
65+
Kspace | 0 | 0 | 0 | 0.0 | 0.00
66+
Neigh | 0 | 0 | 0 | 0.0 | 0.00
67+
Comm | 0 | 0 | 0 | 0.0 | 0.00
68+
Output | 0 | 0 | 0 | 0.0 | 0.00
69+
Modify | 0 | 0 | 0 | 0.0 | 0.00
70+
Other | | 5.4e-07 | | |100.00
71+
72+
Nlocal: 10368 ave 10368 max 10368 min
73+
Histogram: 1 0 0 0 0 0 0 0 0 0
74+
Nghost: 25333 ave 25333 max 25333 min
75+
Histogram: 1 0 0 0 0 0 0 0 0 0
76+
Neighs: 3.69241e+06 ave 3.69241e+06 max 3.69241e+06 min
77+
Histogram: 1 0 0 0 0 0 0 0 0 0
78+
79+
Total # of neighbors = 3692412
80+
Ave neighs/atom = 356.13542
81+
Neighbor list builds = 0
82+
Dangerous builds = 0
83+
Total wall time: 0:00:00

source/api_cc/src/DeepPotPT.cc

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ void DeepPotPT::compute_with_charge(
479479
nall_real);
480480

481481
atom_charge.clear();
482+
int nchannels = 1;
482483
if (outputs.contains("latent_charge")) {
483484
torch::Tensor latent_tensor = outputs.at("latent_charge").toTensor();
484485
if (latent_tensor.dim() != 3 || latent_tensor.size(0) != nframes) {
@@ -489,25 +490,25 @@ void DeepPotPT::compute_with_charge(
489490
throw deepmd::deepmd_exception(
490491
"latent_charge must have last dimension >= 1 to map to atom->q.");
491492
}
492-
// Keep LAMMPS q mapping consistent with Python-side validation path:
493-
// use the first latent_charge channel as scalar q.
494-
latent_tensor = latent_tensor.slice(/*dim=*/2, /*start=*/0, /*end=*/1);
493+
// Preserve all latent_charge channels (dim_out_lr).
494+
// Flatten as [atom0_ch0, atom0_ch1, ..., atom0_chN, atom1_ch0, ...]
495+
nchannels = static_cast<int>(latent_tensor.size(2));
495496
torch::Tensor flat_latent_ =
496-
latent_tensor.squeeze(-1).contiguous().view({-1}).to(floatType);
497+
latent_tensor.contiguous().view({-1}).to(floatType);
497498
torch::Tensor cpu_latent_ = flat_latent_.to(torch::kCPU);
498499
std::vector<VALUETYPE> dcharge_local;
499500
dcharge_local.assign(cpu_latent_.data_ptr<VALUETYPE>(),
500501
cpu_latent_.data_ptr<VALUETYPE>() +
501502
cpu_latent_.numel());
502-
if (dcharge_local.size() != static_cast<size_t>(nframes * nloc)) {
503+
if (dcharge_local.size() != static_cast<size_t>(nframes * nloc * nchannels)) {
503504
throw deepmd::deepmd_exception(
504505
"latent_charge size is inconsistent with local atom count.");
505506
}
506-
std::vector<VALUETYPE> dcharge_nall_real(nall_real, 0);
507+
std::vector<VALUETYPE> dcharge_nall_real(nall_real * nchannels, 0);
507508
std::copy(dcharge_local.begin(), dcharge_local.end(),
508509
dcharge_nall_real.begin());
509-
atom_charge.resize(static_cast<size_t>(nframes) * fwd_map.size(), 0);
510-
select_map<VALUETYPE>(atom_charge, dcharge_nall_real, bkw_map, 1, nframes,
510+
atom_charge.resize(static_cast<size_t>(nframes) * fwd_map.size() * nchannels, 0);
511+
select_map<VALUETYPE>(atom_charge, dcharge_nall_real, bkw_map, nchannels, nframes,
511512
fwd_map.size(), nall_real);
512513
}
513514

source/lmp/pair_deepmd.cpp

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ static const char cite_user_deepmd_package[] =
120120
PairDeepMD::PairDeepMD(LAMMPS* lmp)
121121
: PairDeepBaseModel(lmp, cite_user_deepmd_package) {
122122
latent_charge_to_q = false;
123+
ncharge_channels = 0;
123124
print_summary(" ");
124125
}
125126

@@ -544,13 +545,25 @@ void PairDeepMD::compute(int eflag, int vflag) {
544545
"latent_charge_to_q is enabled but model output has no "
545546
"latent_charge");
546547
}
547-
if (dcharge.size() < static_cast<size_t>(nlocal)) {
548+
// Detect number of charge channels from dcharge size.
549+
// dcharge is flat: [atom0_ch0, atom0_ch1, ..., atom1_ch0, ...]
550+
// dcharge includes ghost atoms: size = nall * nchannels
551+
int nall = nlocal + atom->nghost;
552+
ncharge_channels = static_cast<int>(dcharge.size()) / nall;
553+
if (dcharge.size() % nall != 0) {
548554
error->all(FLERR,
549-
"latent_charge size is smaller than local atom count");
555+
"latent_charge size is not a multiple of total atom count");
550556
}
557+
if (ncharge_channels < 1) {
558+
error->all(FLERR,
559+
"latent_charge size is smaller than atom count");
560+
}
561+
// Store full multi-channel charge for kspace access
562+
dcharge_multi = dcharge;
563+
// Set atom->q to channel 0 for backward compatibility (existing sog kspace)
551564
double* q = atom->q;
552565
for (int ii = 0; ii < nlocal; ++ii) {
553-
q[ii] = dcharge[ii];
566+
q[ii] = dcharge[ii * ncharge_channels];
554567
}
555568
}
556569

source/lmp/pair_deepmd.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ class PairDeepMD : public PairDeepBaseModel {
5757
private:
5858
CommBrickDeepMD* commdata_;
5959
bool latent_charge_to_q;
60+
61+
public:
62+
// Multi-channel charge access for kspace (fast_dp_sog)
63+
int ncharge_channels;
64+
std::vector<double> dcharge_multi;
6065
};
6166

6267
} // namespace LAMMPS_NS

0 commit comments

Comments
 (0)