Skip to content

feat(pt_expt): pluggable NeighborList strategy + O(N) vesin neighbor list for Python/ASE inference#5491

Open
wanghan-iapcm wants to merge 7 commits into
deepmodeling:masterfrom
wanghan-iapcm:feat-pluggable-nlist-vesin
Open

feat(pt_expt): pluggable NeighborList strategy + O(N) vesin neighbor list for Python/ASE inference#5491
wanghan-iapcm wants to merge 7 commits into
deepmodeling:masterfrom
wanghan-iapcm:feat-pluggable-nlist-vesin

Conversation

@wanghan-iapcm
Copy link
Copy Markdown
Collaborator

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

Motivation

When deepmd consumes a neighbor list, forward_common/call_common extends the local region into ~26 periodic-image buffer regions — extend_coord_with_ghosts then a dense build_neighbor_list — materializing ≈27×N ghost atoms and an O(N²) [N, 27N] distance matrix. This is the Python/ASE front-end bottleneck at large N (DPA4 manuscript §2.4).

What this PR does

Makes neighbor-list construction pluggable via an optional NeighborList strategy injected at forward_common/call_common (the layer where the system is extended). The exported forward_common_lower (the .pt2/AOTI/C++ entry) is left untouched, so there is zero export risk.

  • dpmodel (torch-free core)NeighborList base + DefaultNeighborList (the historical dense extend+build). neighbor_list=None reproduces today's behavior byte-identically.
  • pt_exptVesinNeighborList, a device-aware vesin.torch O(N) cell list: it runs on the input tensor's device (CPU or CUDA) for torch, and is CPU-bridged for numpy/dpmodel. It builds an (i, j, S) edge list, materializes only the real-neighbor ghosts coord[j] + S@box, and emits the same extended quartet (extended_coord, extended_atype, nlist, mapping). Because the representation is identical, force / global-virial / atomic-virial all come out of the existing autograd + communicate_extended_output routines unchanged.
  • inferencenlist_backend="auto" | "vesin" | "native" on the pt_expt DeepEval and the ASE DP calculator. auto uses vesin when available/applicable and silently falls back to native otherwise; vesin is strict (raises if unavailable, or for spin / ASE-neighbor_list conflicts); native forces the dense builder.
  • pyproject — depends on vesin[torch].

Verification

native vs vesin agree to fp round-off (energy 0.0, force/virial/atomic-virial ≤ ~1e-18, the only difference being ghost-enumeration order):

  • source/tests/pt_expt/utils/test_neighbor_list.py — builder equivalence (numpy + torch namespaces, PBC/non-PBC, input-device placement) and full model equivalence across 8 descriptor families (se_e2_a, se_r, se_e3, dpa1, se_atten_v2, dpa2, dpa3, hybrid) for dpmodel (energy/atomic-energy) and pt_expt (energy/force/virial/atomic-virial), plus the neighbor_list=None byte-identical fallback.
  • source/tests/pt_expt/infer/test_deep_eval.py::TestDeepEvalNlistBackendnlist_backend dispatch validation and vesin-vs-native equality through the compiled .pte.

Known limitations

  • Python forward_common path only. This is the path where deepmd builds the nlist (DeepPot / ASE). C/C++/LAMMPS enter at the exported forward_lower with an externally-supplied list — accepting (i,j,S) there is a planned follow-up.
  • Energy model validated; spin is gated off vesin; dipole/polar/dos/hessian/multi-task and fparam/aparam are not yet covered by vesin equivalence tests (the seam is model-agnostic and the dense default path is regression-tested).
  • Still materializes O(surface) ghost coords (the minimal extended array); a truly buffer-free / sparse-edge-list consumption is deferred. In dense descriptors the env-mat per-edge tensors dominate memory anyway.
  • No real-GPU CI (the device test runs on CPU); ghosts are not deduped (one per S≠0 edge — correctness preserved via mapping summation).
  • Multi-frame vesin build and the neighbor-truncation (sel-exceeded) path are not yet directly tested.

Summary by CodeRabbit

  • New Features

    • Pluggable neighbor-list backend for calculators and evaluators: "auto" (default), "native", or "vesin"; preserves historical all‑pairs behavior when None.
    • Adds a Vesin-based O(N) neighbor-list option for faster neighbor construction and an explicit Default (all‑pairs) builder.
  • Tests

    • Comprehensive test suites validating backend selection, error handling, device/shape robustness, and numeric equivalence (native vs vesin), including multi-frame cases.
  • Chores

    • Adds vesin[torch] runtime entry for the new backend.

Han Wang added 2 commits June 3, 2026 15:39
…uilder

