Skip to content
Open
47 changes: 39 additions & 8 deletions deepmd/utils/pair_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,48 @@ 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)))
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")