Skip to content

feat(dpmodel): NeighborGraph 3-body angle machinery (PR-E)#5717

Merged
OutisLi merged 14 commits into
deepmodeling:masterfrom
wanghan-iapcm:feat-graph-angles-prE
Jul 9, 2026
Merged

feat(dpmodel): NeighborGraph 3-body angle machinery (PR-E)#5717
OutisLi merged 14 commits into
deepmodeling:masterfrom
wanghan-iapcm:feat-graph-angles-prE

Conversation

@wanghan-iapcm

@wanghan-iapcm wanghan-iapcm commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

NeighborGraph PR-E: the optional 3-body angle extension of the edge-graph neighbor-list contract (design discussion wanghan-iapcm#4). An angle is a pair of edges sharing a center (dst(edge_a) == dst(edge_b)), stored as angle_index (2, A) into [0, E) + angle_mask (A,)edge_vec stays the ONLY geometry leaf, so force/virial assembly is untouched (proven by an invariance test).

Stacked on #5715 (PR-D) — reuses its center_edge_pairs. Please merge #5715 first; this branch then rebases clean onto master. Only the last 10 commits belong to this PR.

What's added (all in deepmd/dpmodel/utils/neighbor_graph/)

  • pad_and_guard_angles (graph.py) — angle-axis padder mirroring pad_and_guard_edges (dynamic guard append / static angle_capacity with overflow ValueError).
  • angles.py (new):
    • build_angle_index(edge_index, edge_vec, edge_mask, n_total, a_rcut, *, ordered=False, include_self=False, layout=None) — unordered, no-self pairs of edges sharing a center where BOTH edges are within a_rcut; built on PR-D's center_edge_pairs; pair_mask folded into angle_mask (never discarded).
    • attach_angles(graph, a_rcut, ...) — post-hoc: edge graph in, graph with angle fields out (dataclasses.replace); default builders keep angles None.
    • angle_to_edge_sum / angle_to_node_sum — segment-sum aggregation to the query edge / shared center.
    • graph_angle_cos(angle_index, edge_vec, eps=1e-6) — per-angle cos θ mirroring dpa3 repflows cosine_ij eps placement exactly (+eps in norm denominators, *(1-eps) on the product).
    • angle_padding_fraction(graph) — mask-derived padding-waste report for static capacities.

Semantics / decisions

  • Unordered, no-self by default: dpa3's dense angle tensor is the redundant ordered a_sel x a_sel square including the j==k diagonal; the graph set keeps one entry per unordered {j,k} pair and moves the degenerate diagonal to the (a_rcut-filtered) edge channel. Dense parity is therefore asserted against the OFF-DIAGONAL cosine_ij[j,k] (j != k) at rtol/atol 1e-12 (same-math fp64), at non-binding a_sel. The ordered+self full square stays available via flags.
  • a_sel = normalization-only (carry-all within a_rcut), consistent with the edge-sel decision.
  • Second oracle: se_t dot-product convention cross-checked from coordinates in the sw == 1 regime (rtol 1e-12).

Tests

source/tests/common/dpmodel/test_angle_builder.py (21) + test_graph_angle_cos_parity.py (6): brute-force triplet oracle (all flag combinations, multi-center, static layout, node_capacity branch), dpa3 dense-parity + no-self-angle assertion, se_t coordinate oracle, force/virial bit-exact invariance with/without angles, padding-fraction (incl. total==0), torch-namespace smoke tests for every new function. Full neighbor-graph suite: 54 passed.

Known limitations

  • Machinery + angle-channel math only — no dpa3 graph descriptor here (dpa3 is message-passing; wiring = PR-G). se_t/se_t_tebd are not migrated (mixed_types=False), used as oracle only.
  • Angle enumeration is the compact eager form (nonzero in center_edge_pairs) even when a static layout is passed — angle_capacity fixes the output shape only; shape-static enumeration for export is deferred to PR-G.
  • Not bit-parity with dense dpa3 by construction (unordered/no-self reformulation; recoverable in PR-G via symmetric angle→edge 2x + edge-channel diagonal).
  • Aggregation helpers follow the mask-then-reduce convention: callers mask per-angle data by angle_mask before summing (padding angles point at edge 0).
  • numpy/torch validated; jax rides the array-API surface (no jax-specific test here); A ~ sum(deg^2) capacity overhead mitigated by a_rcut < rcut and reported by angle_padding_fraction.

Summary by CodeRabbit

  • New Features

    • Added angle-graph utilities to build and attach 3-body angle relationships on top of neighbor graphs.
    • Introduced angle padding/guarding for fixed-capacity layouts.
    • Added helpers to compute angle cosine values, reduce angle data back to edges/nodes, and report angle padding coverage.
    • Expanded publicly available exports for angle/edge-pair utilities and additional segment reductions (max/softmax).
  • Tests

    • Added extensive unit tests covering index building, attachment, masking/aggregation correctness, padding behavior, and cosine parity across NumPy/Torch.

@dosubot dosubot Bot added the new feature label Jul 3, 2026
@github-actions github-actions Bot added the Python label Jul 3, 2026
@coderabbitai

coderabbitai Bot commented Jul 3, 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

This PR adds angle-graph support to NeighborGraph: angle padding/guarding, angle-index construction and attachment, cosine and aggregation helpers, package re-exports, and unit/parity tests.

Changes

Angle graph construction and aggregation

Layer / File(s) Summary
Angle padding/guard primitive
deepmd/dpmodel/utils/neighbor_graph/graph.py, source/tests/common/dpmodel/test_angle_builder.py
pad_and_guard_angles pads a real angle-index prefix with a dummy suffix, producing a boolean mask, supporting dynamic (min_angles) and static (angle_capacity) sizing with overflow checks.
Angle index construction and graph attachment
deepmd/dpmodel/utils/neighbor_graph/angles.py, source/tests/common/dpmodel/test_angle_builder.py
build_angle_index derives cutoff-gated, optionally ordered/self-inclusive edge-pair angle indices via center_edge_pairs and pad_and_guard_angles; attach_angles wires these into a NeighborGraph while preserving other fields; tests cover oracle matching, torch compatibility, multi-center behavior, and layout sizing.
Angle cosine and aggregation utilities
deepmd/dpmodel/utils/neighbor_graph/angles.py, deepmd/dpmodel/utils/neighbor_graph/__init__.py, source/tests/common/dpmodel/test_angle_builder.py, source/tests/common/dpmodel/test_graph_angle_cos_parity.py
graph_angle_cos computes epsilon-stabilized per-angle cosines; angle_to_edge_sum/angle_to_node_sum aggregate per-angle data via segment_sum; angle_padding_fraction reports padding stats; new symbols are re-exported in package __all__; parity tests cover geometry, dense-oracle matching, and torch/numpy agreement.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Possibly related PRs

  • deepmodeling/deepmd-kit#5581: Extends the same NeighborGraph foundation by building on the graph/segment/padding contract used here.
  • deepmodeling/deepmd-kit#5715: Uses the re-exported center_edge_pairs, segment_max, and segment_softmax additions from the same neighbor-graph module surface.

Suggested labels: enhancement

Suggested reviewers: OutisLi, iProzd

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding NeighborGraph 3-body angle machinery in dpmodel.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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.

Comment thread source/tests/common/dpmodel/test_angle_builder.py Fixed
Comment thread source/tests/common/dpmodel/test_angle_builder.py Fixed
Comment thread source/tests/common/dpmodel/test_graph_angle_cos_parity.py Fixed

@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)
source/tests/common/dpmodel/test_angle_builder.py (1)