Make neighbor-list construction pluggable via an optional `NeighborList`
strategy injected at `forward_common`/`call_common` (the layer where the
system is extended), keeping the exported `forward_common_lower` untouched.

- dpmodel (torch-free core): `NeighborList` base + `DefaultNeighborList`
  (the historical dense extend+build). `neighbor_list=None` reproduces the
  current behavior byte-identically.
- pt_expt: `VesinNeighborList`, a device-aware `vesin.torch` O(N) cell list
  (on the input device for torch; CPU-bridged for numpy/dpmodel). It emits
  the same extended quartet, so force/global-virial/atomic-virial all come
  out of the existing autograd routines unchanged.
- inference: `nlist_backend="auto"|"vesin"|"native"` on the pt_expt DeepEval
  and the ASE calculator; `auto` falls back to native when vesin is
  unavailable/unsupported, `vesin` is strict.
- pyproject: depend on `vesin[torch]`.

Tested: builder equivalence (numpy+torch, PBC/noPBC, device) and full model
equivalence across 8 descriptor families (energy/force/virial/atomic virial)
vs the dense builder, plus nlist_backend dispatch and vesin-vs-native equality
through the compiled .pte.
…nce tests

Replace the dynamic setattr metaprogramming over (descriptor family, pbc) with
pytest.mark.parametrize, per project test conventions: one parametrized test
per (family, periodic) so cases can be selected individually with -k and
failures pinpoint the family. No coverage change (44 cases).
Copy link
Copy Markdown

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

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: 6fd4ee5799

ℹ️ 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/pt_expt/infer/deep_eval.py Outdated
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 3, 2026

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 pluggable NeighborList interface with DefaultNeighborList and VesinNeighborList, threads an nlist_backend selector through dpmodel/pt_expt/pt and DP, adds vesin[torch] as a dependency, and provides dispatch and numerical-equivalence tests covering single- and multi-frame, periodic and non-periodic cases.

Changes

Pluggable neighbor-list strategy with Vesin O(N) acceleration

Layer / File(s) Summary
Neighbor-list strategy interface and default implementation
deepmd/dpmodel/utils/neighbor_list.py, deepmd/dpmodel/utils/default_neighbor_list.py, deepmd/dpmodel/utils/__init__.py
Introduces NeighborList base class and DefaultNeighborList (all-pairs builder matching historical behavior); both are exported from dpmodel.utils.
Neighbor-list parameter in dpmodel inference
deepmd/dpmodel/model/make_model.py, deepmd/dpmodel/model/ener_model.py
model_call_from_call_lower and CM.call_common now accept optional neighbor_list; neighbor-list construction delegates to the injected builder or DefaultNeighborList() when None. EnergyModel.call forwards the parameter downstream.
Vesin-backed O(N) neighbor-list
deepmd/pt_expt/utils/vesin_neighbor_list.py
Implements VesinNeighborList using vesin.torch cell lists with per-frame builds, ghost reconstruction, remapping, candidate filtering/sorting, padding, and numpy/torch return handling.
PT-Expt DeepEval backend selection and forwarding
deepmd/pt_expt/infer/deep_eval.py, deepmd/pt_expt/model/ener_model.py
Adds nlist_backend parameter (auto/native/vesin) to DeepEval.__init__, validates/configures Vesin selection, and forwards neighbor_list through model forward paths.
PT DeepEval vesin integration and mapping
deepmd/pt/infer/deep_eval.py
Adds vesin imports, _setup_nlist_backend, _eval_lower_vesin path that builds extended neighbor data, calls forward_common_lower, and maps extended outputs back to local atoms.
High-level calculator API and dependency
deepmd/calculator.py, pyproject.toml
DP.__init__ gains nlist_backend parameter and is forwarded to DeepPot; vesin[torch] is added to runtime dependencies.
Dispatch and equivalence tests
source/tests/pt_expt/infer/test_deep_eval.py, source/tests/pt_expt/utils/test_neighbor_list.py, source/tests/pt/model/test_nlist_backend.py
Adds tests for auto-selection, invalid/unavailable backends, builder equivalence (Default vs Vesin), device placement, fallback behavior, and numerical equivalence across dpmodel, pt_expt, and pt for single-frame and multi-frame inputs.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

Core

Suggested reviewers

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

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% 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 clearly and specifically describes the main change: introducing a pluggable NeighborList strategy and adding O(N) vesin neighbor list support for Python/ASE inference.
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 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: 2

