-
Notifications
You must be signed in to change notification settings - Fork 644
fix(dpmodel): exclude virtual atoms from neighbor stats #5834
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| # SPDX-License-Identifier: LGPL-3.0-or-later | ||
| import unittest | ||
|
|
||
| import array_api_strict as xp | ||
|
|
||
| from deepmd.dpmodel.utils.neighbor_stat import ( | ||
| NeighborStatOP, | ||
| ) | ||
|
|
||
| from .utils import ( | ||
| ArrayAPITest, | ||
| ) | ||
|
|
||
|
|
||
| class TestNeighborStatOP(unittest.TestCase, ArrayAPITest): | ||
| def test_virtual_atoms_are_masked_before_reductions(self) -> None: | ||
| """Virtual-pair masking and neighbor reductions follow the Array API.""" | ||
| coord = xp.reshape( | ||
| xp.asarray( | ||
| [ | ||
| [0.0, 0.0, 0.0], | ||
| [0.0, 0.0, 0.0], | ||
| [1.0, 0.0, 0.0], | ||
| [3.0, 0.0, 0.0], | ||
| ], | ||
| dtype=xp.float64, | ||
| ), | ||
| (1, -1), | ||
| ) | ||
| atype = xp.asarray([[0, -1, 0, 1]], dtype=xp.int64) | ||
| expected_min_rr2 = xp.asarray([[1.0, xp.inf, 1.0, 4.0]], dtype=xp.float64) | ||
|
|
||
| for mixed_types in (False, True): | ||
| with self.subTest(mixed_types=mixed_types): | ||
| min_rr2, max_nnei = NeighborStatOP(2, 1.1, mixed_types).call( | ||
| coord, atype, None | ||
| ) | ||
| expected_max_nnei = xp.asarray( | ||
| [[1]] if mixed_types else [[1, 0]], dtype=xp.int64 | ||
| ) | ||
|
|
||
| self.assertTrue(bool(xp.all(min_rr2 == expected_min_rr2))) | ||
| self.assertTrue(bool(xp.all(max_nnei == expected_max_nnei))) | ||
| self.assert_namespace_equal(min_rr2, coord) | ||
| self.assert_namespace_equal(max_nnei, atype) | ||
| self.assert_device_equal(min_rr2, coord) | ||
| self.assert_device_equal(max_nnei, atype) | ||
| self.assert_dtype_equal(min_rr2, coord) | ||
| self.assert_dtype_equal(max_nnei, atype) | ||
| self.assertEqual(min_rr2.shape, (1, 4)) | ||
| self.assertEqual(max_nnei.shape, expected_max_nnei.shape) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| # SPDX-License-Identifier: LGPL-3.0-or-later | ||
| import unittest | ||
|
|
||
| import numpy as np | ||
|
|
||
| from deepmd.dpmodel.utils.neighbor_stat import ( | ||
| NeighborStatOP, | ||
| ) | ||
|
|
||
|
|
||
| class TestNeighborStatOP(unittest.TestCase): | ||
| def test_virtual_atoms_do_not_affect_statistics(self) -> None: | ||
| """Ignore virtual atoms as both neighbor-stat centers and neighbors.""" | ||
| # Atom 1 is virtual and overlaps atom 0. Without a neighbor mask it | ||
| # drives the minimum distance to zero; without a center mask it sees both | ||
| # type-0 atoms and inflates their maximum neighbor count from one to two. | ||
| coord = np.array( | ||
| [ | ||
| [0.0, 0.0, 0.0], | ||
| [0.0, 0.0, 0.0], | ||
| [1.0, 0.0, 0.0], | ||
| [3.0, 0.0, 0.0], | ||
| ], | ||
| dtype=np.float64, | ||
| ).reshape(1, -1) | ||
| atype = np.array([[0, -1, 0, 1]], dtype=np.int64) | ||
| expected_min_rr2 = np.array([[1.0, np.inf, 1.0, 4.0]]) | ||
|
|
||
| for cell in (None, 10.0 * np.eye(3).reshape(1, 9)): | ||
| for mixed_types in (False, True): | ||
| with self.subTest(cell=cell is not None, mixed_types=mixed_types): | ||
| min_rr2, max_nnei = NeighborStatOP( | ||
| ntypes=2, | ||
| rcut=1.1, | ||
| mixed_types=mixed_types, | ||
| ).call(coord, atype, cell) | ||
|
|
||
| np.testing.assert_allclose(min_rr2, expected_min_rr2) | ||
| expected_max_nnei = [[1]] if mixed_types else [[1, 0]] | ||
| np.testing.assert_array_equal(max_nnei, expected_max_nnei) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,10 +4,17 @@ | |
|
|
||
| import dpdata | ||
| import numpy as np | ||
| import paddle | ||
|
|
||
| from deepmd.entrypoints.neighbor_stat import ( | ||
| neighbor_stat, | ||
| ) | ||
| from deepmd.pd.utils.env import ( | ||
| DEVICE, | ||
| ) | ||
| from deepmd.pd.utils.neighbor_stat import ( | ||
| NeighborStatOP, | ||
| ) | ||
|
|
||
| from ..seed import ( | ||
| GLOBAL_SEED, | ||
|
|
@@ -67,3 +74,40 @@ def test_neighbor_stat(self): | |
| if not mixed_type: | ||
| ret.append(0) | ||
| np.testing.assert_array_equal(max_nbor_size, ret) | ||
|
|
||
|
|
||
| class TestNeighborStatOP(unittest.TestCase): | ||
| def test_virtual_atoms_do_not_affect_statistics(self) -> None: | ||
| """Ignore virtual atoms as both statistics centers and neighbors.""" | ||
| # Atom 1 is virtual and overlaps atom 0. Without both masks, it either | ||
| # sets the minimum distance to zero or inflates the maximum type-0 count. | ||
| coord = paddle.to_tensor( | ||
| [ | ||
| [ | ||
| [0.0, 0.0, 0.0], | ||
| [0.0, 0.0, 0.0], | ||
| [1.0, 0.0, 0.0], | ||
| [3.0, 0.0, 0.0], | ||
| ] | ||
| ], | ||
| dtype=paddle.float64, | ||
| place=DEVICE, | ||
| ).reshape([1, -1]) | ||
| atype = paddle.to_tensor([[0, -1, 0, 1]], dtype=paddle.int64, place=DEVICE) | ||
| expected_min_rr2 = np.array([[1.0, np.inf, 1.0, 4.0]]) | ||
|
|
||
| for cell in ( | ||
| None, | ||
| 10.0 * paddle.eye(3, dtype=paddle.float64).reshape([1, 9]).to(DEVICE), | ||
| ): | ||
| for mixed_types in (False, True): | ||
| with self.subTest(cell=cell is not None, mixed_types=mixed_types): | ||
| min_rr2, max_nnei = NeighborStatOP( | ||
| ntypes=2, | ||
| rcut=1.1, | ||
| mixed_types=mixed_types, | ||
| )(coord, atype, cell) | ||
|
|
||
| np.testing.assert_allclose(min_rr2.numpy(), expected_min_rr2) | ||
| expected_max_nnei = [[1]] if mixed_types else [[1, 0]] | ||
| np.testing.assert_array_equal(max_nnei.numpy(), expected_max_nnei) | ||
|
Comment on lines
+99
to
+113
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Add a periodic self-image regression case. Line 101 uses cell edges of Add a case with at least one cell axis shorter than 🤖 Prompt for AI Agents |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
real_centerandreal_neighborare the fix, and both are correct. The same operator is duplicated in Paddle, which this PR does not touch:deepmd/pd/utils/neighbor_stat.py#L88-L112. It is a hand-writtenpaddle.nn.Layer, not an import of this class, so it does not inherit the change.deepmd/pt,deepmd/jaxanddeepmd/pt_exptall importNeighborStatOPfrom here and do pick it up, which leaves Paddle as the only backend behind.That copy is line-for-line the pre-fix logic: it masks only the
eye(nloc, nall)self pair beforemin_rr2, and its non-mixed branch never excludes virtual centers at all. On this PR's own fixture (coord = [(0,0,0), (0,0,0), (1,0,0), (3,0,0)],atype = [0,-1,0,1],rcut = 1.1) Paddle still returnsmin_rr2 = [0, 0, 1, 4]andmax_nnei = [[2]], against[1, inf, 1, 4]and[[1]]everywhere else. Sodp --pd neighbor-staton a mixed-type dataset whose padding slot lands on a real atom still aborts withRuntimeError: Some atoms are overlapping, and where it does not abort it picks a strictly larger auto-selthan the other backends for the same data. Paddle's ownbuild_neighbor_listalready relocates virtual atoms, so that extraselis pure waste.CLAUDE.md asks for this explicitly: "Before editing shared code (
deepmd/dpmodel/, base classes), find every importer and run all affected backends' tests: backends routinely re-export or subclass generic classes, so one change ripples across tf/pt/pt_expt/dpmodel/jax/pd." The same point came up on #5854 and #5856 and was handled in a follow-up commit each time; the cleanest resolution here is the same.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 8b594d7. The Paddle implementation now excludes self pairs, virtual centers, and virtual neighbors before both the minimum-distance and neighbor-count reductions, matching the generic operator. I also added direct Paddle coverage for periodic/nonperiodic inputs and mixed/type-separated counts using this fixture.
Validation:
pytest source/tests/pd/test_neighbor_stat.py -q: 2 passed, 12 subtests passedruff format .andruff check .: passedCoding agent: Codex
Codex version: codex-cli 0.144.6
Model: gpt-5.6-sol
Reasoning effort: xhigh