326-364: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a padded-angle_index case to the aggregation tests.

Both aggregation tests here only use angle data where every slot is real (no padding). Given the padding-mask precondition gap noted in angles.py (angle_to_edge_sum/angle_to_node_sum), a test with a padded angle_index and non-zero padding data values would catch regressions if that precondition is ever violated.

🤖 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/common/dpmodel/test_angle_builder.py` around lines 326 - 364,
The aggregation tests in test_angle_builder only cover fully real angle slots,
so add a new padded-angle case for angle_to_edge_sum and angle_to_node_sum using
a padded angle_index with non-zero data in the padded positions. Reuse the
existing test_angle_aggregation and test_angle_aggregation_torch_namespace
patterns, but assert that padding is ignored in both the numpy and
torch-namespace paths so regressions around the padding-mask precondition are
caught.
🤖 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/segment.py`:
- Around line 59-89: The masked softmax in segment_softmax can overflow because
shifted is computed from raw data instead of data_for_max, allowing masked
entries to produce inf and then nan when multiplied by the zero mask. Update
segment_softmax to base the subtraction on data_for_max so masked elements
remain -inf before exp, keeping segment_sum and the denom_e guard from being
poisoned; use the existing segment_max, segment_sum, and mask handling paths to
locate the fix.

---

Nitpick comments:
In `@source/tests/common/dpmodel/test_angle_builder.py`:
- Around line 326-364: The aggregation tests in test_angle_builder only cover
fully real angle slots, so add a new padded-angle case for angle_to_edge_sum and
angle_to_node_sum using a padded angle_index with non-zero data in the padded
positions. Reuse the existing test_angle_aggregation and
test_angle_aggregation_torch_namespace patterns, but assert that padding is
ignored in both the numpy and torch-namespace paths so regressions around the
padding-mask precondition are caught.
🪄 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: b038075f-6809-4579-8ce1-5cf5d1d41c90