🤖 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/pt_expt/infer/deep_eval.py`:
- Around line 885-889: The vesin path returns the raw candidate neighbor list
from self._nlist_builder.build(...) without applying the same type-splitting
used by the native path, so modify the branch that handles getattr(self,
"_nlist_builder", None) to post-process the returned candidate nlist with the
same type-distinguishing logic used by build_neighbor_list(...,
distinguish_types=not mixed_types) before returning or handing it to
forward_common_lower; specifically, call the type-split routine (the logic that
enforces distinguish_types / splits by atom type) on the result of
self._nlist_builder.build(coords, atom_types, cells, rcut, sel) (or convert it
into the same structure expected by forward_common_lower) so non-mixed-type
models receive an identical nlist contract as the native forward_common_lower
path.

In `@deepmd/pt_expt/utils/vesin_neighbor_list.py`:
- Around line 192-211: _build_single() currently truncates candidates to the
global nsel nearest neighbors (variables dense_idx, sorted_idx, nlist, keep =
min(nsel, max_nn)), which can drop needed neighbors of other types before
per-type selection (nlist_distinguish_types). Change this so you do not limit to
nsel here: keep all in-cutoff candidates (i.e., all entries with valid &= dists
<= rcut) up to max_nn and populate nlist with that superset (use max_nn or the
count of valid candidates instead of nsel when filling nlist), then defer the
final per-type truncation to nlist_distinguish_types; ensure you still pad with
-1 for missing entries and preserve the existing sorting by distance
(sorted_idx, sorted_valid).
🪄 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: aea2c30e-6a44-475f-8059-64e03b3a5b0f

📥 Commits

Reviewing files that changed from the base of the PR and between 27a18b6 and 6fd4ee5.

📒 Files selected for processing (12)
  • deepmd/calculator.py
  • deepmd/dpmodel/model/ener_model.py
  • deepmd/dpmodel/model/make_model.py
  • deepmd/dpmodel/utils/__init__.py
  • deepmd/dpmodel/utils/default_neighbor_list.py
  • deepmd/dpmodel/utils/neighbor_list.py
  • deepmd/pt_expt/infer/deep_eval.py
  • deepmd/pt_expt/model/ener_model.py
  • deepmd/pt_expt/utils/vesin_neighbor_list.py
  • pyproject.toml
  • source/tests/pt_expt/infer/test_deep_eval.py
  • source/tests/pt_expt/utils/test_neighbor_list.py

Comment thread deepmd/pt_expt/infer/deep_eval.py Outdated
Comment thread deepmd/pt_expt/utils/vesin_neighbor_list.py
Extend the pluggable neighbor list to the pt (torch.jit) backend. The pt model
is reconstructed eagerly in DeepEval, so when nlist_backend selects vesin we
build the (i,j,S) extended representation with VesinNeighborList and run the
model's forward_common_lower + communicate_extended_output directly, leaving the
TorchScript graph untouched. vesin is gated off for spin/hessian models and ASE
neighbor_list conflicts; auto falls back to native, vesin is strict.

Also:
- VesinNeighborList: avoid torch.as_tensor without an explicit device (under a
  non-CPU ambient default device it triggers CUDA init even for CPU tensors);
  bridge numpy via from_numpy and use torch tensors directly. Makes the builder
  device-robust.
- drop the getattr(self, "_use_vesin"/"_nlist_builder", ...) defensive defaults;
  both attributes are always initialized in __init__ via _setup_nlist_backend.

Tested: source/tests/pt/model/test_nlist_backend.py (dispatch + vesin-vs-native
energy/force/virial/atomic-virial equivalence for se_e2_a and dpa1, PBC/non-PBC).
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: 1

♻️ Duplicate comments (1)
deepmd/pt_expt/utils/vesin_neighbor_list.py (1)

205-224: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep all in-cutoff candidates until the lower layer does the type split.

This still truncates to the global sum(sel) nearest neighbors before per-type formatting, so type-separated models can lose required neighbors and diverge from the native builder. A simple sel=[1, 1] case with candidates A@1.0, A@1.1, B@1.2 becomes [A, A] here, leaving no way for the downstream type split to recover the required B. Keep all in-cutoff candidates here (or at least a per-type-safe superset) and defer the final truncation until after type separation.

🤖 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/utils/vesin_neighbor_list.py` around lines 205 - 224, The
current code truncates neighbors early by using keep = min(nsel, max_nn) which
drops in-cutoff candidates needed for per-type splitting; change the logic to
keep all in-cutoff candidates up to max_nn and defer final truncation until
after the type split: allocate a neighbor buffer wide enough for max_nn (e.g.,
nlist_all = torch.full((nloc, max_nn), -1, ...)) or adjust nlist to width =
max(nsel, max_nn), replace keep = min(nsel, max_nn) with keep = max_nn (or
compute keep = sorted_valid.sum(dim=-1).clamp_max(max_nn) if you want per-row
limits), and fill nlist_all[:, :keep] using sorted_idx and sorted_valid just
like the existing torch.where; leave any slicing to nsel to the downstream
type-splitting code so per-type selection can recover required neighbors.
🤖 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/pt/model/test_nlist_backend.py`:
- Around line 123-128: The test is currently iterating dict keys because
dp_native.eval and dp_vesin.eval return dicts; change the comparison to pull the
actual arrays by key (e.g. for label in ["e","f","v","ae","av"] and then compare
ref[label] vs out[label]) instead of zipping ref and out directly. Update the
loop that uses variables ref and out to index into those dicts (ref[label],
out[label]) and then call np.testing.assert_allclose with those values and the
existing rtol/atol and err_msg=f"{name} {label}" so the numeric tensors are
actually compared.

---

Duplicate comments:
In `@deepmd/pt_expt/utils/vesin_neighbor_list.py`:
- Around line 205-224: The current code truncates neighbors early by using keep
= min(nsel, max_nn) which drops in-cutoff candidates needed for per-type
splitting; change the logic to keep all in-cutoff candidates up to max_nn and
defer final truncation until after the type split: allocate a neighbor buffer
wide enough for max_nn (e.g., nlist_all = torch.full((nloc, max_nn), -1, ...))
or adjust nlist to width = max(nsel, max_nn), replace keep = min(nsel, max_nn)
with keep = max_nn (or compute keep = sorted_valid.sum(dim=-1).clamp_max(max_nn)
if you want per-row limits), and fill nlist_all[:, :keep] using sorted_idx and
sorted_valid just like the existing torch.where; leave any slicing to nsel to
the downstream type-splitting code so per-type selection can recover required
neighbors.
🪄 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: 9526f665-8b61-4e12-90c1-dd691dfba614

📥 Commits

Reviewing files that changed from the base of the PR and between 6fd4ee5 and 4a64333.

📒 Files selected for processing (4)
  • deepmd/pt/infer/deep_eval.py
  • deepmd/pt_expt/infer/deep_eval.py
  • deepmd/pt_expt/utils/vesin_neighbor_list.py
  • source/tests/pt/model/test_nlist_backend.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • deepmd/pt_expt/infer/deep_eval.py

Comment thread source/tests/pt/model/test_nlist_backend.py
Han Wang added 2 commits June 3, 2026 16:47
Add multi-frame tests (frames with different box sizes -> different per-frame
ghost counts) that exercise the VesinNeighborList per-frame loop +
pad-to-common-nall + stack path, which was previously only covered with
nframes=1:

- pt_expt: builder-level multi-frame neighbor-multiset equivalence (numpy +
  torch) and end-to-end model multi-frame equivalence (se_e2_a, dpa1).
- pt: multi-frame DeepPot.eval vesin-vs-native equivalence (se_e2_a, dpa1).

The pt suite now runs green on CPU under the cuda:9999999 sentinel default
device (the builder was made device-clean in the previous commit), so it no
longer depends on a GPU runner.
Replace the from_numpy/ascontiguousarray branch with a clean `torch.as_tensor`
inside a `with torch.device(device)` block (device = CPU for numpy/dpmodel
inputs, the input tensor's device for torch), matching the existing
`with torch.device(...)` guard around `nl.compute` and the project convention
of pinning the device on tensor creation. Fixes the CUDA-init-under-placeholder-
default-device issue without the clunky numpy bridge.
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.

🧹 Nitpick comments (1)
source/tests/pt_expt/utils/test_neighbor_list.py (1)

423-448: ⚡ Quick win

Consider parametrizing periodic for the multi-frame pt_expt test.

The single-frame test_pt_expt_equivalence covers both PBC and non-PBC, but this multi-frame variant only exercises the periodic path (box is always passed). Since the PR notes multi-frame vesin paths are not fully tested, adding the non-periodic case closes a real coverage gap at minimal cost.

♻️ Proposed change
-@pytest.mark.parametrize("name", ["se_e2_a", "dpa1"])  # non-mixed + attention
-def test_pt_expt_multiframe_equivalence(name: str) -> None:
+@pytest.mark.parametrize("name", ["se_e2_a", "dpa1"])  # non-mixed + attention
+@pytest.mark.parametrize("periodic", [False, True])  # non-PBC vs PBC
+def test_pt_expt_multiframe_equivalence(name: str, periodic: bool) -> None:
     """Multi-frame (frames with differing ghost counts) pt_expt outputs are
     invariant to the nlist strategy -- exercises the builder's per-frame pad +
     stack feeding the batched model forward.
     """
     coord_np, atype_np, box_np = _multiframe_system()
+    box_np = box_np if periodic else None
     model_dict = ALL_MODELS[name]
     md = get_model(copy.deepcopy(model_dict))
     md.eval()
     atype_t = torch.tensor(atype_np, dtype=torch.int64)
-    box_t = torch.tensor(box_np, dtype=torch.float64)
+    box_t = None if box_np is None else torch.tensor(box_np, dtype=torch.float64)
🤖 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/pt_expt/utils/test_neighbor_list.py` around lines 423 - 448,
Parametrize test_pt_expt_multiframe_equivalence with a periodic flag (e.g., add
pytest.mark.parametrize("periodic", [True, False])) and run the same multi-frame
checks for both periodic and non-periodic cases: when periodic is True build
box_t from box_np and pass box=box_t to md.forward, when False set box_t=None
and call md.forward without the box argument (or pass box=None) so the non-PBC
path is exercised; keep the same neighbor_list variants (DefaultNeighborList,
VesinNeighborList) and assertion checks for "energy", "force", "virial",
"atom_virial".
🤖 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.

