Skip to content

Commit ba1f17c

Browse files
committed
fix(dpa-adapt): resolve type-map validation, cache identity, label loading, and fparam_dim validation issues
- Split try/except in _resolve_type_maps so unsupported-element errors propagate instead of being silently swallowed as missing-atom-names - Make read_data_type_map_union skip all-Type_* placeholder names, consistent with _read_data_type_map, so MFT does not reject valid raw-index data - Add set.*/{key}.npy direct fallback to load_dataset for custom label files (e.g. homo.npy, bandgap.npy) not loaded into dpdata.System.data - Replace first/last-64 sampling in _system_fingerprint with full-array hashing so descriptor cache keys correctly invalidate when structures change - Validate fparam_dim as non-negative int in DPAFineTuner.__init__, matching DPATrainer and MFTFineTuner - Add scikit-learn to the test extra so DPA-ADAPT tests can run in all CI paths
1 parent 5f5e735 commit ba1f17c

5 files changed

Lines changed: 66 additions & 27 deletions

File tree

dpa_adapt/data/dataset.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ def load_dataset(
6464
*every* candidate was skipped, in which case a ``DPADataError``
6565
is raised (fail-fast for training workflows).
6666
"""
67+
from dpa_adapt.data.loader import (
68+
_get_source,
69+
)
70+
6771
systems = load_data(data)
6872

6973
resolved_key = _resolve_label_key(label_key)
@@ -76,6 +80,25 @@ def load_dataset(
7680
# ``data`` dict; label_key (after alias resolution) presence is the litmus test.
7781
if resolved_key in system.data:
7882
validated.append(system)
83+
continue
84+
85+
# Fallback: check set.*/{key}.npy directly (same logic as
86+
# _load_labels() in finetuner.py). Custom labels such as
87+
# "homo.npy", "bandgap.npy" under set.*/ are not generally
88+
# loaded into dpdata.System.data, so this direct check prevents
89+
# valid datasets from being incorrectly skipped.
90+
source = _get_source(system)
91+
if source is not None:
92+
source_path = Path(source)
93+
set_dirs = sorted(source_path.glob("set.*"))
94+
for sd in set_dirs:
95+
if (sd / f"{resolved_key}.npy").exists():
96+
validated.append(system)
97+
break
98+
else:
99+
# None of the set.* dirs had the label file.
100+
identifier = getattr(system, "_dpa_source", f"system[{i}]")
101+
skipped.append(f"{identifier} (missing {resolved_key!r})")
79102
else:
80103
identifier = getattr(system, "_dpa_source", f"system[{i}]")
81104
skipped.append(f"{identifier} (missing {resolved_key!r})")

dpa_adapt/data/desc_cache.py

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -47,44 +47,46 @@ def _cache_dir() -> Path:
4747

4848

4949
# ---------------------------------------------------------------------------
50-
# lightweight system fingerprint (O(1) on array size, O(n) on atom count)
50+
# system fingerprint (O(n) over the full descriptor-relevant arrays)
5151
# ---------------------------------------------------------------------------
5252

5353

54-
def _system_fingerprint(system: dpdata.System) -> str:
55-
"""Return a short hex fingerprint for a dpdata System.
54+
def _hash_array(h: "hashlib._Hash", arr: np.ndarray) -> None:
55+
"""Fold an array's shape, dtype, and full byte content into *h*.
56+
57+
The contiguous buffer is fed to :meth:`hashlib._Hash.update` directly via
58+
the buffer protocol, so no large intermediate ``bytes`` copy is made.
59+
"""
60+
arr = np.ascontiguousarray(arr)
61+
h.update(str(arr.shape).encode())
62+
h.update(str(arr.dtype).encode())
63+
h.update(arr)
5664

57-
Uses only metadata and a tiny sample of coordinate data so it is fast
58-
even for large (10⁵+ frame) systems. Collisions are possible in
59-
principle but vanishingly unlikely in practice given the combination of
60-
shape, dtype, atom_types, and first/last bytes.
65+
66+
def _system_fingerprint(system: dpdata.System) -> str:
67+
"""Return a hex fingerprint for a dpdata System.
68+
69+
Hashes the *full* contents of the descriptor-relevant arrays — ``coords``,
70+
``cells`` and ``atom_types`` — together with ``atom_names``. Sampling
71+
only the first/last few entries (as an earlier version did) let any change
72+
in the middle of a long trajectory keep the same key, so the cache could
73+
return descriptors extracted from a different structure. Hashing every
74+
element costs O(total array size), but that is negligible next to the
75+
descriptor extraction the cache guards, and it makes the key collision-safe
76+
for changed systems.
6177
"""
6278
d = system.data
63-
coords = np.asarray(d["coords"])
64-
atom_types = np.asarray(d["atom_types"])
6579

6680
h = hashlib.sha1()
67-
# structural identity
68-
h.update(str(coords.shape).encode())
69-
h.update(str(coords.dtype).encode())
70-
h.update(atom_types.tobytes())
81+
# atom-type identity
82+
_hash_array(h, np.asarray(d["atom_types"]))
7183
# atom_names (if present)
7284
names = d.get("atom_names", [])
7385
h.update("|".join(str(n) for n in names).encode())
74-
# first / last 64 bytes of coords (captures actual content without
75-
# hashing the entire array)
76-
if coords.size > 0:
77-
flat = coords.ravel()
78-
h.update(flat[: min(64, len(flat))].tobytes())
79-
h.update(flat[-min(64, len(flat)) :].tobytes())
80-
# same for cells, if present
86+
# full geometry
87+
_hash_array(h, np.asarray(d["coords"]))
8188
if "cells" in d:
82-
cells = np.asarray(d["cells"])
83-
h.update(str(cells.shape).encode())
84-
if cells.size > 0:
85-
fc = cells.ravel()
86-
h.update(fc[: min(64, len(fc))].tobytes())
87-
h.update(fc[-min(64, len(fc)) :].tobytes())
89+
_hash_array(h, np.asarray(d["cells"]))
8890
return h.hexdigest()[:16]
8991

9092

dpa_adapt/data/type_map.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ def read_data_type_map_union(systems: list) -> list[str]:
9595
elems: set[str] = set()
9696
for sys in systems:
9797
names = sys.data.get("atom_names", [])
98+
# dpdata generates "Type_0", "Type_1", ... when no type_map.raw was
99+
# present. Treat an all-placeholder type map as "no real atom_names"
100+
# so that callers allow raw atom indices instead of rejecting valid
101+
# data as unsupported elements (consistent with _read_data_type_map
102+
# in finetuner.py).
103+
if names and all(str(n).startswith("Type_") for n in names):
104+
continue
98105
for name in names:
99106
if name:
100107
elems.add(str(name))

dpa_adapt/finetuner.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,10 @@ def __init__(
898898
f"strategy must be one of {sorted(self._VALID_STRATEGIES)}; "
899899
f"got {strategy!r}"
900900
)
901+
if not isinstance(fparam_dim, int) or fparam_dim < 0:
902+
raise ValueError(
903+
f"fparam_dim must be a non-negative int; got {fparam_dim!r}."
904+
)
901905

902906
self.strategy = strategy
903907

@@ -1099,9 +1103,10 @@ def _resolve_type_maps(self, train_data: str | list[str]) -> list[str]:
10991103

11001104
try:
11011105
elements = read_data_type_map_union(systems)
1102-
validate_type_map_subset(elements, tm, label="train data")
11031106
except ValueError:
11041107
pass # no atom_names — deepmd uses raw atom indices
1108+
else:
1109+
validate_type_map_subset(elements, tm, label="train data")
11051110

11061111
return tm
11071112

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ test = [
9191
"pytest-split",
9292
"pytest-timeout",
9393
"dpgui",
94+
# DPA-ADAPT tests import sklearn via dpa_adapt.cv at module load time.
95+
"scikit-learn",
9496
# to support Array API 2024.12
9597
'array-api-strict>=2.2;python_version>="3.9"',
9698
]

0 commit comments

Comments
 (0)