📥 Commits

Reviewing files that changed from the base of the PR and between dd38b35 and 19f0cf1.

📒 Files selected for processing (17)
  • deepmd/dpmodel/descriptor/dpa1.py
  • deepmd/dpmodel/utils/neighbor_graph/__init__.py
  • deepmd/dpmodel/utils/neighbor_graph/angles.py
  • deepmd/dpmodel/utils/neighbor_graph/env.py
  • deepmd/dpmodel/utils/neighbor_graph/graph.py
  • deepmd/dpmodel/utils/neighbor_graph/pairs.py
  • deepmd/dpmodel/utils/neighbor_graph/segment.py
  • source/tests/common/dpmodel/test_angle_builder.py
  • source/tests/common/dpmodel/test_center_edge_pairs.py
  • source/tests/common/dpmodel/test_dpa1_call_graph_block.py
  • source/tests/common/dpmodel/test_dpa1_graph_attention_parity.py
  • source/tests/common/dpmodel/test_graph_angle_cos_parity.py
  • source/tests/common/dpmodel/test_segment_softmax.py
  • source/tests/pt_expt/descriptor/test_dpa1.py
  • source/tests/pt_expt/model/test_dpa1_graph_lower.py
  • source/tests/pt_expt/model/test_linear_model.py
  • source/tests/pt_expt/utils/test_neighbor_list.py

Comment thread deepmd/dpmodel/utils/neighbor_graph/segment.py
@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.50%. Comparing base (b95f21e) to head (10e42a3).
⚠️ Report is 5 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5717      +/-   ##
==========================================
- Coverage   79.61%   79.50%   -0.11%     
==========================================
  Files        1014     1015       +1     
  Lines      115359   115483     +124     
  Branches     4272     4274       +2     
==========================================
- Hits        91839    91813      -26     
- Misses      21978    22127     +149     
- Partials     1542     1543       +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.

@wanghan-iapcm wanghan-iapcm force-pushed the feat-graph-angles-prE branch from ad73de0 to dd58c02 Compare July 7, 2026 08:13
- Remove unused locals flagged by CodeQL (ai_def/ai_full in the
  ordered-superset test, ev in the angle-cos parity test); strengthen the
  ordered+include_self test to assert the default pairs are an actual
  subset of the full pair set (using the previously-dead ai_def/ai_full).
- Add masked-padding aggregation tests (CodeRabbit nitpick): pin the
  precondition that angle_to_edge_sum/angle_to_node_sum ignore guard
  angles only once data is zeroed via angle_mask, in numpy and torch.
@wanghan-iapcm wanghan-iapcm requested review from OutisLi and iProzd July 7, 2026 10:49
@wanghan-iapcm wanghan-iapcm added the Test CUDA Trigger test CUDA workflow label Jul 7, 2026
@github-actions github-actions Bot removed the Test CUDA Trigger test CUDA workflow label Jul 7, 2026
Comment thread deepmd/dpmodel/utils/neighbor_graph/angles.py
Comment thread deepmd/dpmodel/utils/neighbor_graph/angles.py
…nd mask precondition

- Document that build_angle_index's strict '<' a_rcut gate intentionally
  mirrors dpa3's dense angle gate (repflows.py:598 a_dist_mask), unlike the
  edge channel's '<= rcut' (builder.py:284). Answers iProzd's review
  question: yes, boundary divergence is intentional dpa3 parity.
- Document the angle_to_edge_sum/angle_to_node_sum 'caller must zero data
  at padded slots first' contract directly in their docstrings (previously
  only pinned by a test, per iProzd's second review comment).
@wanghan-iapcm wanghan-iapcm requested a review from iProzd July 8, 2026 03:31

