Skip to content

feat(dpa3): decouple charge_spin from fparam#5431

Open
iProzd wants to merge 1 commit intodeepmodeling:masterfrom
iProzd:0505_reformat_chg_spin
Open

feat(dpa3): decouple charge_spin from fparam#5431
iProzd wants to merge 1 commit intodeepmodeling:masterfrom
iProzd:0505_reformat_chg_spin

Conversation

@iProzd
Copy link
Copy Markdown
Member

@iProzd iProzd commented May 5, 2026

add_chg_spin_ebd=True previously hijacked fparam to smuggle the
[charge, spin] scalars into DPA3, forcing users to set numb_fparam=2
on the fitting net and blocking real frame parameters from coexisting
with charge/spin. This PR plumbs charge_spin: Tensor | None as a
first-class kwarg through every forward chain and adds an optional
default_chg_spin fallback on the DPA3 descriptor.

Backends covered: pt, dpmodel, pt_expt. The pd backend is left untouched.
The C/C++/LAMMPS layer is unchanged.

Forward chain

Calculator / deep_eval / dp test / lmdb_data / training.get_data
-> wrapper.forward
-> ener_model.forward / forward_lower
-> make_model.forward_common / forward_common_lower
-> base_atomic_model.forward_common_atomic
-> dp_atomic_model.forward_atomic # default_chg_spin fallback here
-> descriptor.forward # only DPA3 consumes it

All other descriptors (se_e2_a, se_r, se_t, se_t_tebd, dpa1, dpa2,
hybrid) only forward the kwarg through their signatures.

New API surface

On BaseAtomicModel and the wrapped model:

  • has_chg_spin_ebd() -> bool
  • get_dim_chg_spin() -> int # 2 for DPA3, else 0
  • has_default_chg_spin() -> bool
  • get_default_chg_spin() -> list[float] | Tensor | None

DPA3 descriptor gains a default_chg_spin: list[float] | None = None
constructor arg (length 2, validated; round-trips through serialize).
descrpt_dpa3_args exposes the matching Argument and the
add_chg_spin_ebd doc no longer references fparam.

Training data

charge_spin is registered as a DataRequirementItem(ndof=2, atomic=False, must=not has_default_cs, default=cs_default). The
get_data path drops it (along with fparam) on frames where
find_charge_spin == 0, so missing per-frame data falls back to
default_chg_spin when one is configured.

pt_expt specifics

forward_common_atomic, forward_common_lower_exportable, the
make_fx-traced inner fn, _trace_and_compile, and all wrapping
energy/spin/dipole/dos/polar/property/dp_linear/dp_zbl model variants
gained a charge_spin arg in lockstep so the export and inductor-
compiled paths keep matching signatures. deep_eval no longer reuses
fparam for charge/spin — it constructs charge_spin_t (with the
metadata default-fallback) and passes it explicitly.

Tests

Three cs_mode cases are exercised everywhere it matters:
no_chg_spin, explicit_chg_spin, default_chg_spin.

  • pt UT (source/tests/pt/model/test_dpa3.py::test_consistency)
    rewritten over the three modes; default mode also asserts that the
    default-fallback descriptor matches an explicit [5,1] peer.
  • pt_expt UT (source/tests/pt_expt/descriptor/test_dpa3.py) gains
    test_consistency_chg_spin covering explicit and default modes
    against dpmodel.
  • Universal tests: DescriptorParamDPA3 learns default_chg_spin,
    parametrize gains (None, [5.0, 1.0]), and the
    add_chg_spin_ebd skip rule in test_model.py is replaced —
    the universal driver does not feed charge_spin, so chg_spin runs
    rely on the default_chg_spin fallback. 622 DPA3 model cases pass.
  • Consistent tests: descriptor/common.py threads charge_spin
    through every eval_* (pd ignores it). test_dpa3.py swaps
    self.fparam for self.charge_spin. test_ener.py:: TestEnerChgSpinEbdFparam is reparametrized over the three modes
    and no longer touches numb_fparam / default_fparam.

Smoke

examples/water/dpa3 dp --pt train input_torch_dynamic.json --skip-neighbor-stat runs to batch 600 with monotonically decreasing
loss.

Test plan

  • pytest source/tests/pt/model/test_dpa3.py -v
  • pytest source/tests/pt_expt/descriptor/test_dpa3.py -v
  • pytest source/tests/consistent/descriptor/test_dpa3.py -v
  • pytest source/tests/consistent/model/test_ener.py::TestEnerChgSpinEbdFparam -v
  • pytest source/tests/universal/dpmodel/model/test_model.py -k "DPA3 and 5"
  • examples/water/dpa3 smoke training (600 batches, loss decreasing)

Summary by CodeRabbit

Release Notes

  • New Features

    • Models now support optional charge-spin inputs (charge and spin per frame) throughout the inference pipeline.
    • Added capability-query methods to check model support for charge-spin embeddings and default values.
    • Calculator reads charge-spin information from atomic structure metadata.
  • Tests

    • Expanded test coverage for charge-spin embedding across descriptors and model implementations.