Nitpick comments:
In `@source/tests/pt_expt/utils/test_neighbor_list.py`:
- Around line 423-448: Parametrize test_pt_expt_multiframe_equivalence with a
periodic flag (e.g., add pytest.mark.parametrize("periodic", [True, False])) and
run the same multi-frame checks for both periodic and non-periodic cases: when
periodic is True build box_t from box_np and pass box=box_t to md.forward, when
False set box_t=None and call md.forward without the box argument (or pass
box=None) so the non-PBC path is exercised; keep the same neighbor_list variants
(DefaultNeighborList, VesinNeighborList) and assertion checks for "energy",
"force", "virial", "atom_virial".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1f41cd3f-c5a1-4ce8-896f-a8d2d169cec2

📥 Commits

Reviewing files that changed from the base of the PR and between 4a64333 and 6a00c4c.

📒 Files selected for processing (2)
  • source/tests/pt/model/test_nlist_backend.py
  • source/tests/pt_expt/utils/test_neighbor_list.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • source/tests/pt/model/test_nlist_backend.py

vesin's nl.compute rejects an empty `points` array ("`points` can not be a NULL
pointer"), so an all-empty system (e.g. test_zero_input, coords shape [nf,0,3])
crashed once vesin became the default builder. Return an empty extended
representation directly for a zero-atom frame, matching the native builder.