@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 for the careful machinery and the thorough oracle/parity tests. The forward parity is solid; my one change request is about autograd safety of the norm used in graph_angle_cos (details inline). A couple of minor, non-blocking notes will follow separately.

Comment thread deepmd/dpmodel/utils/neighbor_graph/angles.py Outdated

@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.

One non-blocking performance follow-up on the unordered angle enumeration (details inline). The change itself lands in the shared center_edge_pairs primitive.

Comment thread deepmd/dpmodel/utils/neighbor_graph/angles.py

@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.

One more non-blocking readability note on the pair_mask fold (inline).

Comment thread deepmd/dpmodel/utils/neighbor_graph/angles.py
Address OutisLi's blocking review on PR deepmodeling#5717. graph_angle_cos normalized
with xp.linalg.vector_norm, but the dpa3 dense channel it mirrors uses
safe_for_vector_norm for the same step (repflows.py:642-643), and the a_rcut
gate mirrors repflows.py:598 which also uses safe_for_vector_norm.

The two are value-identical for non-zero vectors (fp64 dense/se_t parity still
passes at 1e-12), but differ in gradient at ||v||==0: plain vector_norm
back-props NaN under jax, whereas safe_for_vector_norm yields a 0 gradient.
edge_vec is the sole autograd leaf, so this was a latent NaN-gradient landmine
on the geometry path (not yet triggered: padding angles point at a real,
non-zero edge, and no gradients flow until the dpa3 graph descriptor lands in
PR-G). Switching both call sites removes the landmine and makes the
'mirrors dpa3 exactly' docstring claims literally true.

Add test_graph_angle_cos_zero_edge_grad_is_finite: a jax-autograd test with a
zero-length edge_vec asserting the geometry-path gradient stays finite, plus a
discrimination check that plain vector_norm DOES produce a NaN gradient under
jax on the same construction (torch's vector_norm is already safe at zero, so
jax is the discriminating backend). Guarded by pytest.importorskip('jax').
@iProzd iProzd enabled auto-merge July 8, 2026 07:49
@iProzd iProzd disabled auto-merge July 8, 2026 07:49
@wanghan-iapcm wanghan-iapcm requested a review from OutisLi July 9, 2026 10:27
@OutisLi OutisLi added this pull request to the merge queue Jul 9, 2026
Merged via the queue into deepmodeling:master with commit f73de32 Jul 9, 2026
57 checks passed
HydrogenSulfate pushed a commit to HydrogenSulfate/deepmd-kit that referenced this pull request Jul 10, 2026
deepmodeling#5728)

## Summary

`source/tests/pt_expt/test_plugin.py` (from deepmodeling#5559) pops `deepmd.pt_expt`
from `sys.modules` without restoring it, leaving the package's cached
submodules bound to a dead parent. Any later import of a cached
submodule (e.g. `deepmd.pt_expt.infer.deep_eval`) re-creates a BARE
parent package whose `utils`/`infer` attributes are never rebound, and
`mock.patch("deepmd.pt_expt.utils...")` in
`test_deep_eval_serialize_api.py` then fails with `AttributeError:
module 'deepmd.pt_expt' has no attribute 'utils'` under py3.10's mock
target resolution (py3.13 tolerates it).

The failure is shard-order dependent: it needs (1) something to import
`deepmd.pt_expt.infer.deep_eval` before `test_plugin` runs, and (2) the
serialize-API test to run after — so it appears/disappears as PRs add
test files and reshuffle the duration-based shards. It currently fails
`Test Python (4, 3.10)` on deepmodeling#5714 and `Test Python (8, 3.10)` on deepmodeling#5717
while master stays green by ordering luck.

## Fix

Snapshot the whole `deepmd.pt_expt.*` module tree before the re-import
and restore it (including the `deepmd` parent-package attribute binding)
in the `finally`.

Verified: fixed test passes together with the serialize-API tests; an
inline emulation of the worst-case ordering (child cached →
pop/reimport/pop → `mock.patch` target resolution) resolves cleanly.

Same one-file fix is cherry-picked on deepmodeling#5714 (as 6422007) and deepmodeling#5717;
whichever lands first, the others resolve trivially.

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

* **Tests**
* Improved import handling in the plugin loading test to better isolate
module state between test runs.
* Reduced the chance of leftover cached imports affecting later tests in
the same session.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Han Wang <wang_han@iapcm.ac.cn>
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.

4 participants