Skip to content

feat(dpmodel): NeighborGraph foundation — contract, segment toolkit, numpy builder, edge force/virial#5581

Merged
wanghan-iapcm merged 24 commits into
deepmodeling:masterfrom
wanghan-iapcm:feat-dpmodel-neighbor-graph
Jun 25, 2026
Merged

feat(dpmodel): NeighborGraph foundation — contract, segment toolkit, numpy builder, edge force/virial#5581
wanghan-iapcm merged 24 commits into
deepmodeling:masterfrom
wanghan-iapcm:feat-dpmodel-neighbor-graph

Conversation

@wanghan-iapcm

@wanghan-iapcm wanghan-iapcm commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

First, foundational slice of the unified NeighborGraph neighbor-list design (design discussion: wanghan-iapcm#4). This PR adds the backend-agnostic deepmd/dpmodel core only — pure numpy / array-API, no torch/jax/model dependencies — so it is self-contained and not yet wired into any model.

What this adds (deepmd/dpmodel/utils/)

  • neighbor_graph.pyNeighborGraph dataclass (the edge-graph contract: n_node, edge_index (2,E) SoA [src,dst], edge_vec (E,3) = r_src − r_dst, edge_mask, optional n_local/angle_index/angle_mask) and GraphLayout (edge/angle/node/frame capacities + min_edges). Node validity is derived (arange(N) < sum(n_node)), not a stored field; node_validity_mask and pad_and_guard_edges (compact-prefix layout, capacity/guard, overflow) helpers.
  • segment.py — mask-aware, backend-dispatched segment_sum / segment_mean (on xp_add_at).
  • numpy_neighbor_list.pyNumpyNeighborList, an all-pairs reference builder (coords → NeighborGraph, periodic + non-periodic, frame-offset).
  • edge_derivatives.pyedge_force_virial: assembles per-node force, atom virial (attributed full-to-src, the canonical TF==PT convention), and global virial from a per-edge gradient g_e.

Tests

source/tests/common/dpmodel/: 22 tests, all passing — dataclasses, node_validity_mask, segment ops, pad_and_guard_edges, builder correctness (brute-force-verified, periodic + non-periodic + multi-frame + capacity padding), and force/virial assembly (force formula, global virial, full-to-src atom virial summing to global, masked-edge no-op).

Scope / deferred

Numpy-only foundation. Deferred to follow-ups: torch/jax backend dispatch + O(N) vesin/nvalchemiops builders; the forward_common_lower_graph model seam + edge_energy_deriv autograd; descriptor ports; attention segment_softmax + the 3-body angle list; jax static-E_max capacity/bin-packing; C/C++ ABI + LAMMPS + multi-rank.

Known limitations

  • NumpyNeighborList is an O(N²·shells) reference builder with an orthorhombic-shell heuristic — reference/test use; production O(N) builders come later.
  • edge_force_virial consumes a pre-computed g_e; the autograd that produces it is a follow-up.
  • segment_max/segment_softmax not yet implemented (only needed by attention descriptors later).
  • Rectangular multi-frame only; ragged bin-packing is a later (jax) step.

Summary by CodeRabbit

  • New Features
    • Added neighbor-graph utilities for building graphs from inputs, including padding/guard edges and node validity masking.
    • Added segment reduction helpers (sum/mean) with safe handling for empty segments.
    • Added edge-based force and virial assembly from per-edge gradients and geometry.
    • Expanded top-level exports to make these utilities available directly.
  • Bug Fixes
    • Ensured padded/guard edges are fully excluded (contribute zero to forces and virials).
  • Tests
    • Added extensive unit tests covering graph construction, edge/virial behavior (including masking and multi-frame cases), and segment reductions.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new deepmd.dpmodel.utils.neighbor_graph package with graph contracts, builders, reductions, and edge force/virial assembly, then re-exports the new symbols from deepmd.dpmodel.utils. Tests cover construction, padding, builders, reductions, and virial behavior.

Changes

Backend-agnostic neighbor graph utilities

Layer / File(s) Summary
Segment reductions
deepmd/dpmodel/utils/neighbor_graph/segment.py, source/tests/common/dpmodel/test_segment.py
Adds segment_sum and segment_mean, including empty-segment handling and per-shape reduction tests.
Neighbor graph contracts and padding helpers
deepmd/dpmodel/utils/neighbor_graph/graph.py, source/tests/common/dpmodel/test_neighbor_graph.py
Defines NeighborGraph and GraphLayout, plus pad_and_guard_edges and node_validity_mask, with tests for defaults, padding, overflow handling, and exports.
Neighbor graph builder functions
deepmd/dpmodel/utils/neighbor_graph/builder.py, source/tests/common/dpmodel/test_neighbor_graph_builder.py
Adds neighbor_graph_from_extended and build_neighbor_graph, with tests for periodic/non-periodic neighbor sets, padding, offsets, and ghost mapping.
Edge force and virial computation
deepmd/dpmodel/utils/neighbor_graph/derivatives.py, source/tests/common/dpmodel/test_edge_force_virial.py
Adds edge_force_virial and tests its force and virial outputs, including masked-edge and ragged multi-frame behavior.
Package re-exports and public API wiring
deepmd/dpmodel/utils/neighbor_graph/__init__.py, deepmd/dpmodel/utils/__init__.py, source/tests/common/dpmodel/test_neighbor_graph.py
Sets up the package namespace and re-exports the new symbols from deepmd.dpmodel.utils, with importability checks.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant build_neighbor_graph
  participant extend_input_and_build_neighbor_list
  participant neighbor_graph_from_extended
  participant pad_and_guard_edges
  participant edge_force_virial
  participant segment_sum

  Caller->>build_neighbor_graph: coord, atype, box, rcut, sel, layout
  build_neighbor_graph->>extend_input_and_build_neighbor_list: coord, atype, rcut, sel, box
  extend_input_and_build_neighbor_list-->>build_neighbor_graph: extended_coord, nlist, mapping
  build_neighbor_graph->>neighbor_graph_from_extended: extended_coord, nlist, mapping, layout
  neighbor_graph_from_extended->>pad_and_guard_edges: edge_index, edge_vec, capacity
  pad_and_guard_edges-->>neighbor_graph_from_extended: padded edge_index, edge_vec, edge_mask
  neighbor_graph_from_extended-->>build_neighbor_graph: NeighborGraph
  Caller->>edge_force_virial: g_e, edge_vec, edge_index, edge_mask, n_node
  edge_force_virial->>segment_sum: masked aggregation
  segment_sum-->>edge_force_virial: per-node and per-frame reductions
  edge_force_virial-->>Caller: force, atom virial, frame virial
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

Core

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.76% 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 title accurately summarizes the main additions: NeighborGraph contract, segment helpers, builder, and edge force/virial.
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

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 341a4081e4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread deepmd/dpmodel/utils/numpy_neighbor_list.py Outdated
Comment thread deepmd/dpmodel/utils/neighbor_graph/derivatives.py Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
deepmd/dpmodel/utils/numpy_neighbor_list.py (1)

73-79: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Normalize or validate box before frame indexing.

Line 78 slices box with [f] unconditionally. If a caller passes a single-frame (3, 3) box, bx becomes a (3,) row and _frame_edges receives the wrong shape. Accept (3, 3) for nf == 1 or raise a clear shape error before the loop.

Proposed fix
         if layout is None:
             layout = GraphLayout()
         coord = np.asarray(coord, dtype=np.float64)
+        if coord.ndim != 3 or coord.shape[-1] != 3:
+            raise ValueError("coord must have shape (nf, nloc, 3)")
         nf, nloc = coord.shape[0], coord.shape[1]
+        box_arr = None
+        if box is not None:
+            box_arr = np.asarray(box, dtype=np.float64)
+            if box_arr.shape == (3, 3):
+                if nf != 1:
+                    raise ValueError("a single (3, 3) box is only valid for nf == 1")
+                box_arr = box_arr[None, ...]
+            elif box_arr.shape != (nf, 3, 3):
+                raise ValueError("box must have shape (nf, 3, 3)")
         n_node = np.full((nf,), nloc, dtype=np.int64)
         src_all, dst_all, vec_all = [], [], []
         for f in range(nf):
-            bx = None if box is None else np.asarray(box, dtype=np.float64)[f]
+            bx = None if box_arr is None else box_arr[f]
🤖 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/utils/numpy_neighbor_list.py` around lines 73 - 79, The `box`
handling in the numpy neighbor list builder is too strict for single-frame
inputs because `box[f]` is applied unconditionally before calling
`_frame_edges`. Normalize `box` up front so a `(3, 3)` box is accepted when `nf
== 1`, or validate and raise a clear shape error before the frame loop if the
shape does not match the number of frames. Keep the existing `_frame_edges` call
path unchanged once `bx` has the correct per-frame shape.
🤖 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 `@source/tests/common/dpmodel/test_numpy_neighbor_list.py`:
- Around line 86-94: The test in test_static_capacity_padding currently uses a
tautological check on ng.edge_mask.sum() versus the same sliced sum, so it does
not verify padding behavior. Update this assertion to compare the number of true
entries against the expected real-edge count for the NumpyNeighborList build
with GraphLayout(edge_capacity=64), and also verify the masked tail is compactly
padded after the last real edge. Keep the existing NumpyNeighborList, build, and
edge_mask/edge_vec checks, but make the mask assertion explicitly validate that
only the real edges are marked and the remaining suffix is masked out.

---

Nitpick comments:
In `@deepmd/dpmodel/utils/numpy_neighbor_list.py`:
- Around line 73-79: The `box` handling in the numpy neighbor list builder is
too strict for single-frame inputs because `box[f]` is applied unconditionally
before calling `_frame_edges`. Normalize `box` up front so a `(3, 3)` box is
accepted when `nf == 1`, or validate and raise a clear shape error before the
frame loop if the shape does not match the number of frames. Keep the existing
`_frame_edges` call path unchanged once `bx` has the correct per-frame shape.
🪄 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: 8cbbb3f7-390c-412c-932a-8d5fbeddb3cd

📥 Commits

Reviewing files that changed from the base of the PR and between b52c359 and a7e4382.

📒 Files selected for processing (9)
  • deepmd/dpmodel/utils/__init__.py
  • deepmd/dpmodel/utils/edge_derivatives.py
  • deepmd/dpmodel/utils/neighbor_graph.py
  • deepmd/dpmodel/utils/numpy_neighbor_list.py
  • deepmd/dpmodel/utils/segment.py
  • source/tests/common/dpmodel/test_edge_force_virial.py
  • source/tests/common/dpmodel/test_neighbor_graph.py
  • source/tests/common/dpmodel/test_numpy_neighbor_list.py
  • source/tests/common/dpmodel/test_segment.py

Comment thread source/tests/common/dpmodel/test_numpy_neighbor_list.py Outdated
@codecov

codecov Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.30%. Comparing base (b52c359) to head (f32efc8).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5581      +/-   ##
==========================================
+ Coverage   82.27%   82.30%   +0.03%     
==========================================
  Files         882      887       +5     
  Lines      100009   100161     +152     
  Branches     4045     4043       -2     
==========================================
+ Hits        82283    82440     +157     
+ Misses      16267    16263       -4     
+ Partials     1459     1458       -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Han Wang added 2 commits June 24, 2026 19:18
…yNeighborList

Add neighbor_graph_from_extended (quartet -> ghost-free NeighborGraph, src =
mapping[neighbor] local owner) and build_neighbor_graph (reuses the tested
extend_input_and_build_neighbor_list). Drop the all-pairs NumpyNeighborList
from the library; the independent brute-force oracle now lives in the test
file (test_neighbor_graph_builder.py) and cross-validates the adapter.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 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/utils/neighbor_graph.py`:
- Around line 197-215: The public signature of the neighbor-graph builder is too
broad for the upstream helper it delegates to: the function currently advertises
`sel` as `int | list[int]` but passes it unchanged into
`extend_input_and_build_neighbor_list`, which expects `list[int]`. Update the
`neighbor_graph` entrypoint to either normalize an integer `sel` into the list
form before calling `extend_input_and_build_neighbor_list`, or narrow the `sel`
annotation and doc contract to match the helper’s `list[int]` expectation so the
API and implementation stay aligned.
🪄 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: 26df6060-a292-4698-b218-9fa775e3cf1c

📥 Commits

Reviewing files that changed from the base of the PR and between a7e4382 and 28a3627.

📒 Files selected for processing (4)
  • deepmd/dpmodel/utils/__init__.py
  • deepmd/dpmodel/utils/neighbor_graph.py
  • source/tests/common/dpmodel/test_neighbor_graph.py
  • source/tests/common/dpmodel/test_neighbor_graph_builder.py
✅ Files skipped from review due to trivial changes (1)
  • source/tests/common/dpmodel/test_neighbor_graph.py

Comment thread deepmd/dpmodel/utils/neighbor_graph/builder.py Outdated
Han Wang added 2 commits June 24, 2026 19:26
Move the flat utils modules (neighbor_graph.py, segment.py, edge_derivatives.py)
into deepmd/dpmodel/utils/neighbor_graph/ as a cohesive subpackage:
  graph.py (contract + node-validity + edge padding), builder.py
  (neighbor_graph_from_extended, build_neighbor_graph), segment.py, derivatives.py.
Public API re-exported from the package __init__ and from utils/__init__, so
import paths (deepmd.dpmodel.utils.*) are unchanged.
…ng test

Address CodeRabbit review on deepmodeling#5581:
- build_neighbor_graph normalizes an int sel to list form before calling
  extend_input_and_build_neighbor_list (annotated list[int]), so the public
  int | list[int] contract is honored. No behavior change -- the underlying
  build_neighbor_list already accepts int -- but the call is now type-consistent.
  Add a test that int sel reproduces the list-sel real-edge environment.
- test_static_capacity_padding: replace the (already-removed) tautological mask
  check's intent with explicit assertions on the exact real-edge count and the
  compact-prefix padding (real edges first, masked tail after).
Comment thread deepmd/dpmodel/utils/neighbor_graph/builder.py
Comment thread deepmd/dpmodel/utils/neighbor_graph/derivatives.py Outdated
Han Wang added 5 commits June 25, 2026 00:15
…el contract

Address PR deepmodeling#5581 review (iProzd + chatgpt-codex):
- edge_force_virial now returns PER-FRAME virial (nf,3,3) instead of a single
  (3,3) that collapsed all frames; takes n_node (per-frame counts), assigns each
  edge to its dst's frame via searchsorted(cumsum(n_node)). Adds a multi-frame
  regression test (the bug was invisible to the single-frame UT).
- builder.py: document the LEGACY-COMPATIBLE / sel-truncating contract of
  neighbor_graph_from_extended / build_neighbor_graph explicitly; the carry-all
  graph-native builder is separate (from_ijs via ASE/vesin).
…ding)

