From 642a745d4d5444825aa28a54334e087add69af3b Mon Sep 17 00:00:00 2001 From: Cedric Conday <277679649+CedricConday@users.noreply.github.com> Date: Thu, 9 Jul 2026 17:53:42 +0000 Subject: [PATCH] fix(deepmd/hdf5): skip empty optional frame arrays on dump (#996) The deepmd/hdf5 writer reshaped and wrote optional frame arrays (e.g. forces or virials that are empty when cal_force/cal_stress is disabled) as a (nframes, 0) dataset, which the loader then failed to reshape back to the full shape, raising 'cannot reshape array of size 0'. Skip empty optional arrays on dump, matching the existing deepmd/raw and deepmd/npy writers. Non-empty arrays are unaffected. --- dpdata/formats/deepmd/hdf5.py | 7 ++++++ tests/test_deepmd_hdf5.py | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/dpdata/formats/deepmd/hdf5.py b/dpdata/formats/deepmd/hdf5.py index 6349a960..5885bb19 100644 --- a/dpdata/formats/deepmd/hdf5.py +++ b/dpdata/formats/deepmd/hdf5.py @@ -201,6 +201,13 @@ def dump( for dt, prop in data_types.items(): if dt in data: if prop["dump"]: + if nframes > 0 and np.asarray(data[dt]).size == 0: + # An optional frame property (e.g. forces/virials when + # cal_force/cal_stress is disabled) may be empty while the + # system still has frames. Skip it instead of writing a + # (nframes, 0) dataset that cannot be reshaped on load. This + # matches the behaviour of the deepmd/raw and deepmd/npy writers. + continue ddata = np.reshape(data[dt], prop["shape"]) if np.issubdtype(ddata.dtype, np.floating): ddata = ddata.astype(comp_prec) diff --git a/tests/test_deepmd_hdf5.py b/tests/test_deepmd_hdf5.py index 9d65f29a..a198bc3c 100644 --- a/tests/test_deepmd_hdf5.py +++ b/tests/test_deepmd_hdf5.py @@ -300,3 +300,50 @@ def test_regular_hdf5_groups_are_not_mixed(self): ) self.assertEqual(len(systems), 0) + + +class TestHDF5EmptyOptionalArray(unittest.TestCase): + """Regression test for #996. + + An empty optional frame array (e.g. forces when they are not computed) must not + break the deepmd/hdf5 write/read round-trip. Previously the writer stored a + ``(nframes, 0)`` dataset that could not be reshaped on load. + """ + + def tearDown(self): + if os.path.isfile("tmp.deepmd.empty.hdf5"): + os.remove("tmp.deepmd.empty.hdf5") + + @staticmethod + def _make(forces): + return dpdata.LabeledSystem( + data={ + "atom_names": ["H"], + "atom_numbs": [1], + "atom_types": np.array([0]), + "cells": np.eye(3).reshape(1, 3, 3), + "coords": np.zeros((1, 1, 3)), + "energies": np.zeros(1), + "orig": np.zeros(3), + "forces": forces, + } + ) + + def test_empty_forces_round_trip(self): + system = self._make(np.zeros((1, 0, 3))) + system.to("deepmd/hdf5", "tmp.deepmd.empty.hdf5") + reloaded = dpdata.LabeledSystem("tmp.deepmd.empty.hdf5", fmt="deepmd/hdf5") + # The empty optional array is skipped rather than written as (nframes, 0). + self.assertNotIn("forces", reloaded.data) + np.testing.assert_allclose(reloaded["coords"], system["coords"]) + + def test_real_forces_preserved(self): + forces = np.arange(3, dtype=float).reshape(1, 1, 3) + system = self._make(forces) + system.to("deepmd/hdf5", "tmp.deepmd.empty.hdf5") + reloaded = dpdata.LabeledSystem("tmp.deepmd.empty.hdf5", fmt="deepmd/hdf5") + np.testing.assert_allclose(reloaded["forces"], forces) + + +if __name__ == "__main__": + unittest.main()