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
49 changes: 41 additions & 8 deletions deepmd/utils/pair_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,50 @@ def reinit(self, filename: str, rcut: float | None = None) -> None:
if filename is None:
self.tab_info, self.tab_data = None, None
return
self.vdata = np.loadtxt(filename, dtype=self.data_type)
self.rmin = self.vdata[0][0]
self.rmax = self.vdata[-1][0]
self.hh = self.vdata[1][0] - self.vdata[0][0]
ncol = self.vdata.shape[1] - 1
vdata = np.loadtxt(filename, dtype=self.data_type)
rmin = vdata[0][0]
rmax = vdata[-1][0]
hh = vdata[1][0] - vdata[0][0]
dx = np.diff(vdata[:, 0])
if not np.all(dx > 0):
raise ValueError(
f"The distance grid in the pairwise table {filename} is not "
"strictly increasing. The tabulated potential must be provided "
"on a uniform grid with distances sorted in ascending order and "
"without duplicated rows. Please regrid the table."
)
# validate against absolute node positions rather than per-interval
# spacing: consumers (the C++ kernel and _make_data) index by
# rmin + i * hh, so that is what must stay accurate, not each dx.
n = vdata.shape[0]
hh_ref = (rmax - rmin) / (n - 1)
deviation = np.abs(
vdata[:, 0] - (rmin + hh_ref * np.arange(n, dtype=self.data_type))
)
tol = 1e-2 * abs(hh_ref)
if np.any(deviation > tol):
bad_row = int(np.argmax(deviation > tol))
raise ValueError(
f"The distance grid in the pairwise table {filename} is not "
"evenly spaced. The tabulated potential must be provided on a "
f"uniform grid, but row {bad_row} (distance "
f"{vdata[bad_row, 0]}) does not match the constant step "
f"inferred from rmin and rmax ({hh_ref}). Please regrid the "
"table to use a constant distance step."
)
ncol = vdata.shape[1] - 1
n0 = (-1 + np.sqrt(1 + 8 * ncol)) * 0.5
self.ntypes = int(n0 + 0.1)
assert self.ntypes * (self.ntypes + 1) // 2 == ncol, (
f"number of volumes provided in {filename} does not match guessed number of types {self.ntypes}"
ntypes = int(n0 + 0.1)
assert ntypes * (ntypes + 1) // 2 == ncol, (
f"number of volumes provided in {filename} does not match guessed number of types {ntypes}"
)

self.vdata = vdata
self.rmin = rmin
self.rmax = rmax
self.hh = hh
self.ntypes = ntypes

# check table data against rcut and update tab_file if needed, table upper boundary is used as rcut if not provided.
self.rcut = rcut if rcut is not None else self.rmax
self._check_table_upper_boundary()
Expand Down
131 changes: 131 additions & 0 deletions source/tests/common/dpmodel/test_pairtab_preprocess.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# SPDX-License-Identifier: LGPL-3.0-or-later
import copy
import os
import tempfile
import unittest
from unittest.mock import (
patch,
Expand Down Expand Up @@ -275,5 +278,133 @@ def test_preprocess(self) -> None:
)


class TestPairTabGridSpacing(unittest.TestCase):
@patch("numpy.loadtxt")
def test_non_uniform_grid(self, mock_loadtxt) -> None:
mock_loadtxt.return_value = np.array(
[
[0.00, 1.0],
[0.01, 0.8],
[0.02, 0.6],
[0.09, 0.3],
[0.16, 0.0],
]
)
with self.assertRaisesRegex(ValueError, "evenly spaced"):
PairTab(filename="dummy_path", rcut=0.16)

@patch("numpy.loadtxt")
def test_duplicate_distances(self, mock_loadtxt) -> None:
mock_loadtxt.return_value = np.array(
[
[0.01, 1.0],
[0.01, 0.8],
[0.01, 0.6],
[0.01, 0.0],
]
)
with self.assertRaisesRegex(ValueError, "strictly increasing"):
PairTab(filename="dummy_path", rcut=0.04)

@patch("numpy.loadtxt")
def test_descending_grid(self, mock_loadtxt) -> None:
mock_loadtxt.return_value = np.array(
[
[0.04, 1.0],
[0.03, 0.8],
[0.02, 0.6],
[0.01, 0.0],
]
)
with self.assertRaisesRegex(ValueError, "strictly increasing"):
PairTab(filename="dummy_path", rcut=0.04)

@patch("numpy.loadtxt")
def test_non_uniform_fine_grid(self, mock_loadtxt) -> None:
mock_loadtxt.return_value = np.array(
[
[0.0, 1.0],
[1e-10, 0.8],
[1.1e-9, 0.6],
[2.1e-9, 0.3],
[3.1e-9, 0.0],
]
)
with self.assertRaisesRegex(ValueError, "evenly spaced"):
PairTab(filename="dummy_path", rcut=3.1e-9)

@patch("numpy.loadtxt")
def test_uniform_fine_grid(self, mock_loadtxt) -> None:
mock_loadtxt.return_value = np.array(
[
[0.0, 1.0],
[1e-9, 0.8],
[2e-9, 0.6],
[3e-9, 0.3],
[4e-9, 0.0],
]
)
tab = PairTab(filename="dummy_path", rcut=4e-9)
self.assertAlmostEqual(tab.hh, 1e-9)

@patch("numpy.loadtxt")
def test_failed_reinit_keeps_state(self, mock_loadtxt) -> None:
uniform = np.array(
[
[0.00, 1.0],
[0.01, 0.8],
[0.02, 0.6],
[0.03, 0.3],
[0.04, 0.0],
]
)
mock_loadtxt.return_value = uniform
tab = PairTab(filename="dummy_path", rcut=0.04)
expected = copy.deepcopy(tab.serialize())

mock_loadtxt.return_value = np.array(
[
[0.00, 1.0],
[0.01, 0.8],
[0.02, 0.6],
[0.09, 0.3],
[0.16, 0.0],
]
)
with self.assertRaisesRegex(ValueError, "evenly spaced"):
tab.reinit(filename="dummy_path", rcut=0.16)

actual = tab.serialize()
for key in ("rmin", "rmax", "hh", "ntypes", "rcut", "nspline"):
self.assertEqual(actual[key], expected[key])
for key in ("vdata", "tab_info", "tab_data"):
np.testing.assert_array_equal(
actual["@variables"][key], expected["@variables"][key]
)

@patch("numpy.loadtxt")
def test_uniform_grid(self, mock_loadtxt) -> None:
mock_loadtxt.return_value = np.array(
[
[0.00, 1.0],
[0.01, 0.8],
[0.02, 0.6],
[0.03, 0.3],
[0.04, 0.0],
]
)
tab = PairTab(filename="dummy_path", rcut=0.04)
np.testing.assert_allclose(tab.hh, 0.01)

def test_uniform_grid_rounded_text_precision(self) -> None:
rr = np.linspace(0.0, 6.0, 1000)
ee = np.exp(-rr)
with tempfile.TemporaryDirectory() as tmpdir:
path = os.path.join(tmpdir, "table.txt")
np.savetxt(path, np.stack((rr, ee), axis=1), fmt="%.6f")
tab = PairTab(filename=path)
self.assertAlmostEqual(tab.hh, rr[1] - rr[0], places=6)


if __name__ == "__main__":
unittest.main(warnings="ignore")