Address review follow-up: the per-frame-virial bug slipped through because the
only test was single-frame. Generalize coverage:
- edge_force_virial: add node_capacity for a padded node axis; new test exercises
  the MOST GENERAL layout (ragged n_node=[3,5], uneven per-frame edges, masked
  guard edges, padded node axis) + per-frame virial/atom-virial closure.
- builder: strengthen multi-frame test to TWO DIFFERENT frames checked per-frame
  vs the brute-force oracle (non-periodic + periodic); add multi-frame adapter
  test. (Builder is rectangular-node by construction; ragged-node building is a
  future ragged builder.)
…ce_virial tests

The ragged+padded general test subsumes the square multi-frame and the
single-frame per-frame/edge-padding cases. Keep two minimal convention-pinning
tests the general test cannot assert: force dst-src sign, and full-to-src
attribution (decision deepmodeling#5 — the general closure is attribution-agnostic).
n_node=[3,0,5] with an empty middle frame: verifies searchsorted frame
assignment on duplicate cumsum boundaries [3,3,8] skips the zero-width block
(node 3 -> frame 2) and the empty frame's virial is exactly zero. Combined with
a padded node axis (node_capacity 9 > sum 8) to also exercise node padding at
the primitive level (the only node_capacity consumer in this PR; a model-level
caller arrives with the graph lower in PR-A/jax in PR-F).
…virial

Single- and multi-frame systems with nodes but NO real edges (only masked guard
edges): force/atom_virial/per-frame virial must all be exactly zero with correct
shapes. Covers isolated-atom / rcut-below-all-distances geometries.
@wanghan-iapcm wanghan-iapcm requested a review from iProzd June 24, 2026 16:35

@iProzd iProzd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM now.

Han Wang added 6 commits June 25, 2026 08:47
…onverter to from_dense_quartet

build_neighbor_graph now SEARCHES neighbors directly from coordinates and
keeps every neighbor within rcut (sel-free), instead of routing through the
sel-truncating dense nlist. The legacy quartet adapter is renamed
neighbor_graph_from_extended -> from_dense_quartet and documented as the
backward-compat converter (inherits sel truncation), separating the
'compute from geometry' dispatcher from the 'adapt an existing list'
converters (spec decision deepmodeling#17).
…tet over frames

Replace the per-frame Python loop in both the carry-all search and the
quartet converter with flat (frame, center, neighbor) index grids and
cross-frame gathers via (frame * nall + idx) flat indices. No behavior
change (edges stay frame-major); tests unchanged and green.
A pair that is a neighbor ONLY across the boundary (direct distance > rcut,
minimum image < rcut): build_neighbor_graph must find the image edge with the
right vector, find nothing without the box, and agree with the brute-force
oracle. Existing periodic test had the pair as a direct neighbor too, so it
did not isolate periodic-image handling.
Run build_neighbor_graph and from_dense_quartet on torch.from_numpy inputs
(periodic + non-periodic) and assert the neighbor environment matches the
numpy result and the output stays in the torch namespace. Guards the
vectorized broadcasting/gather against numpy-only ops that break torch/jax.
Remove the shallow 'smoke' test; torch-namespace behavior of the graph
builders is covered properly by PR-A's cross-backend consistency harness
(full graph forward under pt_expt/torch), not a standalone smoke check.
@wanghan-iapcm wanghan-iapcm enabled auto-merge June 25, 2026 01:53

@OutisLi OutisLi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @wanghan-iapcm — clean, self-contained foundation, and the test coverage is excellent (force sign, the full-to-src ↔ per-frame virial closure, empty/ragged frames, padding/guard edges, and the cross-PBC-image case). I re-derived the force/virial assembly independently and it checks out. A few non-blocking points inline. The only one I'd suggest addressing before merge is the dangling memory/spec_unified_edge_nlist.md references, since they'll be confusing for other maintainers.

Comment thread deepmd/dpmodel/utils/neighbor_graph/__init__.py
Comment thread deepmd/dpmodel/utils/neighbor_graph/derivatives.py
Comment thread deepmd/dpmodel/utils/neighbor_graph/derivatives.py
@wanghan-iapcm wanghan-iapcm added this pull request to the merge queue Jun 25, 2026
Merged via the queue into deepmodeling:master with commit 99523dc Jun 25, 2026
70 checks passed
@wanghan-iapcm wanghan-iapcm deleted the feat-dpmodel-neighbor-graph branch June 25, 2026 07:59
github-merge-queue Bot pushed a commit that referenced this pull request Jun 29, 2026
…5583)

## Summary

Adds the graph-native forward path for `dpa1(attn_layer=0)` (the
factorizable, mixed-types case), built on the `NeighborGraph` foundation
from #5581. Geometry enters the descriptor only through per-edge
`edge_vec`; the neighbor-axis reduction becomes a `segment_sum` over
edge centers. For `pt_expt` this becomes the **default** forward
(force/virial via a single autograd backward through `edge_vec`).

## What it adds

- **dpmodel**: `edge_env_mat` (per-edge env-mat 4-vector),
`DescrptBlockSeAtten._call_graph` + `DescrptDPA1.call_graph`, model
`call_lower_graph` (energy), `neighbor_graph_from_ijs` + an optional
**ASE** O(N) carry-all builder.
- **pt_expt**: `edge_energy_deriv` (autograd `grad(E, edge_vec)` →
`edge_force_virial`) + `forward_common_lower_graph` (energy + force +
virial + atom_virial).
- The dense `DescrptDPA1.call` becomes a thin adapter
(`from_dense_quartet → call_graph`) preserving the 5-tuple ABI; a
**shape-static** converter keeps it `jax.jit` /
`torch.export`-traceable.

## Default behavior

- **pt_expt** defaults graph-eligible `dpa1(attn_layer=0, concat tebd,
no exclude_types)` models to the carry-all graph (it has the autograd
force/virial path).
- **dpmodel/jax** keep the dense default (they compute force/virial
analytically; the graph lower is energy-only), and **agree with pt_expt
at non-binding `sel`**.
- Ineligible configs (attention, strip tebd, `exclude_types`,
linear/ZBL) fall back to the dense path unchanged.
`neighbor_graph_method="legacy"` forces dense; `"dense"`/`"ase"` force
the graph.

## Parity (graph vs legacy dense lower, fp64 CPU)

| | energy | force | virial | atom_virial |
|---|---|---|---|---|
| max abs diff | 0 | ~1e-19 | ~1e-18 | **~1e-18** |

atom_virial matches the canonical TF==pt-legacy full-to-src convention.
dpa1 descriptor + model consistency suites green across dp/jax/pt_expt.

## Known limitations

- Default-flip is **pt_expt-only**; full carry-all default for dp/jax
needs analytical/jax graph force (follow-up).
- `make_fx` (forward + grad) traces; **full `.pt2` AOTI export is a
follow-up** (PR-B). The carry-all builders
(`build_neighbor_graph`/`from_ijs`) still use `nonzero` (eager-only);
their static variants land with the export PR.
- Single-rank only; CUDA unvalidated (CPU box); ASE is opt-in O(N)
(vesin O(N) is a follow-up); no jax graph force / dpa2-3 message-passing
yet.

Also folds in three follow-up fixes to the #5581 foundation from
@OutisLi's review (dangling spec refs → design discussion,
`edge_force_virial` jax int-sum short-circuit, `Array` typing).


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added graph-native “lowering” for DPA1 when compatible, including
graph-native descriptor/forward execution and graph-native
descriptor→model output conversion.
* Introduced opt-in `neighbor_graph_method` routing for
energy/force/virial, with carry-all neighbor graphs and graph-output
fitting/post-processing.
* Added new neighbor-graph utilities (including ASE-based carry-all
building, `(i,j,S)` conversion, and per-edge environment-matrix
computation), exported as part of the public API.
* **Bug Fixes**
* Improved stability for masked/padded edges, virtual atom handling, and
parameter protection consistency; refined traced virial assembly when
node-capacity is used.
* **Tests**
* Expanded parity/regression suites for graph lowering,
energy/force/virial, conversion correctness, ragged graphs, and FX
tracing.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Han Wang <wang_han@iapcm.ac.cn>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
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