Copilot AI review requested due to automatic review settings May 5, 2026 16:07
@github-actions github-actions Bot added the Python label May 5, 2026
@dosubot dosubot Bot added the new feature label May 5, 2026
@iProzd iProzd requested review from njzjz and wanghan-iapcm May 5, 2026 16:08
Comment on lines +40 to +48
def forward(
self,
coord: torch.Tensor,
atype: torch.Tensor,
box: torch.Tensor | None = None,
fparam: torch.Tensor | None = None,
aparam: torch.Tensor | None = None,
charge_spin: torch.Tensor | None = None,
do_atomic_virial: bool = False,
Comment on lines +70 to +79
return model_predict

def forward_lower(
self,
extended_coord: torch.Tensor,
extended_atype: torch.Tensor,
nlist: torch.Tensor,
mapping: torch.Tensor | None = None,
fparam: torch.Tensor | None = None,
aparam: torch.Tensor | None = None,
Comment on lines +40 to +48
def forward(
self,
coord: torch.Tensor,
atype: torch.Tensor,
box: torch.Tensor | None = None,
fparam: torch.Tensor | None = None,
aparam: torch.Tensor | None = None,
charge_spin: torch.Tensor | None = None,
do_atomic_virial: bool = False,
Comment on lines +64 to +73
return model_predict

def forward_lower(
self,
extended_coord: torch.Tensor,
extended_atype: torch.Tensor,
nlist: torch.Tensor,
mapping: torch.Tensor | None = None,
fparam: torch.Tensor | None = None,
aparam: torch.Tensor | None = None,
Comment on lines +40 to +48
def forward(
self,
coord: torch.Tensor,
atype: torch.Tensor,
box: torch.Tensor | None = None,
fparam: torch.Tensor | None = None,
aparam: torch.Tensor | None = None,
charge_spin: torch.Tensor | None = None,
do_atomic_virial: bool = False,
Comment on lines +70 to +79
return model_predict

def forward_lower(
self,
extended_coord: torch.Tensor,
extended_atype: torch.Tensor,
nlist: torch.Tensor,
mapping: torch.Tensor | None = None,
fparam: torch.Tensor | None = None,
aparam: torch.Tensor | None = None,
Comment on lines +40 to +48
def forward(
self,
coord: torch.Tensor,
atype: torch.Tensor,
box: torch.Tensor | None = None,
fparam: torch.Tensor | None = None,
aparam: torch.Tensor | None = None,
charge_spin: torch.Tensor | None = None,
do_atomic_virial: bool = False,
Comment on lines +64 to +73
return model_predict

def forward_lower(
self,
extended_coord: torch.Tensor,
extended_atype: torch.Tensor,
nlist: torch.Tensor,
mapping: torch.Tensor | None = None,
fparam: torch.Tensor | None = None,
aparam: torch.Tensor | None = None,
Comment on lines +44 to +52
def forward(
self,
coord: torch.Tensor,
atype: torch.Tensor,
box: torch.Tensor | None = None,
fparam: torch.Tensor | None = None,
aparam: torch.Tensor | None = None,
charge_spin: torch.Tensor | None = None,
do_atomic_virial: bool = False,
Comment on lines +69 to +78
return model_predict

def forward_lower(
self,
extended_coord: torch.Tensor,
extended_atype: torch.Tensor,
nlist: torch.Tensor,
mapping: torch.Tensor | None = None,
fparam: torch.Tensor | None = None,
aparam: torch.Tensor | None = None,
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR makes charge/spin a first-class per-frame input for DPA3 by introducing a dedicated charge_spin kwarg through the forward/eval/training chains, removing the prior coupling to fparam, and adding an optional default_chg_spin fallback on the DPA3 descriptor.

Changes:

  • Plumb charge_spin: Tensor | None through PT / dpmodel / PT_EXPT forward paths, plus data loading/statistics and CLI evaluation entrypoints.
  • Add default_chg_spin to DPA3 descriptor config/serialization and wire a default fallback in dp_atomic_model.forward_atomic.
  • Update/expand unit and consistency tests to cover no/explicit/default charge+spin modes.

Reviewed changes

Copilot reviewed 60 out of 60 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
source/tests/universal/dpmodel/model/test_model.py Update universal skip logic to require default_chg_spin when add_chg_spin_ebd is enabled.
source/tests/universal/dpmodel/descriptor/test_descriptor.py Add default_chg_spin parameterization for DPA3 universal descriptor tests.
source/tests/pt/model/test_dpa3.py Rework PT DPA3 consistency test across three cs_mode cases and switch inputs from fparam to charge_spin.
source/tests/pt_expt/descriptor/test_dpa3.py Add PT_EXPT DPA3 charge/spin consistency test vs dpmodel, including default mode.
source/tests/consistent/model/test_ener.py Reparameterize energy consistency test across charge/spin modes and switch plumbing to charge_spin.
source/tests/consistent/descriptor/test_dpa3.py Switch consistent descriptor tests from fparam to charge_spin.
source/tests/consistent/descriptor/common.py Thread charge_spin through backend-agnostic descriptor evaluation helpers.
deepmd/utils/argcheck.py Add default_chg_spin argument and update add_chg_spin_ebd documentation for DPA3.
deepmd/pt/utils/stat.py Pass charge_spin through PT stat prediction path.
deepmd/pt/train/wrapper.py Accept and forward charge_spin through the PT training wrapper.
deepmd/pt/train/training.py Add charge_spin input key handling and register it as a data requirement when needed.
deepmd/pt/model/model/make_model.py Forward charge_spin through common forward paths and expose new chg/spin-related query methods.
deepmd/pt/model/model/ener_model.py Add charge_spin to PT energy model forward/forward_lower signatures and calls.
deepmd/pt/model/descriptor/se_t.py Add charge_spin passthrough to descriptor forward signature.
deepmd/pt/model/descriptor/se_t_tebd.py Add charge_spin passthrough to descriptor forward signature.
deepmd/pt/model/descriptor/se_r.py Add charge_spin passthrough to descriptor forward signature.
deepmd/pt/model/descriptor/se_a.py Add charge_spin passthrough to descriptor forward signature.
deepmd/pt/model/descriptor/hybrid.py Forward charge_spin into sub-descriptors during hybrid descriptor evaluation.
deepmd/pt/model/descriptor/dpa3.py Implement default_chg_spin config/serialization + consume charge_spin instead of fparam.
deepmd/pt/model/descriptor/dpa2.py Add charge_spin passthrough to descriptor forward signature.
deepmd/pt/model/descriptor/dpa1.py Add charge_spin passthrough to descriptor forward signature.
deepmd/pt/model/atomic_model/pairtab_atomic_model.py Add charge_spin passthrough to atomic model forward signature.
deepmd/pt/model/atomic_model/linear_atomic_model.py Add charge_spin passthrough and forward it into underlying atomic model.
deepmd/pt/model/atomic_model/dp_atomic_model.py Apply default_chg_spin fallback and pass charge_spin into the descriptor.
deepmd/pt/model/atomic_model/base_atomic_model.py Add base hooks and forward plumbing for charge_spin across atomic-model interfaces.
deepmd/pt/infer/deep_eval.py Add charge_spin to PT deep_eval eval path and model call plumbing.
deepmd/pt_expt/train/wrapper.py Accept and forward charge_spin through the PT_EXPT training wrapper.
deepmd/pt_expt/train/training.py Add charge/spin data requirement and thread charge_spin through trace/compile + runtime get_data.
deepmd/pt_expt/model/spin_ener_model.py Add charge_spin to PT_EXPT spin-energy model forward chains and exportable tracing fn signatures.
deepmd/pt_expt/model/property_model.py Add charge_spin to PT_EXPT property model forward/lower/exportable paths.
deepmd/pt_expt/model/polar_model.py Add charge_spin to PT_EXPT polar model forward/lower/exportable paths.
deepmd/pt_expt/model/make_model.py Thread charge_spin through PT_EXPT common/atomic forward and Hessian helpers.
deepmd/pt_expt/model/ener_model.py Add charge_spin to PT_EXPT energy model forward/lower/exportable paths.
deepmd/pt_expt/model/dp_zbl_model.py Add charge_spin to PT_EXPT ZBL model forward/lower/exportable paths.
deepmd/pt_expt/model/dp_linear_model.py Add charge_spin to PT_EXPT linear model forward/lower/exportable paths.
deepmd/pt_expt/model/dos_model.py Add charge_spin to PT_EXPT DOS model forward/lower/exportable paths.
deepmd/pt_expt/model/dipole_model.py Add charge_spin to PT_EXPT dipole model forward/lower/exportable paths.
deepmd/pt_expt/infer/deep_eval.py Add charge_spin to PT_EXPT evaluator API, metadata, and input preparation (incl. default fallback).
deepmd/infer/deep_eval.py Add chg/spin capability query hooks on the backend-agnostic evaluator interface.
deepmd/entrypoints/test.py Extend dp test to request/pass charge_spin when required by the model.
deepmd/dpmodel/utils/stat.py Pass charge_spin through dpmodel stat prediction path.
deepmd/dpmodel/utils/lmdb_data.py Ensure LMDB frames always get find_charge_spin alongside other optional inputs.
deepmd/dpmodel/utils/batch.py Treat charge_spin as a model input key in batch normalization/splitting.
deepmd/dpmodel/model/make_model.py Thread charge_spin through dpmodel forward/call_common interfaces and add chg/spin query methods.
deepmd/dpmodel/model/ener_model.py Add charge_spin to dpmodel energy model call/call_lower signatures and calls.
deepmd/dpmodel/descriptor/se_t.py Add charge_spin passthrough to dpmodel descriptor call signature.
deepmd/dpmodel/descriptor/se_t_tebd.py Add charge_spin passthrough to dpmodel descriptor call signature.
deepmd/dpmodel/descriptor/se_r.py Add charge_spin passthrough to dpmodel descriptor call signature.
deepmd/dpmodel/descriptor/se_e2_a.py Add charge_spin passthrough to dpmodel descriptor call signature.
deepmd/dpmodel/descriptor/make_base_descriptor.py Update base descriptor interface to accept charge_spin.
deepmd/dpmodel/descriptor/hybrid.py Forward charge_spin into sub-descriptors during hybrid descriptor evaluation.
deepmd/dpmodel/descriptor/dpa3.py Implement default_chg_spin config/serialization + consume charge_spin instead of fparam.
deepmd/dpmodel/descriptor/dpa2.py Add charge_spin passthrough to dpmodel descriptor call signature.
deepmd/dpmodel/descriptor/dpa1.py Add charge_spin passthrough to dpmodel descriptor call signature.
deepmd/dpmodel/atomic_model/pairtab_atomic_model.py Add charge_spin passthrough to dpmodel atomic model forward signature.
deepmd/dpmodel/atomic_model/make_base_atomic_model.py Update base atomic model interface to accept charge_spin.
deepmd/dpmodel/atomic_model/linear_atomic_model.py Add charge_spin passthrough and forward it into underlying atomic model.
deepmd/dpmodel/atomic_model/dp_atomic_model.py Apply default_chg_spin fallback and pass charge_spin into the descriptor; add chg/spin query methods.
deepmd/dpmodel/atomic_model/base_atomic_model.py Add base hooks and forward plumbing for charge_spin across dpmodel atomic-model interfaces.
deepmd/calculator.py Allow ASE calculator path to pass atoms.info["charge_spin"] into evaluator.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +129 to +131
assert len(default_chg_spin) == 2, (
"default_chg_spin must be a list of length 2 [charge, spin]."
)
Comment on lines +273 to +276
return torch.tensor(
self.default_chg_spin,
dtype=self.prec,
device=env.DEVICE,
self.use_econf_tebd = use_econf_tebd
self.add_chg_spin_ebd = add_chg_spin_ebd
self.default_chg_spin = default_chg_spin
if self.add_chg_spin_ebd and self.default_chg_spin is not None:
Comment on lines +558 to +571
@torch.jit.export
def has_chg_spin_ebd(self) -> bool:
"""Check if the model has charge spin embedding."""
return self.atomic_model.has_chg_spin_ebd()

@torch.jit.export
def has_default_chg_spin(self) -> bool:
"""Check if the model has default charge_spin values."""
return self.atomic_model.has_default_chg_spin()

@torch.jit.export
def get_default_chg_spin(self) -> torch.Tensor | None:
"""Get the default charge_spin values."""
return self.atomic_model.get_default_chg_spin()
Comment on lines +711 to +721
def has_chg_spin_ebd(self) -> bool:
"""Check if the model has charge spin embedding."""
return self.atomic_model.has_chg_spin_ebd()

def has_default_chg_spin(self) -> bool:
"""Check if the model has default charge_spin values."""
return self.atomic_model.has_default_chg_spin()

def get_default_chg_spin(self) -> list[float] | None:
"""Get the default charge_spin values."""
return self.atomic_model.get_default_chg_spin()
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

📝 Walkthrough

Walkthrough

This PR adds dedicated charge_spin input support throughout the DeepMD stack, enabling models to receive charge and spin information as a dedicated parameter instead of via fparam. The change threads charge_spin from calculator/inference APIs through descriptors and atomic models to predictions, updates DPA3 to compute embeddings from charge_spin rather than fparam, and includes default_chg_spin configuration for descriptor defaults.

Changes

Charge-Spin Input Threading

Layer / File(s) Summary
Public API Signatures
deepmd/calculator.py, deepmd/infer/deep_eval.py, deepmd/pt/infer/deep_eval.py, deepmd/pt_expt/infer/deep_eval.py
Calculator and inference entry points accept optional charge_spin parameter and expose has_chg_spin_ebd() / has_default_chg_spin() capability queries.
Model/Descriptor Interfaces
deepmd/dpmodel/model/ener_model.py, deepmd/pt/model/model/ener_model.py, deepmd/pt_expt/model/*_model.py
Energy, dipole, DOS, polar, property, spin-energy, linear, and ZBL models add charge_spin parameter to forward / forward_lower / forward_lower_exportable methods.
Atomic Model Implementations
deepmd/dpmodel/atomic_model/base_atomic_model.py, deepmd/dpmodel/atomic_model/dp_atomic_model.py, deepmd/pt/model/atomic_model/base_atomic_model.py, deepmd/pt/model/atomic_model/dp_atomic_model.py, deepmd/pt_expt/model/make_model.py
Base and concrete atomic models accept charge_spin, forward it through the call chain, and expose capability/default-value query methods.
Descriptor Implementations
deepmd/dpmodel/descriptor/dpa1.py, deepmd/dpmodel/descriptor/dpa2.py, deepmd/dpmodel/descriptor/dpa3.py, deepmd/dpmodel/descriptor/hybrid.py, deepmd/dpmodel/descriptor/se_*.py, deepmd/pt/model/descriptor/*
All descriptor call / forward methods add charge_spin parameter; most accept and forward without use, while DPA3 actively consumes it.
DPA3 Charge-Spin Embedding
deepmd/dpmodel/descriptor/dpa3.py, deepmd/pt/model/descriptor/dpa3.py
DPA3 now accepts default_chg_spin config, derives charge/spin from charge_spin input instead of fparam, and exposes get_dim_chg_spin() / has_default_chg_spin() / get_default_chg_spin() query methods.
Helper Method Additions
deepmd/dpmodel/atomic_model/base_atomic_model.py, deepmd/dpmodel/model/make_model.py, deepmd/pt/model/atomic_model/base_atomic_model.py, deepmd/pt/model/model/make_model.py, deepmd/infer/deep_eval.py
Introduce has_chg_spin_ebd(), has_default_chg_spin(), get_default_chg_spin() (and related) methods to expose charge-spin embedding capabilities and defaults.
Data Flow & Batching
deepmd/dpmodel/utils/batch.py, deepmd/dpmodel/utils/lmdb_data.py, deepmd/dpmodel/utils/stat.py
Add charge_spin to model input key set; extend LMDB reader to set find_charge_spin flag; thread charge_spin through statistics computation.
Training Pipeline Integration
deepmd/pt/train/training.py, deepmd/pt/train/wrapper.py, deepmd/pt_expt/train/training.py, deepmd/pt_expt/train/wrapper.py
Training data requirements now conditionally include charge_spin when model supports embeddings; ModelWrapper forwards it through input_dict to model; compilation and traced export paths include charge_spin.
Test Interface Updates
source/tests/consistent/descriptor/common.py
Descriptor evaluation helpers (eval_dp_descriptor, eval_pt_descriptor, etc.) accept and forward charge_spin to backend implementations.
Test Refactoring
source/tests/consistent/descriptor/test_dpa3.py, source/tests/consistent/model/test_ener.py, source/tests/pt/model/test_dpa3.py, source/tests/pt_expt/descriptor/test_dpa3.py, source/tests/universal/dpmodel/descriptor/test_descriptor.py, source/tests/universal/dpmodel/model/test_model.py
Test fixtures and parameterization updated to use charge_spin instead of fparam for charge/spin embedding; added cs_mode parameter (no/explicit/default variants); fixed universal model skip conditions.
Configuration & Documentation
deepmd/utils/argcheck.py, deepmd/entrypoints/test.py
Added default_chg_spin parameter to DPA3 descriptor schema; updated test_ener to conditionally register and extract charge_spin from test data.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~35 minutes

Possibly related PRs

Suggested labels

new feature, Python, C++, LAMMPS

Suggested reviewers

  • njzjz
  • wanghan-iapcm
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main change: decoupling charge_spin from fparam in the DPA3 descriptor. This aligns with the primary objective of making charge_spin a first-class keyword argument throughout the forward chain rather than embedding it in fparam.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
deepmd/pt/model/descriptor/dpa3.py (1)

206-211: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Off-by-one in charge embedding capacity — charge=100 causes IndexError at runtime.

TypeEmbedNet(200, ...) creates an embedding with valid indices 0–199. After the + 100 offset, the maximum valid input charge value is 99 (index 199), not 100. Passing charge=100 produces index 200, which is out-of-bounds and raises a runtime IndexError. Yet the comment directly above claims "-100 ~ 100 is a conservative bound", so the intent was clearly to support 100 as a valid value.

Fix: use 201 entries to honour the documented inclusive range [-100, 100]:

🐛 Proposed fix
-            # -100 ~ 100 is a conservative bound
-            self.chg_embedding = TypeEmbedNet(
-                200,
+            # -100 ~ 100 inclusive, shifted by +100 → indices 0..200
+            self.chg_embedding = TypeEmbedNet(
+                201,
                 self.tebd_dim,
                 ...
             )

And correspondingly in the forward:

-            charge = charge_spin[:, 0].to(dtype=torch.int64) + 100
+            charge = (charge_spin[:, 0].to(dtype=torch.int64) + 100).clamp(0, 200)

The same concern applies to the spin embedding (TypeEmbedNet(100, ...), valid indices 0–99): there is no guard against negative spin values or spin ≥ 100, and no documentation of the valid range. Consider adding a clamp or assert.

Also applies to: 586-597

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/pt/model/descriptor/dpa3.py` around lines 206 - 211, The charge
embedding chg_embedding currently constructs TypeEmbedNet(200, ...) while inputs
are shifted by +100, causing an off-by-one (charge=100 -> index 200) and
IndexError; change the embedding size to 201 so indices 0..200 cover charges
-100..100 and ensure the forward mapping that adds 100 uses that new range. Also
audit the spin embedding (TypeEmbedNet(100, ...) and any uses that index it) and
add a clamp or assert in the forward method to enforce valid spin and charge
ranges (or expand the embedding size if the intended inclusive range is larger);
apply the same fix to the other occurrence around the spin/charge embedding
block referenced later (lines ~586-597).
deepmd/pt_expt/model/spin_ener_model.py (1)

151-188: ⚠️ Potential issue | 🔴 Critical

SpinModel.forward_common_lower_exportable must accept and forward charge_spin parameter

SpinModel.forward_common_lower_exportable (lines 51–126 in spin_model.py) does not include charge_spin in its signature or pass it to the traced inner function. However, SpinEnerModel.forward_lower_exportable calls it with charge_spin=charge_spin and then attempts to call the returned traced module with charge_spin as the 8th positional argument.

This creates a critical mismatch:

  1. The call self.forward_common_lower_exportable(..., charge_spin=charge_spin) passes charge_spin as an unexpected keyword argument (will be absorbed into **make_fx_kwargs and cause an error in make_fx)
  2. The traced function was created without charge_spin in its signature, so calling traced(..., charge_spin) will fail with a positional argument count mismatch

Update SpinModel.forward_common_lower_exportable to match the pattern in make_model.py (lines 333–435): add charge_spin to the signature, include it in the inner fn parameters as the 7th positional argument (before do_atomic_virial), and pass it through the traced call chain.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/pt_expt/model/spin_ener_model.py` around lines 151 - 188,
SpinModel.forward_common_lower_exportable currently omits the charge_spin
parameter causing a signature mismatch when
SpinEnerModel.forward_lower_exportable passes charge_spin through; update
SpinModel.forward_common_lower_exportable to add charge_spin to its signature,
include charge_spin as the 7th positional parameter in the inner traced wrapper
(the inner fn that currently accepts extended_coord, extended_atype,
extended_spin, nlist, mapping, fparam, aparam, ...) and ensure charge_spin is
forwarded into the traced(...) call and through any make_fx_kwargs plumbing (so
it is not swallowed by **make_fx_kwargs). Reference
SpinModel.forward_common_lower_exportable,
SpinEnerModel.forward_lower_exportable, the traced object and inner fn to find
and make the change.
deepmd/pt_expt/infer/deep_eval.py (1)

1117-1141: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The positional vs. keyword charge_spin asymmetry between .pt2 and eager paths is correctly identified.

Both the non-spin (_eval_model) and spin (_eval_model_spin) paths show this pattern: _pt2_runnerreceivescharge_spin_tas a positional argument (7th for non-spin, 8th for spin afterext_spin_t), while exported_modulereceives it as a keyword argument. The export-time signatures inener_model.py(non-spin) andspin_ener_model.py(spin) correctly definecharge_spinin these exact positions, so the current code is safe. However, the concern about fragility is valid—reordering arguments at export time or inserting new ones beforecharge_spinwould silently pass wrong tensors to.pt2runners. Adding an end-to-end test that exercises.pt2 with non-None charge_spin` would catch such regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/pt_expt/infer/deep_eval.py` around lines 1117 - 1141, The positional
vs keyword asymmetry can lead to silent mis-wiring of arguments; update calls to
_pt2_runner (in both _eval_model and _eval_model_spin) to pass charge_spin
explicitly as a keyword (charge_spin=charge_spin_t) instead of as a positional
arg (and similarly ensure ext_spin_t remains positional in the spin path), and
add an end-to-end test that invokes the .pt2 runner with a non-None charge_spin
to catch future argument-reordering regressions.
🧹 Nitpick comments (5)
deepmd/dpmodel/model/make_model.py (1)

301-334: ⚡ Quick win

charge_spin bypasses _input_type_cast, risking dtype mismatch

In call_common (line 318) and call_common_lower (line 387–388), fparam and aparam are explicitly cast to the global float precision through _input_type_cast. charge_spin is forwarded uncasted. If a caller supplies charge_spin with a different dtype than coord, the descriptor receives inconsistently typed inputs, leading to runtime dtype errors in strict backends (e.g., PyTorch with mixed float32/float64).

🛠️ Minimal inline fix in `call_common_lower`
 cc_ext, _, fp, ap, input_prec = self._input_type_cast(
     extended_coord, fparam=fparam, aparam=aparam
 )
 del extended_coord, fparam, aparam
+if charge_spin is not None:
+    xp_cs = array_api_compat.array_namespace(charge_spin)
+    global_dtype = get_xp_precision(
+        xp_cs, RESERVED_PRECISION_DICT[self.global_np_float_precision]
+    )
+    if charge_spin.dtype != global_dtype:
+        charge_spin = xp_cs.astype(charge_spin, global_dtype)
 model_predict = self.forward_common_atomic(

A symmetric fix should be applied in call_common as well (before model_call_from_call_lower).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/dpmodel/model/make_model.py` around lines 301 - 334, The call path
forwards charge_spin without type normalization, so modify call_common and
call_common_lower to include charge_spin in the inputs processed by
_input_type_cast (or explicitly cast charge_spin to the same float dtype as
returned by _input_type_cast) so that charge_spin is converted to input_prec
before calling model_call_from_call_lower or passing into the lower-level
descriptor; update the call sites: in call_common (before invoking
model_call_from_call_lower) and in call_common_lower (at the start of the
function) to use the normalized/converted charge_spin variable returned or
produced by _input_type_cast, ensuring all arrays
(coord/box/fparam/aparam/charge_spin) share the same dtype.
deepmd/pt_expt/infer/deep_eval.py (3)

1273-1297: ⚖️ Poor tradeoff

Charge_spin handling duplicated between _eval_model_spin and _prepare_inputs.

_eval_model_spin re-implements its full input prep inline (including this new charge_spin block) instead of going through _prepare_inputs. The block is byte-identical to the one at lines 1057-1081, so any future change (e.g., to the default-fallback policy or dtype) has to be made in two places and will silently drift if missed. This mirrors the pre-existing fparam / aparam duplication in the same method, so it's not a regression — but it's worth refactoring _eval_model_spin to share the prep with _prepare_inputs (e.g., by extracting an _extend_spin helper to handle the spin-specific part and delegating the rest). Deferable; flagging for tracking.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/pt_expt/infer/deep_eval.py` around lines 1273 - 1297, The charge_spin
handling is duplicated between _eval_model_spin and _prepare_inputs; extract the
shared logic into a single helper (e.g., _extend_spin or prepare_charge_spin)
and have both _prepare_inputs and _eval_model_spin call it to produce
charge_spin_t (handling numpy/torch, dtype/device, reshape/unsqueeze/expand,
default_chg_spin fallback and the ValueError path); update _eval_model_spin to
remove its inline block and delegate to the new helper so future dtype or
default-fallback changes are made in one place.

1526-1535: 💤 Low value

Gate on dp_am.add_chg_spin_ebd is sound; consider extracting the conditional.

The getattr(dp_am, "add_chg_spin_ebd", False) guard correctly avoids handing charge_spin to descriptors that don't consume it (everything other than DPA3, per PR objectives). Same conditional is repeated verbatim in eval_fitting_last_layer at lines 1601-1603. A tiny helper (e.g., _chg_spin_for(dp_am, charge_spin_t)) would DRY this up and make future descriptor additions a one-line change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/pt_expt/infer/deep_eval.py` around lines 1526 - 1535, The code repeats
the conditional getattr(dp_am, "add_chg_spin_ebd", False) when passing
charge_spin to dp_am.descriptor (and later in eval_fitting_last_layer), so
extract a small helper (e.g., a function named _chg_spin_for or similar) that
takes dp_am and charge_spin_t and returns charge_spin_t if getattr(dp_am,
"add_chg_spin_ebd", False) else None; replace the inline conditional in the
dp_am.descriptor call and in eval_fitting_last_layer with a call to that helper
to DRY the logic and simplify future descriptor additions (reference symbols:
dp_am.add_chg_spin_ebd, dp_am.descriptor, eval_fitting_last_layer).

594-635: 💤 Low value

Document the new charge_spin parameter in eval's docstring.

charge_spin is now a first-class kwarg on the public eval API, but the docstring still only describes coords, cells, atom_types, atomic, fparam, aparam, **kwargs. Adding a short entry mirroring fparam (shape nframes x 2, optional, falls back to default_chg_spin when the model has add_chg_spin_ebd=True) will help library users discover the migration path away from packing charge/spin into fparam.

Suggested docstring addition
         aparam
             The atomic parameter.
             The array should be of size nframes x natoms x dim_aparam.
+        charge_spin
+            Per-frame [charge, spin] values for models trained with
+            ``add_chg_spin_ebd=True``. The array should be of size
+            ``nframes x 2``. If omitted, the model's ``default_chg_spin``
+            (when configured) is used.
         **kwargs
             Other parameters
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/pt_expt/infer/deep_eval.py` around lines 594 - 635, Add a docstring
entry for the new public parameter charge_spin in the eval method: describe that
charge_spin is optional, has shape nframes x 2, and that when omitted the code
falls back to default_chg_spin if the model has add_chg_spin_ebd=True (mirror
the style of the existing fparam/aparam descriptions); update the parameter list
under eval(...) to include this short description so users know to stop packing
charge/spin into fparam and where the default comes from (refer to eval,
parameter name charge_spin, model flag add_chg_spin_ebd, and default_chg_spin).
source/tests/consistent/descriptor/common.py (1)

260-293: 💤 Low value

eval_pd_descriptor silently drops charge_spin.

The signature accepts charge_spin, but unlike the other backends (DP/PT/PT_EXPT/JAX/array-api-strict) it is neither converted to a paddle tensor nor forwarded into pd_obj(...). A caller passing charge_spin=... to this helper will get a result computed without it, with no warning. Per the PR objectives the pd backend is intentionally untouched, so either propagate the kwarg (if pd supports it) or at minimum document this in a docstring/inline comment so future test authors don't assume parity with the other helpers.

Suggested clarifying note
     def eval_pd_descriptor(
         self,
         pd_obj: Any,
         natoms: np.ndarray,
         coords: np.ndarray,
         atype: np.ndarray,
         box: np.ndarray,
         mixed_types: bool = False,
         fparam: np.ndarray | None = None,
         charge_spin: np.ndarray | None = None,
     ) -> Any:
+        # NOTE: the pd backend does not yet consume charge_spin; the kwarg
+        # is accepted for signature parity with the other eval_*_descriptor
+        # helpers but is intentionally not forwarded into pd_obj(...).
         ext_coords, ext_atype, mapping = extend_coord_with_ghosts_pd(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@source/tests/consistent/descriptor/common.py` around lines 260 - 293, The
helper eval_pd_descriptor is accepting charge_spin but not forwarding it to
pd_obj; either forward it (if pd_obj supports it) by converting charge_spin to a
paddle tensor on PD_DEVICE (e.g., paddle.to_tensor(charge_spin).to(PD_DEVICE))
and reshaping to the batch form used for coords/atype (match shape [1, -1, ...]
as appropriate) and include charge_spin=... in the pd_obj(...) call, or if pd
backend truly must remain unchanged, add a clear inline comment/docstring in
eval_pd_descriptor stating that charge_spin is intentionally not
handled/forwarded for the pd backend so callers know it will be ignored; update
references to eval_pd_descriptor and pd_obj accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@deepmd/dpmodel/descriptor/dpa3.py`:
- Around line 419-422: Replace assert-based checks for default_chg_spin and
charge_spin with explicit runtime exceptions: locate the validation blocks in
dpa3.py where self.add_chg_spin_ebd and self.default_chg_spin (and the similar
check for charge_spin around the later block) are validated and change the
assert len(...) == 2 to an explicit conditional that raises a ValueError (or
TypeError) with the same message when the length is not 2; ensure both checks
(the one referencing self.default_chg_spin and the one referencing charge_spin)
are updated so validation still runs under optimized Python.

In `@deepmd/entrypoints/test.py`:
- Around line 642-645: The branch that sets charge_spin uses
test_data.get("find_charge_spin", 1.0) which defaults to enabling the branch and
can cause a KeyError; change the default to 0.0 so missing find_charge_spin
falls through safely (i.e., replace test_data.get("find_charge_spin", 1.0) with
test_data.get("find_charge_spin", 0.0")) and keep the rest of the logic around
dp.has_chg_spin_ebd(), test_data["charge_spin"], and numb_test unchanged.

In `@deepmd/pt/model/atomic_model/base_atomic_model.py`:
- Around line 200-202: The return type of BaseAtomicModel.get_default_chg_spin
is too narrow (torch.Tensor | None) and conflicts with
DPAtomicModel.get_default_chg_spin which returns list[float] | None; update the
type annotation on BaseAtomicModel.get_default_chg_spin to accept both
list[float] and torch.Tensor (e.g., list[float] | torch.Tensor | None) so it
matches DPAtomicModel and other subclasses, and add a note in callers (e.g.,
model_forward where the isinstance guard at lines ~658–661 exists) to convert
list->torch.Tensor via a helper like to_torch_tensor when a torch.Tensor is
required.

In `@deepmd/pt/model/atomic_model/dp_atomic_model.py`:
- Around line 389-413: The new `@torch.jit.export` methods in DPAtomicModel
(has_chg_spin_ebd, get_dim_chg_spin, has_default_chg_spin, get_default_chg_spin)
call into self.descriptor (BaseDescriptor) so add declarations on the
BaseDescriptor class for get_dim_chg_spin() -> int, has_default_chg_spin() ->
bool, and get_default_chg_spin() -> Optional[torch.Tensor] (or torch.Tensor |
None) — either as abstract methods or as concrete defaults (return 0, False,
None) — using TorchScript-compatible types so torch.jit.script() can compile all
code paths; update BaseDescriptor’s import/type hints to include torch.Tensor if
needed.

In `@deepmd/pt/model/descriptor/dpa3.py`:
- Around line 128-131: Replace the runtime assertion in the constructor that
checks default_chg_spin with a proper exception: in the code block handling
default_chg_spin (the conditional around default_chg_spin is not None in
dpa3.py), remove the assert and instead raise a ValueError with the same message
("default_chg_spin must be a list of length 2 [charge, spin].") so validation
cannot be bypassed when Python is run with optimizations; keep the same check
for len(default_chg_spin) == 2 and the existing message to aid debugging.
- Around line 268-277: The get_default_chg_spin method's return annotation uses
PEP 604 union syntax (torch.Tensor | None) which TorchScript/@torch.jit.export
doesn't accept; change the signature of get_default_chg_spin to use
typing.Optional[torch.Tensor] (or typing.Union[torch.Tensor, None]) and add the
necessary import for Optional from typing so the exported method compiles under
TorchScript.

---

Outside diff comments:
In `@deepmd/pt_expt/infer/deep_eval.py`:
- Around line 1117-1141: The positional vs keyword asymmetry can lead to silent
mis-wiring of arguments; update calls to _pt2_runner (in both _eval_model and
_eval_model_spin) to pass charge_spin explicitly as a keyword
(charge_spin=charge_spin_t) instead of as a positional arg (and similarly ensure
ext_spin_t remains positional in the spin path), and add an end-to-end test that
invokes the .pt2 runner with a non-None charge_spin to catch future
argument-reordering regressions.

In `@deepmd/pt_expt/model/spin_ener_model.py`:
- Around line 151-188: SpinModel.forward_common_lower_exportable currently omits
the charge_spin parameter causing a signature mismatch when
SpinEnerModel.forward_lower_exportable passes charge_spin through; update
SpinModel.forward_common_lower_exportable to add charge_spin to its signature,
include charge_spin as the 7th positional parameter in the inner traced wrapper
(the inner fn that currently accepts extended_coord, extended_atype,
extended_spin, nlist, mapping, fparam, aparam, ...) and ensure charge_spin is
forwarded into the traced(...) call and through any make_fx_kwargs plumbing (so
it is not swallowed by **make_fx_kwargs). Reference
SpinModel.forward_common_lower_exportable,
SpinEnerModel.forward_lower_exportable, the traced object and inner fn to find
and make the change.

In `@deepmd/pt/model/descriptor/dpa3.py`:
- Around line 206-211: The charge embedding chg_embedding currently constructs
TypeEmbedNet(200, ...) while inputs are shifted by +100, causing an off-by-one
(charge=100 -> index 200) and IndexError; change the embedding size to 201 so
indices 0..200 cover charges -100..100 and ensure the forward mapping that adds
100 uses that new range. Also audit the spin embedding (TypeEmbedNet(100, ...)
and any uses that index it) and add a clamp or assert in the forward method to
enforce valid spin and charge ranges (or expand the embedding size if the
intended inclusive range is larger); apply the same fix to the other occurrence
around the spin/charge embedding block referenced later (lines ~586-597).

---

Nitpick comments:
In `@deepmd/dpmodel/model/make_model.py`:
- Around line 301-334: The call path forwards charge_spin without type
normalization, so modify call_common and call_common_lower to include
charge_spin in the inputs processed by _input_type_cast (or explicitly cast
charge_spin to the same float dtype as returned by _input_type_cast) so that
charge_spin is converted to input_prec before calling model_call_from_call_lower
or passing into the lower-level descriptor; update the call sites: in
call_common (before invoking model_call_from_call_lower) and in
call_common_lower (at the start of the function) to use the normalized/converted
charge_spin variable returned or produced by _input_type_cast, ensuring all
arrays (coord/box/fparam/aparam/charge_spin) share the same dtype.

In `@deepmd/pt_expt/infer/deep_eval.py`:
- Around line 1273-1297: The charge_spin handling is duplicated between
_eval_model_spin and _prepare_inputs; extract the shared logic into a single
helper (e.g., _extend_spin or prepare_charge_spin) and have both _prepare_inputs
and _eval_model_spin call it to produce charge_spin_t (handling numpy/torch,
dtype/device, reshape/unsqueeze/expand, default_chg_spin fallback and the
ValueError path); update _eval_model_spin to remove its inline block and
delegate to the new helper so future dtype or default-fallback changes are made
in one place.
- Around line 1526-1535: The code repeats the conditional getattr(dp_am,
"add_chg_spin_ebd", False) when passing charge_spin to dp_am.descriptor (and
later in eval_fitting_last_layer), so extract a small helper (e.g., a function
named _chg_spin_for or similar) that takes dp_am and charge_spin_t and returns
charge_spin_t if getattr(dp_am, "add_chg_spin_ebd", False) else None; replace
the inline conditional in the dp_am.descriptor call and in
eval_fitting_last_layer with a call to that helper to DRY the logic and simplify
future descriptor additions (reference symbols: dp_am.add_chg_spin_ebd,
dp_am.descriptor, eval_fitting_last_layer).
- Around line 594-635: Add a docstring entry for the new public parameter
charge_spin in the eval method: describe that charge_spin is optional, has shape
nframes x 2, and that when omitted the code falls back to default_chg_spin if
the model has add_chg_spin_ebd=True (mirror the style of the existing
fparam/aparam descriptions); update the parameter list under eval(...) to
include this short description so users know to stop packing charge/spin into
fparam and where the default comes from (refer to eval, parameter name
charge_spin, model flag add_chg_spin_ebd, and default_chg_spin).

In `@source/tests/consistent/descriptor/common.py`:
- Around line 260-293: The helper eval_pd_descriptor is accepting charge_spin
but not forwarding it to pd_obj; either forward it (if pd_obj supports it) by
converting charge_spin to a paddle tensor on PD_DEVICE (e.g.,
paddle.to_tensor(charge_spin).to(PD_DEVICE)) and reshaping to the batch form
used for coords/atype (match shape [1, -1, ...] as appropriate) and include
charge_spin=... in the pd_obj(...) call, or if pd backend truly must remain
unchanged, add a clear inline comment/docstring in eval_pd_descriptor stating
that charge_spin is intentionally not handled/forwarded for the pd backend so
callers know it will be ignored; update references to eval_pd_descriptor and
pd_obj accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 30459192-fb59-447b-9f10-92019ad210db

📥 Commits

Reviewing files that changed from the base of the PR and between 0a481de and 4643268.

📒 Files selected for processing (60)
  • deepmd/calculator.py
  • deepmd/dpmodel/atomic_model/base_atomic_model.py
  • deepmd/dpmodel/atomic_model/dp_atomic_model.py
  • deepmd/dpmodel/atomic_model/linear_atomic_model.py
  • deepmd/dpmodel/atomic_model/make_base_atomic_model.py
  • deepmd/dpmodel/atomic_model/pairtab_atomic_model.py
  • deepmd/dpmodel/descriptor/dpa1.py
  • deepmd/dpmodel/descriptor/dpa2.py
  • deepmd/dpmodel/descriptor/dpa3.py
  • deepmd/dpmodel/descriptor/hybrid.py
  • deepmd/dpmodel/descriptor/make_base_descriptor.py
  • deepmd/dpmodel/descriptor/se_e2_a.py
  • deepmd/dpmodel/descriptor/se_r.py
  • deepmd/dpmodel/descriptor/se_t.py
  • deepmd/dpmodel/descriptor/se_t_tebd.py
  • deepmd/dpmodel/model/ener_model.py
  • deepmd/dpmodel/model/make_model.py
  • deepmd/dpmodel/utils/batch.py
  • deepmd/dpmodel/utils/lmdb_data.py
  • deepmd/dpmodel/utils/stat.py
  • deepmd/entrypoints/test.py
  • deepmd/infer/deep_eval.py
  • deepmd/pt/infer/deep_eval.py
  • deepmd/pt/model/atomic_model/base_atomic_model.py
  • deepmd/pt/model/atomic_model/dp_atomic_model.py
  • deepmd/pt/model/atomic_model/linear_atomic_model.py
  • deepmd/pt/model/atomic_model/pairtab_atomic_model.py
  • deepmd/pt/model/descriptor/dpa1.py
  • deepmd/pt/model/descriptor/dpa2.py
  • deepmd/pt/model/descriptor/dpa3.py
  • deepmd/pt/model/descriptor/hybrid.py
  • deepmd/pt/model/descriptor/se_a.py
  • deepmd/pt/model/descriptor/se_r.py
  • deepmd/pt/model/descriptor/se_t.py
  • deepmd/pt/model/descriptor/se_t_tebd.py
  • deepmd/pt/model/model/ener_model.py
  • deepmd/pt/model/model/make_model.py
  • deepmd/pt/train/training.py
  • deepmd/pt/train/wrapper.py
  • deepmd/pt/utils/stat.py
  • deepmd/pt_expt/infer/deep_eval.py
  • deepmd/pt_expt/model/dipole_model.py
  • deepmd/pt_expt/model/dos_model.py
  • deepmd/pt_expt/model/dp_linear_model.py
  • deepmd/pt_expt/model/dp_zbl_model.py
  • deepmd/pt_expt/model/ener_model.py
  • deepmd/pt_expt/model/make_model.py
  • deepmd/pt_expt/model/polar_model.py
  • deepmd/pt_expt/model/property_model.py
  • deepmd/pt_expt/model/spin_ener_model.py
  • deepmd/pt_expt/train/training.py
  • deepmd/pt_expt/train/wrapper.py
  • deepmd/utils/argcheck.py
  • source/tests/consistent/descriptor/common.py
  • source/tests/consistent/descriptor/test_dpa3.py
  • source/tests/consistent/model/test_ener.py
  • source/tests/pt/model/test_dpa3.py
  • source/tests/pt_expt/descriptor/test_dpa3.py
  • source/tests/universal/dpmodel/descriptor/test_descriptor.py
  • source/tests/universal/dpmodel/model/test_model.py

Comment on lines +419 to +422
if self.add_chg_spin_ebd and self.default_chg_spin is not None:
assert len(self.default_chg_spin) == 2, (
"default_chg_spin must have exactly 2 values [charge, spin]"
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="$(fd -p 'dpa3.py' | head -n 1)"
echo "Inspecting: $FILE"
nl -ba "$FILE" | sed -n '400,710p' | rg -n 'assert|default_chg_spin|charge_spin'

Repository: deepmodeling/deepmd-kit

Length of output: 172


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="$(fd -p 'dpa3.py' | head -n 1)"
echo "Inspecting: $FILE"
echo "=== Lines 415-425 (around first assert) ==="
cat -n "$FILE" | sed -n '415,425p'
echo ""
echo "=== Lines 685-695 (around second assert) ==="
cat -n "$FILE" | sed -n '685,695p'

Repository: deepmodeling/deepmd-kit

Length of output: 1327


Replace assert-based input checks with explicit exceptions.

Lines 420-422 and 690-692 rely on assert for runtime validation. In optimized Python runs (-O), those checks are removed, so invalid default_chg_spin/charge_spin can bypass validation and fail later in less controlled ways.

Proposed fix
         self.add_chg_spin_ebd = add_chg_spin_ebd
         self.default_chg_spin = default_chg_spin
         if self.add_chg_spin_ebd and self.default_chg_spin is not None:
-            assert len(self.default_chg_spin) == 2, (
-                "default_chg_spin must have exactly 2 values [charge, spin]"
-            )
+            if len(self.default_chg_spin) != 2:
+                raise ValueError(
+                    "default_chg_spin must have exactly 2 values [charge, spin]"
+                )
...
         if self.add_chg_spin_ebd:
-            assert charge_spin is not None
-            assert self.chg_embedding is not None
-            assert self.spin_embedding is not None
+            if charge_spin is None:
+                raise ValueError(
+                    "charge_spin must be provided when add_chg_spin_ebd is enabled."
+                )
+            if self.chg_embedding is None or self.spin_embedding is None:
+                raise RuntimeError("charge/spin embeddings are not initialized.")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/dpmodel/descriptor/dpa3.py` around lines 419 - 422, Replace
assert-based checks for default_chg_spin and charge_spin with explicit runtime
exceptions: locate the validation blocks in dpa3.py where self.add_chg_spin_ebd
and self.default_chg_spin (and the similar check for charge_spin around the
later block) are validated and change the assert len(...) == 2 to an explicit
conditional that raises a ValueError (or TypeError) with the same message when
the length is not 2; ensure both checks (the one referencing
self.default_chg_spin and the one referencing charge_spin) are updated so
validation still runs under optimized Python.

Comment on lines +642 to +645
if dp.has_chg_spin_ebd() and test_data.get("find_charge_spin", 1.0) != 0.0:
charge_spin = test_data["charge_spin"][:numb_test]
else:
charge_spin = None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unsafe default for find_charge_spin — should be 0.0, not 1.0.

test_data.get("find_charge_spin", 1.0) differs from the established fparam pattern (line 634 uses direct access test_data["find_fparam"]). If find_charge_spin is absent from test_data (e.g., in an unexpected LMDB layout), a default of 1.0 makes the condition True, which then unconditionally accesses test_data["charge_spin"] and raises KeyError. A default of 0.0 falls through safely to charge_spin = None.

🛡️ Proposed fix
-    if dp.has_chg_spin_ebd() and test_data.get("find_charge_spin", 1.0) != 0.0:
+    if dp.has_chg_spin_ebd() and test_data.get("find_charge_spin", 0.0) != 0.0:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/entrypoints/test.py` around lines 642 - 645, The branch that sets
charge_spin uses test_data.get("find_charge_spin", 1.0) which defaults to
enabling the branch and can cause a KeyError; change the default to 0.0 so
missing find_charge_spin falls through safely (i.e., replace
test_data.get("find_charge_spin", 1.0) with test_data.get("find_charge_spin",
0.0")) and keep the rest of the logic around dp.has_chg_spin_ebd(),
test_data["charge_spin"], and numb_test unchanged.

Comment on lines +200 to +202
def get_default_chg_spin(self) -> torch.Tensor | None:
"""Get the default charge_spin values."""
return None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Return type annotation conflicts with dpmodel subclass implementations

get_default_chg_spin() is declared as returning torch.Tensor | None in PT BaseAtomicModel, but its concrete dpmodel counterpart (DPAtomicModel.get_default_chg_spin() in deepmd/dpmodel/atomic_model/dp_atomic_model.py) returns list[float] | None. PT's DPAtomicModel inherits from the dpmodel version; its actual return type does not conform to the torch.Tensor | None contract declared here. The isinstance guard added at lines 658–661 acts as a runtime workaround for the model_forward path but does not fix the annotation contract for all callers.

🛠️ Suggested fix — align return type with dpmodel
-    def get_default_chg_spin(self) -> torch.Tensor | None:
+    def get_default_chg_spin(self) -> list[float] | None:
         """Get the default charge_spin values."""
         return None

Any call sites that need a torch.Tensor should convert explicitly (e.g., via to_torch_tensor).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/pt/model/atomic_model/base_atomic_model.py` around lines 200 - 202,
The return type of BaseAtomicModel.get_default_chg_spin is too narrow
(torch.Tensor | None) and conflicts with DPAtomicModel.get_default_chg_spin
which returns list[float] | None; update the type annotation on
BaseAtomicModel.get_default_chg_spin to accept both list[float] and torch.Tensor
(e.g., list[float] | torch.Tensor | None) so it matches DPAtomicModel and other
subclasses, and add a note in callers (e.g., model_forward where the isinstance
guard at lines ~658–661 exists) to convert list->torch.Tensor via a helper like
to_torch_tensor when a torch.Tensor is required.

Comment on lines +389 to +413
@torch.jit.export
def has_chg_spin_ebd(self) -> bool:
"""Check if the model has charge spin embedding."""
return self.add_chg_spin_ebd

@torch.jit.export
def get_dim_chg_spin(self) -> int:
"""Get the dimension of charge_spin input."""
if self.add_chg_spin_ebd:
return self.descriptor.get_dim_chg_spin()
return 0

@torch.jit.export
def has_default_chg_spin(self) -> bool:
"""Check if the model has default charge_spin values."""
if self.add_chg_spin_ebd:
return self.descriptor.has_default_chg_spin()
return False

@torch.jit.export
def get_default_chg_spin(self) -> torch.Tensor | None:
"""Get the default charge_spin values as a tensor."""
if self.add_chg_spin_ebd and self.descriptor.has_default_chg_spin():
return self.descriptor.get_default_chg_spin()
return None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that BaseDescriptor in the PT backend declares the charge_spin query methods
# that DPAtomicModel's `@torch.jit.export` methods call.
# Expect: all four method names to appear in base_descriptor.py.
rg -n "def get_dim_chg_spin\|def has_default_chg_spin\|def get_default_chg_spin\|def has_chg_spin_ebd" \
   deepmd/pt/model/descriptor/base_descriptor.py

# Also scan dpmodel BaseDescriptor for completeness (used by dpmodel backend)
rg -n "def get_dim_chg_spin\|def has_default_chg_spin\|def get_default_chg_spin\|def has_chg_spin_ebd" \
   deepmd/dpmodel/descriptor/base_descriptor.py 2>/dev/null || true

Repository: deepmodeling/deepmd-kit

Length of output: 49


BaseDescriptor must declare the charge-spin query methods called by new @torch.jit.export methods.

All four new @torch.jit.export methods — has_chg_spin_ebd, get_dim_chg_spin, has_default_chg_spin, and get_default_chg_spin — call methods on self.descriptor (typed as BaseDescriptor). TorchScript compiles all code paths statically, so the if self.add_chg_spin_ebd: guards do not prevent compilation. BaseDescriptor must declare get_dim_chg_spin(), has_default_chg_spin(), and get_default_chg_spin() (at minimum as abstract methods or with default implementations). Without these declarations, torch.jit.script() will fail for any model that wraps a DPAtomicModel, breaking the inference path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/pt/model/atomic_model/dp_atomic_model.py` around lines 389 - 413, The
new `@torch.jit.export` methods in DPAtomicModel (has_chg_spin_ebd,
get_dim_chg_spin, has_default_chg_spin, get_default_chg_spin) call into
self.descriptor (BaseDescriptor) so add declarations on the BaseDescriptor class
for get_dim_chg_spin() -> int, has_default_chg_spin() -> bool, and
get_default_chg_spin() -> Optional[torch.Tensor] (or torch.Tensor | None) —
either as abstract methods or as concrete defaults (return 0, False, None) —
using TorchScript-compatible types so torch.jit.script() can compile all code
paths; update BaseDescriptor’s import/type hints to include torch.Tensor if
needed.

Comment on lines +128 to +131
if default_chg_spin is not None:
assert len(default_chg_spin) == 2, (
"default_chg_spin must be a list of length 2 [charge, spin]."
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prefer raise ValueError over assert for public constructor validation.

assert can be silently disabled when Python is run with -O, removing the only guard against a malformed default_chg_spin list.

🛡️ Proposed fix
-        if default_chg_spin is not None:
-            assert len(default_chg_spin) == 2, (
-                "default_chg_spin must be a list of length 2 [charge, spin]."
-            )
+        if default_chg_spin is not None and len(default_chg_spin) != 2:
+            raise ValueError(
+                "default_chg_spin must be a list of length 2 [charge, spin]."
+            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/pt/model/descriptor/dpa3.py` around lines 128 - 131, Replace the
runtime assertion in the constructor that checks default_chg_spin with a proper
exception: in the code block handling default_chg_spin (the conditional around
default_chg_spin is not None in dpa3.py), remove the assert and instead raise a
ValueError with the same message ("default_chg_spin must be a list of length 2
[charge, spin].") so validation cannot be bypassed when Python is run with
optimizations; keep the same check for len(default_chg_spin) == 2 and the
existing message to aid debugging.

Comment on lines +268 to +277
@torch.jit.export
def get_default_chg_spin(self) -> torch.Tensor | None:
"""Get the default charge_spin values as a tensor."""
if self.default_chg_spin is None:
return None
return torch.tensor(
self.default_chg_spin,
dtype=self.prec,
device=env.DEVICE,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does PyTorch TorchScript support PEP 604 union syntax (e.g., torch.Tensor | None) in @torch.jit.export method return type annotations in PyTorch 2.11?

💡 Result:

No, PyTorch TorchScript does not support PEP 604 union syntax (e.g., torch.Tensor | None) in @torch.jit.export method return type annotations in PyTorch 2.11. TorchScript is deprecated and in maintenance mode [1][2], with limited support for new Python features. While basic Union types from typing have been supported since around PyTorch 1.12 [3], and PEP 604 | syntax was requested [4][5] and partially added via PR #109293 around PyTorch 2.2 [4], issues persist in 2025 with union usage, such as unpacking unions [6] and optimization flags [7]. Specific errors like "Expression of type | cannot be used in a type expression" were reported for torch.jit.script [5], and TorchScript's type parsing does not fully handle | in annotations reliably [4]. Examples using torch.Tensor | None or similar in @torch.jit.export methods fail compilation, as evidenced by ongoing GitHub issues post-2.2 [6][7]. Use Optional[torch.Tensor] or Union[torch.Tensor, None] instead for compatibility. PyTorch recommends migrating to torch.export or torch.compile [2].

Citations:


🏁 Script executed:

head -280 deepmd/pt/model/descriptor/dpa3.py | tail -20

Repository: deepmodeling/deepmd-kit

Length of output: 780


Change return type annotation from torch.Tensor | None to Optional[torch.Tensor].

The @torch.jit.export decorator requires Optional[torch.Tensor] from typing for the return type; PyTorch TorchScript does not support PEP 604 union syntax (X | None) in exported method signatures as of PyTorch 2.11. Replace with Optional[torch.Tensor] or Union[torch.Tensor, None] to ensure TorchScript compilation succeeds.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@deepmd/pt/model/descriptor/dpa3.py` around lines 268 - 277, The
get_default_chg_spin method's return annotation uses PEP 604 union syntax
(torch.Tensor | None) which TorchScript/@torch.jit.export doesn't accept; change
the signature of get_default_chg_spin to use typing.Optional[torch.Tensor] (or
typing.Union[torch.Tensor, None]) and add the necessary import for Optional from
typing so the exported method compiles under TorchScript.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants