Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions deepmd/dpmodel/utils/neighbor_stat.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,23 +86,38 @@ def call(
- xp.reshape(coord0, (nframes, -1, 3))[:, :, None, :]
)
assert list(diff.shape) == [nframes, nloc, nall, 3]
# remove the diagonal elements
mask = xp.eye(nloc, nall, dtype=xp.bool, device=array_api_compat.device(diff))
mask = xp.tile(mask[None, :, :, None], (nframes, 1, 1, 3))
diff = xp.where(mask, xp.full_like(diff, xp.inf), diff)
# A valid statistics pair must contain two real atoms. In particular,
# virtual centers must not contribute an artificially large neighbor row,
# and virtual neighbors must not drive the minimum distance to zero.
self_pair = xp.eye(
nloc, nall, dtype=xp.bool, device=array_api_compat.device(diff)
)
real_center = atype >= 0
real_neighbor = extend_atype >= 0
Comment thread
njzjz marked this conversation as resolved.
valid_pair = (
~self_pair[None, :, :] & real_center[:, :, None] & real_neighbor[:, None, :]
)
Comment thread
njzjz-bot marked this conversation as resolved.
rr2 = xp.sum(xp.square(diff), axis=-1)
rr2 = xp.where(valid_pair, rr2, xp.full_like(rr2, xp.inf))
min_rr2 = xp.min(rr2, axis=-1)
# count the number of neighbors
within_rcut = valid_pair & (rr2 < self.rcut**2)
if not self.mixed_types:
mask = rr2 < self.rcut**2
nneis = []
for ii in range(self.ntypes):
nneis.append(xp.sum(mask & (extend_atype == ii)[:, None, :], axis=-1))
nneis.append(
xp.sum(
xp.astype(
within_rcut & (extend_atype == ii)[:, None, :],
extend_atype.dtype,
),
axis=-1,
)
)
nnei = xp.stack(nneis, axis=-1)
else:
mask = rr2 < self.rcut**2
# virtual type (<0) are not counted
nnei = xp.sum(mask & (extend_atype >= 0)[:, None, :], axis=-1)
# Array API reductions accept numeric rather than boolean inputs.
nnei = xp.sum(xp.astype(within_rcut, extend_atype.dtype), axis=-1)
nnei = xp.reshape(nnei, (nframes, nloc, 1))
max_nnei = xp.max(nnei, axis=1)
return min_rr2, max_nnei
Expand Down
46 changes: 27 additions & 19 deletions deepmd/pd/utils/neighbor_stat.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,30 +85,38 @@ def forward(
1
) - coord0.reshape([nframes, -1, 3]).unsqueeze(2)
assert list(diff.shape) == [nframes, nloc, nall, 3]
# remove the diagonal elements
mask = paddle.eye(nloc, nall).to(dtype=paddle.bool, device=diff.place)
# diff[:, mask] = float("inf")
# diff.masked_fill_(
# paddle.broadcast_to(mask.unsqueeze([0, -1]), diff.shape),
# paddle.to_tensor(float("inf")),
# )
diff[paddle.broadcast_to(mask.unsqueeze([0, -1]), diff.shape)] = float("inf")
# Match the generic neighbor-stat operator: virtual atoms cannot be
# statistics centers or neighbors, and self pairs are always excluded.
self_pair = paddle.eye(nloc, nall).to(dtype=paddle.bool, device=diff.place)
real_center = atype >= 0
real_neighbor = extend_atype >= 0
valid_pair = (
~self_pair.unsqueeze(0)
& real_center.unsqueeze(2)
& real_neighbor.unsqueeze(1)
)
rr2 = paddle.sum(paddle.square(diff), axis=-1)
rr2 = paddle.where(valid_pair, rr2, paddle.full_like(rr2, float("inf")))
min_rr2 = paddle.min(rr2, axis=-1)
# count the number of neighbors
within_rcut = valid_pair & (rr2 < self.rcut**2)
if not self.mixed_types:
mask = rr2 < self.rcut**2
nnei = paddle.zeros([nframes, nloc, self.ntypes], dtype=paddle.int64)
for ii in range(self.ntypes):
nnei[:, :, ii] = paddle.sum(
mask & ((extend_atype == ii)[:, None, :]), axis=-1
)
nnei = paddle.stack(
[
paddle.sum(
(within_rcut & (extend_atype == ii).unsqueeze(1)).astype(
extend_atype.dtype
),
axis=-1,
)
for ii in range(self.ntypes)
],
axis=-1,
)
else:
mask = rr2 < self.rcut**2
# virtual types (<0) are not counted
nnei = paddle.sum(
mask & ((extend_atype >= 0).unsqueeze(1)), axis=-1
).reshape([nframes, nloc, 1])
nnei = paddle.sum(within_rcut.astype(extend_atype.dtype), axis=-1).reshape(
[nframes, nloc, 1]
)
max_nnei = paddle.max(nnei, axis=1)
return min_rr2, max_nnei

Expand Down
4 changes: 4 additions & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]

# External inventories are optional cross-reference metadata. Limit connection
# and read-inactivity waits so Sphinx can warn and continue if a host stalls.
# This is not a deadline for the full download or documentation build.
intersphinx_timeout = 30
Comment thread
coderabbitai[bot] marked this conversation as resolved.
intersphinx_mapping = {
"numpy": ("https://docs.scipy.org/doc/numpy/", None),
"python": ("https://docs.python.org/", None),
Expand Down
51 changes: 51 additions & 0 deletions source/tests/common/dpmodel/array_api/test_neighbor_stat.py
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)
40 changes: 40 additions & 0 deletions source/tests/common/dpmodel/test_neighbor_stat.py
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)
64 changes: 64 additions & 0 deletions source/tests/pd/test_neighbor_stat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -67,3 +74,60 @@ 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_periodic_self_images_remain_neighbors(self) -> None:
"""Exclude the original self pair but retain its two nearby images."""
coord = paddle.zeros([1, 6], dtype=paddle.float64).to(DEVICE)
atype = paddle.to_tensor([[0, -1]], dtype=paddle.int64, place=DEVICE)
cell = paddle.to_tensor(
[[1.0, 0.0, 0.0, 0.0, 10.0, 0.0, 0.0, 0.0, 10.0]],
dtype=paddle.float64,
place=DEVICE,
)
for mixed_types in (False, True):
with self.subTest(mixed_types=mixed_types):
min_rr2, max_nnei = NeighborStatOP(
ntypes=2, rcut=1.1, mixed_types=mixed_types
)(coord, atype, cell)
# Only the real atom's images at x = -1 and +1 contribute.
np.testing.assert_allclose(min_rr2.numpy(), [[1.0, np.inf]])
np.testing.assert_array_equal(
max_nnei.numpy(), [[2]] if mixed_types else [[2, 0]]
)

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 thread
njzjz marked this conversation as resolved.
Loading