Constraining the neural interaction to a physical prior¶
The SpookyNet hybrid potential predicts a total energy that is the sum of a learned neural part and a set of physical priors (CGenFF Lennard-Jones, point-charge electrostatics, ZBL repulsion):
flowchart LR
R["Geometry R, Z, mol_id"] --> N["Neural atomic energies<br/>(message passing, 6 Å cutoff)"]
R --> LJ["CGenFF LJ<br/>(ε, Rmin/2 tables)"]
R --> EL["Electrostatics<br/>(predicted charges)"]
R --> ZBL["ZBL repulsion<br/>(fixed)"]
N --> S(("Σ"))
LJ --> S
EL --> S
ZBL --> S
S --> E["Total energy → loss vs E, F"]
Nothing in the default model constrains how large the neural interaction energy may be relative to those priors. Measured on the 15-pair dimer scans, it is loudest exactly where it should be quietest:
The neural interaction is 4.1× larger where there is no data
| training structures for the pair | mean |neural interaction| |
|---|---|
| ≥ 300 | 0.93 kcal/mol |
| < 15 | 3.85 kcal/mol |
On ACE–BENZ (2 training structures) the neural interaction is 7.76
kcal/mol sitting on an LJ prior of 0.007 — so the network invents a
surface that is +11.9 kcal/mol from PBE0-D3BJ where plain CGenFF is within
0.5. Separately, the learned LJ scaling had collapsed the prior itself to
~1% of its physical value (global_vdw_scale = 0.14).
Three training flags constrain this, in increasing sophistication. All are opt-in; the default behaviour is unchanged.
1. --fixed-cgenff-vdw — pin the LJ prior¶
By default the CGenFF ε is multiplied by three learned factors (a per-atom predicted scale, a global scale, and a per-element scale), so the network can scale the force-field prior toward zero and take over. This flag removes all three, so the LJ term is a fixed physical baseline the network can only add to.
flowchart TB
subgraph Default["default (learned scaling)"]
d1["ε_ij (CGenFF)"] --> d2["× per-atom scale γ_i"]
d2 --> d3["× global_vdw_scale"]
d3 --> d4["× element_vdw_scale"]
d4 --> d5["ε_ij ≈ 0.014 × physical<br/>(prior erased)"]
end
subgraph Fixed["--fixed-cgenff-vdw"]
f1["ε_ij (CGenFF)"] --> f2["ε_ij = physical<br/>(prior intact, network adds on top)"]
end
--fixed-cgenff-vdw
Freezing the prior alone is not enough — it can make things worse
A control that only froze the prior (no other change) scored 6.66
kcal/mol mean dimer RMSE vs 3.63 for the unconstrained model. Restoring the
prior to full strength while the neural term still compensates for a scaled
prior double-counts. --fixed-cgenff-vdw is only useful together with a
constraint on the neural term (§2/§3).
2. --neural-interaction-l2 <λ> — shrink the neural interaction¶
A ridge penalty on the neural interaction energy, i.e. the part of the neural prediction that couples the two monomers:
$$ \mathcal{L} \mathrel{+}= \lambda \,\Big\langle \big(E^{\text{int}}{\text{neural}}\big)^2 \Big\rangle, \qquad E^{\text{int}}{\text{neural}} = E_{\text{neural}}(AB) - E_{\text{neural}}(A) - E_{\text{neural}}(B) $$
The target stays the total energy — this only regularises how loudly the neural term speaks on top of the prior, so the zero-evidence limit becomes CGenFF instead of an invented surface.
The monomer reference $E_{\text{neural}}(A)+E_{\text{neural}}(B)$ is obtained exactly by one extra forward pass with the inter-monomer edges removed from the message-passing graph:
flowchart LR
subgraph Full["forward pass 1: dimer AB"]
g1["all edges<br/>(intra + inter-monomer)"] --> e1["E_neural(AB)"]
end
subgraph Masked["forward pass 2: edge_mask"]
g2["inter-monomer edges cut<br/>(mol_id[dst] ≠ mol_id[src])"] --> e2["E_neural(A) + E_neural(B)"]
end
e1 --> D["E_int = E_neural(AB) − [E_neural(A)+E_neural(B)]"]
e2 --> D
D --> P["penalty λ · ⟨E_int²⟩"]
edge_mask, not batch_mask
The prior terms (LJ / electrostatics / ZBL) are gated by batch_mask, but the
neural message-passing graph runs over dst_idx/src_idx independently.
Masking batch_mask alone leaves the interface fully connected and the
penalty is identically zero. SpookyPhysNet therefore takes an optional
edge_mask that zeroes the message-passing basis for masked edges. The
masked interaction is verified nonzero at contact and exactly zero beyond the
6 Å cutoff.
--fixed-cgenff-vdw --neural-interaction-l2 1.0
A single scalar λ works but is blunt: it fixes sparse dispersion pairs
(BENZ–BENZ 3.62 → 0.74) while over-taxing well-covered hydrogen bonds that
genuinely need a large neural correction (ACE–TIP3, 1632 structures,
0.70 → 2.84). One knob cannot say "shrink C–C hard, leave O–H alone."
3. --interaction-trust-map — a learned per-element-pair λ¶
Generalises the scalar λ to a learned $6\times6$ log-shrinkage matrix over (H, C, N, O, S, Cl), fit by an evidence-balanced negative log-likelihood:
$$ \mathcal{L}_{\text{tm}} = \tfrac12\,\Lambda_s\,r_s^2 \;-\;\tfrac12\,\gamma\,\log \Lambda_s \;+\;\eta\,\text{Var}(\log\lambda) $$
where $r_s$ is a dimer's neural interaction energy and $\Lambda_s$ is the interface-contact-weighted average of $\lambda_{Z_iZ_j}$ over its inter-monomer element pairs. The two terms pull in opposite directions:
flowchart TB
R["r_s = neural interaction<br/>for dimer s"] --> DT["data term<br/>½ Λ_s r_s²<br/>(wants λ small)"]
L["log-evidence term<br/>−½ γ log Λ_s<br/>(wants λ large)"] --> BAL
DT --> BAL{"stationary point"}
BAL --> EQ["λ_c ≈ γ / ⟨r²⟩_c"]
EQ --> SMALL["small λ_c → data justified a<br/>large correction (trust neural)"]
EQ --> LARGE["large λ_c → no correction needed<br/>(fall back to CGenFF prior)"]
HP["shared hyperprior η·Var(log λ)"] -.->|ties sparse buckets<br/>to the common mean| EQ
The stationary point $\lambda_c \approx \gamma/\langle r^2\rangle_c$ (unit-tested) is what makes the matrix meaningful: shrinkage is small where the data supported a large neural correction, large where it did not. The shared hyperprior lets data-poor element pairs borrow strength from the rest.
--fixed-cgenff-vdw --interaction-trust-map \
--neural-interaction-l2 1.0 \
--trust-map-evidence 1.0 --trust-map-hyperprior 0.1
The trust map as a data-provenance fingerprint¶
The learned matrix is a genuine model parameter (checkpointed and restarted like any other). Dump and rank it with:
python scripts/dump_trust_map.py path/to/step-000XXXXX_params.json
H C N O S Cl
H 0.759 0.759 0.746 0.740 0.724 0.735
C 0.759 0.748 0.744 0.744 0.726 0.735
...
Least-shrunk (most trusted) pairs are O-containing — hydrogen-bonded chemistry where the network correction is real; most-shrunk are H–C / C–C — alkyl/aromatic carbon where CGenFF already suffices.
It is a trust map, not a membership oracle
$\lambda_c$ reads "where does the model rely on a learned correction", which
conflates two independent things: how much data the pair had, and whether
CGenFF was already adequate. Measured across pairs,
Spearman(coverage, prior-deviation) = -0.01 — so a chemistry the prior
already handles reads as "unseen" regardless of coverage (e.g. ACE–ACE, 125
training structures, but CGenFF is nearly exact). Use it to know when to
trust the ML term vs. the force field, not to reconstruct the training set.
What the constraints actually buy¶
Two evaluation axes disagree, and the disagreement is the point.
=== "Dimer PES (15 wells, vs PBE0-D3BJ)"
| model | mean RMSE | n≥300 |
|---|---|---|
| CGenFF (force field) | 1.07 | 0.96 |
| ML best (unconstrained) | **3.63** | **0.54** |
| scalar L2 λ=0.1 | 4.46 | 2.06 |
| learned trust map | 4.41 | 2.05 |
The unconstrained model wins — it **overfits** the specific wells, several of
which have 2–13 training structures.
=== "Held-out test set (75k structures, E/F/D MAE)"
| model | energy MAE (kcal/mol) | force MAE (eV/Å) |
|---|---|---|
| L2 λ=1.0 | **0.807** | 0.071 |
| L2 λ=0.1 | 0.829 | 0.071 |
| trust map | 0.844 | 0.071 |
| ML best (unconstrained) | **1.548** | **0.092** |
The ranking **reverses**: the constrained models generalise ~2× better on
energy and clearly better on forces. Cross-check on the other split: the
trust map (0.737) beats the unconstrained model (1.315) even on the latter's
own training split.
Takeaway
The dimer wells reward overfitting; broad held-out generalisation rewards the physical prior. Constraining the neural interaction toward CGenFF trades a little dimer-well sharpness for substantially better generalisation — the regularisation the penalties were designed to provide.
Confound (honest): the two model families trained on different caches, so no single test set is perfectly fair to both. The constrained models win on both test sets, which is robust to that.
Reference¶
| flag | effect |
|---|---|
--fixed-cgenff-vdw |
pin CGenFF LJ at published ε/Rmin (no learned scaling) |
--neural-interaction-l2 <λ> |
ridge-shrink the neural interaction toward the prior |
--interaction-trust-map |
learn per-element-pair λ (empirical Bayes) instead of a scalar |
--trust-map-evidence <γ> |
sets the λ scale via λ ≈ γ/⟨r²⟩ (default 1.0) |
--trust-map-hyperprior <η> |
ties per-element-pair λ toward their common mean (default 0.1) |
Tools:
scripts/dump_trust_map.py— print and rank the learned trust map.scripts/eval_spooky_on_cache.py— held-out E/F/D/Q MAE on a prepared Orbax cache.