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
52 changes: 48 additions & 4 deletions dpdata/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -1481,9 +1481,32 @@ def __add__(self, others):
raise RuntimeError("Unspported data structure")

@classmethod
def from_file(cls, file_name, fmt: str, **kwargs: Any):
def from_file(
cls, file_name, fmt: str, *, labeled: bool = True, **kwargs: Any
) -> MultiSystems:
"""Load multiple systems from a file or directory.

Parameters
----------
file_name
Source accepted by the selected format backend.
fmt : str
Format identifier, such as ``"deepmd/npy/mixed"``.
labeled : bool, default=True
Load :class:`LabeledSystem` objects when true. Set this to false for
coordinate-only data that does not contain energies or forces.
**kwargs
Additional arguments forwarded to the format backend.

Returns
-------
MultiSystems
Systems reconstructed from the source.
"""
multi_systems = cls()
multi_systems.load_systems_from_file(file_name=file_name, fmt=fmt, **kwargs)
multi_systems.load_systems_from_file(
file_name=file_name, fmt=fmt, labeled=labeled, **kwargs
)
return multi_systems

@classmethod
Expand All @@ -1506,10 +1529,31 @@ def from_dir(
)
return multi_systems

def load_systems_from_file(self, file_name=None, fmt: str | None = None, **kwargs):
def load_systems_from_file(
self,
file_name=None,
fmt: str | None = None,
*,
labeled: bool = True,
**kwargs,
):
"""Load systems into this collection.

``labeled=False`` selects regular :class:`System` objects, which is
required for DeepMD datasets that omit label arrays.
"""
assert fmt is not None
fmt = fmt.lower()
return self.from_fmt_obj(load_format(fmt), file_name, **kwargs)
try:
return self.from_fmt_obj(
load_format(fmt), file_name, labeled=labeled, **kwargs
)
except DataError as exc:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this addresses both of my earlier points -- the signature is explicit now, and the default call gives a real pointer instead of a bare energies not found in data.

One thing to tighten: this wrapper is gated on the format name but not on the cause. Any DataError raised while loading a mixed dataset now gets "pass labeled=False" appended -- including a genuinely labeled dataset that fails for an unrelated reason, e.g. a corrupt or truncated force.npy. In that case the message sends the user in exactly the wrong direction. Narrowing to the missing-label case (checking for the absent energy/force file, or matching the specific condition) would be safer, and no test currently covers that misleading path.

Two smaller notes:

  • The hint only fires via from_file / load_systems_from_file; MultiSystems().from_fmt_obj(...) still emits the bare message.
  • The concatenation is missing a separator, so it currently reads energies not found in data For coordinate-only mixed datasets, pass labeled=False.

if labeled and fmt in {"deepmd/npy/mixed", "deepmd/hdf5/mixed"}:
raise DataError(
f"{exc} For coordinate-only mixed datasets, pass labeled=False."
) from exc
raise

def get_nframes(self) -> int:
"""Returns number of frames in all systems."""
Expand Down
43 changes: 43 additions & 0 deletions tests/test_deepmd_mixed.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import os
import shutil
import tempfile
import unittest
from glob import glob
from inspect import Parameter, signature

import numpy as np
from comp_sys import (
Expand All @@ -17,10 +19,51 @@

from dpdata.data_type import (
Axis,
DataError,
DataType,
)


class TestMixedMultiSystemsUnlabeled(unittest.TestCase):
"""Regression coverage for loading coordinate-only mixed datasets."""

def test_from_file_unlabeled_round_trip(self):
labeled_parameter = signature(dpdata.MultiSystems.from_file).parameters[
"labeled"
]
self.assertIs(labeled_parameter.kind, Parameter.KEYWORD_ONLY)
self.assertIs(labeled_parameter.default, True)

system = dpdata.System("poscars/POSCAR.h2o.md", fmt="vasp/poscar")

with tempfile.TemporaryDirectory() as tmpdir:
mixed_dir = os.path.join(tmpdir, "mixed")
dpdata.MultiSystems(system).to("deepmd/npy/mixed", mixed_dir)

# The default remains labeled for backward compatibility, but the
# error now points coordinate-only users to the public flag.
with self.assertRaisesRegex(DataError, "pass labeled=False"):
dpdata.MultiSystems.from_file(
mixed_dir,
fmt="deepmd/npy/mixed",
)

# The explicit, discoverable flag selects ordinary System objects.
systems = dpdata.MultiSystems.from_file(
mixed_dir, fmt="deepmd/npy/mixed", labeled=False
Comment thread
njzjz-bot marked this conversation as resolved.
)

self.assertEqual(len(systems), 1)
loaded = next(iter(systems.systems.values()))
self.assertIs(type(loaded), dpdata.System)
self.assertNotIn("energies", loaded.data)
np.testing.assert_array_equal(
loaded.data["atom_types"], system.data["atom_types"]
)
np.testing.assert_allclose(loaded.data["cells"], system.data["cells"])
np.testing.assert_allclose(loaded.data["coords"], system.data["coords"])


class TestMixedMultiSystemsDumpLoad(
unittest.TestCase, CompLabeledMultiSys, MultiSystems, MSAllIsNoPBC
):
Expand Down
Loading