Skip to content
Merged
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
41 changes: 34 additions & 7 deletions deepmd/tf/infer/deep_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,15 +549,23 @@ def build_neighbor_list(
atype: np.ndarray,
imap: np.ndarray,
neighbor_list: "ase.neighborlist.NeighborList | None",
) -> tuple[np.ndarray, np.ndarray]:
) -> tuple[
np.ndarray,
np.ndarray,
np.ndarray,
np.ndarray,
np.ndarray,
np.ndarray,
]:
"""Make the mesh with neighbor list for a single frame.

Parameters
----------
coords : np.ndarray
The coordinates of atoms. Should be of shape [natoms, 3]
cell : Optional[np.ndarray]
The cell of the system. Should be of shape [3, 3]
The cell of the system. Should be of shape [3, 3]. None denotes
open boundary conditions.
atype : np.ndarray
The type of atoms. Should be of shape [natoms]
imap : np.ndarray
Expand Down Expand Up @@ -587,7 +595,11 @@ def build_neighbor_list(
The index map of ghost atoms. Should be of shape [nghost]
"""
pbc = np.repeat(cell is not None, 3)
cell = cell.reshape(3, 3)
# ASE still requires a 3x3 cell for non-periodic systems, but the cell
# must not be used to infer periodicity or create ghost atoms.
cell = (
np.zeros((3, 3), dtype=np.float64) if cell is None else cell.reshape(3, 3)
)
positions = coords.reshape(-1, 3)
neighbor_list.bothways = True
neighbor_list.self_interaction = False
Expand Down Expand Up @@ -814,6 +826,9 @@ def _prepare_feed_dict(
else:
pbc = True
cells = np.array(cells).reshape([nframes, 9])
# Keep the original boundary semantics separate from the identity box
# used only to satisfy TensorFlow's non-optional box placeholder.
neighbor_cell = cells if pbc else None

if self.has_fparam:
assert fparam is not None
Expand Down Expand Up @@ -884,7 +899,7 @@ def _prepare_feed_dict(
ghost_map,
) = self.build_neighbor_list(
coords,
cells if cells is not None else None,
neighbor_cell,
atom_types,
imap,
self.neighbor_list,
Expand Down Expand Up @@ -1534,15 +1549,23 @@ def build_neighbor_list(
atype: np.ndarray,
imap: np.ndarray,
neighbor_list: "ase.neighborlist.NeighborList | None",
) -> tuple[np.ndarray, np.ndarray]:
) -> tuple[
np.ndarray,
np.ndarray,
np.ndarray,
np.ndarray,
np.ndarray,
np.ndarray,
]:
"""Make the mesh with neighbor list for a single frame.

Parameters
----------
coords : np.ndarray
The coordinates of atoms. Should be of shape [natoms, 3]
cell : Optional[np.ndarray]
The cell of the system. Should be of shape [3, 3]
The cell of the system. Should be of shape [3, 3]. None denotes
open boundary conditions.
atype : np.ndarray
The type of atoms. Should be of shape [natoms]
imap : np.ndarray
Expand Down Expand Up @@ -1572,7 +1595,11 @@ def build_neighbor_list(
The index map of ghost atoms. Should be of shape [nghost]
"""
pbc = np.repeat(cell is not None, 3)
cell = cell.reshape(3, 3)
# ASE still requires a 3x3 cell for non-periodic systems, but the cell
# must not be used to infer periodicity or create ghost atoms.
cell = (
np.zeros((3, 3), dtype=np.float64) if cell is None else cell.reshape(3, 3)
)
positions = coords.reshape(-1, 3)
neighbor_list.bothways = True
neighbor_list.self_interaction = False
Expand Down
10 changes: 8 additions & 2 deletions deepmd/tf/infer/deep_tensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ def eval(
else:
pbc = True
cells = np.array(cells).reshape([nframes, 9])
# Keep the original boundary semantics separate from the identity box
# used only to satisfy TensorFlow's non-optional box placeholder.
neighbor_cell = cells if pbc else None

# sort inputs
coords, atom_types, imap, sel_at, sel_imap = self.sort_input(
Expand All @@ -227,7 +230,7 @@ def eval(
_,
) = self.build_neighbor_list(
coords,
cells if cells is not None else None,
neighbor_cell,
atom_types,
imap,
self.neighbor_list,
Expand Down Expand Up @@ -346,6 +349,9 @@ def eval_full(
else:
pbc = True
cells = np.array(cells).reshape([nframes, 9])
# Keep the original boundary semantics separate from the identity box
# used only to satisfy TensorFlow's non-optional box placeholder.
neighbor_cell = cells if pbc else None
nout = self.output_dim

# sort inputs
Expand Down Expand Up @@ -373,7 +379,7 @@ def eval_full(
ghost_map,
) = self.build_neighbor_list(
coords,
cells if cells is not None else None,
neighbor_cell,
atom_types,
imap,
self.neighbor_list,
Expand Down
56 changes: 43 additions & 13 deletions source/tests/infer/test_models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
import unittest

import ase
import ase.neighborlist
import dpdata
import numpy as np

Expand Down Expand Up @@ -32,15 +32,9 @@
STRICT_KEYS = frozenset(("se_e2_a", "se_e2_r"))


@parameterized(
(
"se_e2_a",
"se_e2_r",
"fparam_aparam",
), # key
(".pb", ".pth", ".pte", ".pt2"), # model extension
)
class TestDeepPot(unittest.TestCase):
class DeepPotTestMixin:
"""Shared DeepPotential checks for native and external neighbor lists."""

# moved from tests/tf/test_deeppot_a.py

@classmethod
Expand Down Expand Up @@ -396,19 +390,33 @@ def test_model_script_def(self) -> None:
)


@parameterized(
(
"se_e2_a",
"se_e2_r",
"fparam_aparam",
), # key
(".pb", ".pth", ".pte", ".pt2"), # model extension
)
class TestDeepPot(DeepPotTestMixin, unittest.TestCase):
"""Run the common inference checks with native neighbor construction."""


@parameterized(
("se_e2_a",), # key
(".pb",), # model extension
)
class TestDeepPotNeighborList(TestDeepPot):
class TestDeepPotNeighborList(DeepPotTestMixin, unittest.TestCase):
"""Run the common inference checks with an external ASE neighbor list."""

@classmethod
def setUpClass(cls) -> None:
key, extension = cls.param
cls.places = STRICT_PLACES if key in STRICT_KEYS else default_places
cls.case = get_cases()[key]
model_name = cls.case.get_model(extension)
cls.model_name = cls.case.get_model(extension)
cls.dp = DeepEval(
model_name,
cls.model_name,
neighbor_list=ase.neighborlist.NewPrimitiveNeighborList(
cutoffs=cls.case.rcut, bothways=True
),
Expand All @@ -421,3 +429,25 @@ def test_2frame_atm(self) -> None:
@unittest.skip("Zero atoms not supported")
def test_zero_input(self) -> None:
pass

def test_nopbc_matches_reference(self) -> None:
"""The ASE path must preserve a testcase's open-boundary semantics."""
result = next(result for result in self.case.results if result.box is None)
ee, ff, vv, ae, av = self.dp.eval(
result.coord,
None,
result.atype,
atomic=True,
fparam=result.fparam,
aparam=result.aparam,
)[:5]

np.testing.assert_almost_equal(ff.ravel(), result.force.ravel(), STRICT_PLACES)
np.testing.assert_almost_equal(
ae.ravel(), result.atomic_energy.ravel(), STRICT_PLACES
)
np.testing.assert_almost_equal(
av.ravel(), result.atomic_virial.ravel(), STRICT_PLACES
)
np.testing.assert_almost_equal(ee.ravel(), result.energy, STRICT_PLACES)
np.testing.assert_almost_equal(vv.ravel(), result.virial, STRICT_PLACES)
47 changes: 47 additions & 0 deletions source/tests/tf/test_deepdipole.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
from deepmd.tf.infer import (
DeepDipole,
)
from deepmd.tf.infer.deep_dipole import (
DeepDipoleOld,
)
from deepmd.tf.utils.convert import (
convert_pbtxt_to_pb,
)
Expand Down Expand Up @@ -1162,3 +1165,47 @@ def test_2frame_full_atm(self) -> None:
@unittest.skip("multiple frames not supported")
def test_2frame_old_atm(self) -> None:
pass

def test_nopbc_matches_native_neighbor_building(self) -> None:
"""ASE and native tensor inference must agree for an open system."""

def assert_evaluators_match(external, native) -> None:
actual_tensor = external.eval(self.coords, None, self.atype, atomic=True)
expected_tensor = native.eval(self.coords, None, self.atype, atomic=True)
np.testing.assert_almost_equal(
actual_tensor,
expected_tensor,
default_places,
)

actual_full = external.eval_full(
self.coords,
None,
self.atype,
atomic=True,
)
expected_full = native.eval_full(
self.coords,
None,
self.atype,
atomic=True,
)
for actual, expected in zip(actual_full, expected_full, strict=True):
np.testing.assert_almost_equal(actual, expected, default_places)

# The public wrapper reaches deepmd/tf/infer/deep_eval.py.
with DeepDipole("deepdipole_new.pb") as native:
assert_evaluators_match(self.dp, native)

# Exercise the legacy TF-specific deep_tensor.py path separately; the
# public DeepDipole wrapper does not instantiate this implementation.
with (
DeepDipoleOld(
"deepdipole_new.pb",
neighbor_list=ase.neighborlist.NewPrimitiveNeighborList(
cutoffs=6, bothways=True
),
) as external_old,
DeepDipoleOld("deepdipole_new.pb") as native_old,
):
assert_evaluators_match(external_old, native_old)
Loading