Fixes the CI failures in test_models.py::TestDeepPot_fparam_aparam_*::test_zero_input
(.pth and .pte). Adds a builder-level empty-system regression test.
sel = self._sel
mixed_types = self._mixed_types

if self._nlist_builder is not None:
Copy link
Copy Markdown
Contributor

@njzjz-bot njzjz-bot Jun 3, 2026

Choose a reason for hiding this comment

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

This currently bypasses an explicitly supplied ASE neighbor list whenever nlist_backend="auto" (the new default). In DeepEval.__init__, _setup_nlist_backend() runs before self.neighbor_list is assigned, so ase_provided is always false; if vesin is installed, _nlist_builder is enabled and this early return is taken before the native/ASE branch below.

That changes existing behavior for callers/tests using DeepPot(..., neighbor_list=ase.neighborlist.NewPrimitiveNeighborList(...)). It also matches the current CI failure in TestEvalDescriptorASE.test_eval_descriptor_ase_vs_native, where the ASE result differs because the ASE path is not actually used.

Please set self.neighbor_list = neighbor_list before calling _setup_nlist_backend() (as done in the pt backend), or otherwise make auto preserve explicit ASE neighbor lists.

Authored by OpenClaw (model: custom-chat-jinzhezeng-group/gpt-5.5)

…r path

eval_descriptor calls the descriptor directly, bypassing forward_common_lower's
format_nlist. The native _build_nlist_native builds with
distinguish_types=not mixed_types, so for a non-mixed-type model it hands the
descriptor a type-blocked nlist; the vesin branch returned a non-distinguished
list, giving a wrong descriptor on this path (CI: TestEvalDescriptorASE).

Apply nlist_distinguish_types to the vesin output when not mixed_types, matching
the native builder. The main eval path is unaffected (its format_nlist re-formats;
energy/force/virial already matched native to ~1e-19). Adds a direct
eval_descriptor vesin-vs-native regression test.
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.

2 participants