From 8e9dd508afe51f1811018eb731b76080e93abb7a Mon Sep 17 00:00:00 2001 From: hcustc <268120833+hcustc@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:17:46 +0800 Subject: [PATCH 1/3] fix(dpmodel): preserve nopbc batch semantics Convert non-periodic default_mesh encodings to box=None before normalize_batch drops the legacy metadata. This prevents pt_expt neighbor construction from treating zero box placeholders as periodic cells.\n\nAdd regression coverage for standard and mixed-type mesh encodings, periodic box preservation, and a real nopbc NPY data system. --- deepmd/dpmodel/utils/batch.py | 7 ++ source/tests/common/test_batch_nopbc.py | 90 +++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 source/tests/common/test_batch_nopbc.py diff --git a/deepmd/dpmodel/utils/batch.py b/deepmd/dpmodel/utils/batch.py index d5657fc76d..3e7e656fbe 100644 --- a/deepmd/dpmodel/utils/batch.py +++ b/deepmd/dpmodel/utils/batch.py @@ -37,6 +37,8 @@ def normalize_batch(batch: dict[str, Any]) -> dict[str, Any]: * ``"type"`` is renamed to ``"atype"`` (int64). * ``"natoms_vec"`` (1-D) is tiled to 2-D ``[nframes, 2+ntypes]`` and stored as ``"natoms"``. + * Non-periodic ``default_mesh`` encodings convert the zero ``box`` + placeholder to ``None``. * ``find_*`` flags are converted to ``np.bool_``. * Metadata keys (``default_mesh``, ``sid``, ``fid``) are dropped. @@ -56,6 +58,8 @@ def normalize_batch(batch: dict[str, Any]) -> dict[str, Any]: Normalized batch dict (new dict; the input is not mutated). """ out: dict[str, Any] = {} + default_mesh = batch.get("default_mesh") + is_nonperiodic = default_mesh is not None and np.size(default_mesh) in (0, 1) for key, val in batch.items(): if key in _DROP_KEYS: @@ -74,6 +78,9 @@ def normalize_batch(batch: dict[str, Any]) -> dict[str, Any]: else: out[key] = val + if is_nonperiodic and "box" in out: + out["box"] = None + if out.get("charge_spin") is not None and bool(out.get("find_charge_spin", True)): validate_charge_states(out["charge_spin"]) diff --git a/source/tests/common/test_batch_nopbc.py b/source/tests/common/test_batch_nopbc.py new file mode 100644 index 0000000000..b564f11f15 --- /dev/null +++ b/source/tests/common/test_batch_nopbc.py @@ -0,0 +1,90 @@ +# SPDX-License-Identifier: LGPL-3.0-or-later +"""Preserve periodic-boundary semantics across batch normalization.""" + +import tempfile +import unittest +from pathlib import ( + Path, +) + +import numpy as np + +from deepmd.dpmodel.utils.batch import ( + normalize_batch, + split_batch, +) +from deepmd.utils.data_system import ( + DeepmdDataSystem, +) + + +class TestNormalizeBatchPBC(unittest.TestCase): + """Translate the legacy mesh encoding before dropping its metadata.""" + + @staticmethod + def _batch(default_mesh_size: int, box: np.ndarray) -> dict: + return { + "coord": np.zeros((1, 2, 3)), + "type": np.zeros((1, 2), dtype=np.int32), + "box": box, + "default_mesh": np.zeros(default_mesh_size, dtype=np.int32), + } + + def test_nonperiodic_box_is_none(self) -> None: + for mixed_type, mesh_size in ((False, 0), (True, 1)): + with self.subTest(mixed_type=mixed_type): + box = np.zeros((1, 9)) + batch = self._batch(mesh_size, box) + + normalized = normalize_batch(batch) + + self.assertIsNone(normalized["box"]) + self.assertNotIn("default_mesh", normalized) + self.assertIs(batch["box"], box) + + def test_periodic_box_is_preserved(self) -> None: + for mixed_type, mesh_size in ((False, 6), (True, 7)): + with self.subTest(mixed_type=mixed_type): + box = np.eye(3).reshape(1, 9) + + normalized = normalize_batch(self._batch(mesh_size, box)) + + self.assertIs(normalized["box"], box) + + +class TestDeepmdDataSystemNopbc(unittest.TestCase): + """A valid nopbc NPY system must reach the model without a box.""" + + def test_zero_box_placeholder_is_removed(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + system = Path(tmpdir) + set_dir = system / "set.000" + set_dir.mkdir() + (system / "type.raw").write_text("0 0\n") + (system / "type_map.raw").write_text("H\n") + (system / "nopbc").touch() + np.save( + set_dir / "coord.npy", + np.array([[0.0, 0.0, 0.0, 0.74, 0.0, 0.0]]), + ) + + data_system = DeepmdDataSystem( + [str(system)], + batch_size=1, + test_size=1, + rcut=6.0, + type_map=["H"], + trn_all_set=True, + shuffle_test=False, + ) + raw = data_system.get_batch() + inputs, _ = split_batch(normalize_batch(raw)) + + self.assertFalse(data_system.data_systems[0].pbc) + self.assertEqual(raw["default_mesh"].size, 0) + self.assertTrue(np.allclose(raw["box"], 0.0)) + self.assertIsNone(inputs["box"]) + + +if __name__ == "__main__": + unittest.main() From 1dcebad5e05842d2128f309ef29be6a7a22169eb Mon Sep 17 00:00:00 2001 From: hcustc <268120833+hcustc@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:36:55 +0800 Subject: [PATCH 2/3] docs(dpmodel): clarify nopbc normalization Describe the default_mesh conversion in terms of the canonical model input rather than implying that normalize_batch inspects box values. --- deepmd/dpmodel/utils/batch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deepmd/dpmodel/utils/batch.py b/deepmd/dpmodel/utils/batch.py index 3e7e656fbe..debff523c2 100644 --- a/deepmd/dpmodel/utils/batch.py +++ b/deepmd/dpmodel/utils/batch.py @@ -37,8 +37,8 @@ def normalize_batch(batch: dict[str, Any]) -> dict[str, Any]: * ``"type"`` is renamed to ``"atype"`` (int64). * ``"natoms_vec"`` (1-D) is tiled to 2-D ``[nframes, 2+ntypes]`` and stored as ``"natoms"``. - * Non-periodic ``default_mesh`` encodings convert the zero ``box`` - placeholder to ``None``. + * Non-periodic ``default_mesh`` encodings set the canonical model input + ``box`` to ``None``. * ``find_*`` flags are converted to ``np.bool_``. * Metadata keys (``default_mesh``, ``sid``, ``fid``) are dropped. From 4399883b84223b5bf9cbf2c4e303538747a79fc4 Mon Sep 17 00:00:00 2001 From: hcustc <268120833+hcustc@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:39:23 +0800 Subject: [PATCH 3/3] refactor(dpmodel): clarify nopbc mesh semantics --- deepmd/dpmodel/utils/batch.py | 7 +++++-- source/tests/common/test_batch_nopbc.py | 27 ++++++++++++++++++++----- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/deepmd/dpmodel/utils/batch.py b/deepmd/dpmodel/utils/batch.py index debff523c2..340225c060 100644 --- a/deepmd/dpmodel/utils/batch.py +++ b/deepmd/dpmodel/utils/batch.py @@ -59,7 +59,10 @@ def normalize_batch(batch: dict[str, Any]) -> dict[str, Any]: """ out: dict[str, Any] = {} default_mesh = batch.get("default_mesh") - is_nonperiodic = default_mesh is not None and np.size(default_mesh) in (0, 1) + mesh_says_nonperiodic = default_mesh is not None and np.size(default_mesh) in ( + 0, + 1, + ) for key, val in batch.items(): if key in _DROP_KEYS: @@ -78,7 +81,7 @@ def normalize_batch(batch: dict[str, Any]) -> dict[str, Any]: else: out[key] = val - if is_nonperiodic and "box" in out: + if mesh_says_nonperiodic and "box" in out: out["box"] = None if out.get("charge_spin") is not None and bool(out.get("find_charge_spin", True)): diff --git a/source/tests/common/test_batch_nopbc.py b/source/tests/common/test_batch_nopbc.py index b564f11f15..730a3f502a 100644 --- a/source/tests/common/test_batch_nopbc.py +++ b/source/tests/common/test_batch_nopbc.py @@ -31,9 +31,9 @@ def _batch(default_mesh_size: int, box: np.ndarray) -> dict: } def test_nonperiodic_box_is_none(self) -> None: - for mixed_type, mesh_size in ((False, 0), (True, 1)): - with self.subTest(mixed_type=mixed_type): - box = np.zeros((1, 9)) + for mesh_size in (0, 1): + with self.subTest(mesh_size=mesh_size): + box = np.eye(3).reshape(1, 9) batch = self._batch(mesh_size, box) normalized = normalize_batch(batch) @@ -43,14 +43,31 @@ def test_nonperiodic_box_is_none(self) -> None: self.assertIs(batch["box"], box) def test_periodic_box_is_preserved(self) -> None: - for mixed_type, mesh_size in ((False, 6), (True, 7)): - with self.subTest(mixed_type=mixed_type): + for mesh_size in (6, 7): + with self.subTest(mesh_size=mesh_size): box = np.eye(3).reshape(1, 9) normalized = normalize_batch(self._batch(mesh_size, box)) self.assertIs(normalized["box"], box) + def test_missing_default_mesh_preserves_box(self) -> None: + box = np.eye(3).reshape(1, 9) + batch = self._batch(0, box) + del batch["default_mesh"] + + normalized = normalize_batch(batch) + + self.assertIs(normalized["box"], box) + + def test_nonperiodic_mesh_without_box_does_not_add_box(self) -> None: + batch = self._batch(0, np.eye(3).reshape(1, 9)) + del batch["box"] + + normalized = normalize_batch(batch) + + self.assertNotIn("box", normalized) + class TestDeepmdDataSystemNopbc(unittest.TestCase): """A valid nopbc NPY system must reach the model without a box."""