Skip to content

Commit e155bfe

Browse files
authored
Merge pull request #25 from zhaiwenxi/feat-dpa-adapt-scripted-training-predict
feat: support scripted prediction for dpa adapt training
2 parents b8e0868 + 771bcdd commit e155bfe

1 file changed

Lines changed: 144 additions & 3 deletions

File tree

dpa_adapt/finetuner.py

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
import logging
88
import os
9+
import re
10+
import subprocess
911
from pathlib import (
1012
Path,
1113
)
@@ -871,6 +873,128 @@ def _fit_training(self, train_data, valid_data, type_map):
871873
self._fitted = True
872874
return ckpt_path
873875

876+
def _latest_training_checkpoint(self) -> str:
877+
ckpts = list(Path(self.output_dir).glob("model.ckpt-*.pt"))
878+
if not ckpts:
879+
raise RuntimeError(
880+
f"No model.ckpt-*.pt found in {self.output_dir}; call fit() first."
881+
)
882+
883+
def step_of(path):
884+
return int(path.stem.split("-")[-1])
885+
886+
return str(max(ckpts, key=step_of))
887+
888+
@staticmethod
889+
def _expand_system_specs(data) -> list[str]:
890+
import glob
891+
892+
patterns = [data] if isinstance(data, str) else list(data)
893+
systems = []
894+
for pattern in patterns:
895+
matches = sorted(glob.glob(str(pattern)))
896+
systems.extend(matches or [str(pattern)])
897+
898+
seen = set()
899+
systems = [s for s in systems if not (s in seen or seen.add(s))]
900+
if not systems:
901+
raise DPADataError(f"No systems matched {data!r}.")
902+
return systems
903+
904+
def _run_training_predict(self, data, fmt=None) -> DotDict:
905+
"""Run ``dp --pt test`` and parse property predictions from detail files."""
906+
from dpa_adapt.trainer import (
907+
DPATrainer,
908+
)
909+
910+
if fmt is not None:
911+
raise ValueError(
912+
"fmt is not supported for frozen_head/finetune predict(); "
913+
"provide deepmd/npy system directories."
914+
)
915+
916+
ckpt = self._latest_training_checkpoint()
917+
systems = self._expand_system_specs(data)
918+
919+
output_dir = Path(self.output_dir)
920+
output_dir.mkdir(parents=True, exist_ok=True)
921+
datafile = output_dir / "predict_systems.txt"
922+
datafile.write_text("\n".join(systems) + "\n")
923+
924+
detail_prefix = output_dir / "predict_detail"
925+
for old in output_dir.glob(f"{detail_prefix.name}.property.out.*"):
926+
old.unlink()
927+
928+
cmd = [
929+
"dp",
930+
"--pt",
931+
"test",
932+
"-m",
933+
ckpt,
934+
"-f",
935+
str(datafile),
936+
"-n",
937+
"999999",
938+
"-d",
939+
str(detail_prefix),
940+
]
941+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
942+
combined = result.stdout + "\n" + result.stderr
943+
944+
detail_files = sorted(
945+
output_dir.glob(f"{detail_prefix.name}.property.out.*"),
946+
key=lambda p: int(p.name.rsplit(".", 1)[-1]),
947+
)
948+
if not detail_files:
949+
raise RuntimeError(
950+
"dp --pt test completed but no property detail files were written. "
951+
f"Command was: {' '.join(cmd)}"
952+
)
953+
954+
rows = []
955+
for path in detail_files:
956+
arr = np.loadtxt(path)
957+
arr = np.asarray(arr, dtype=float)
958+
if arr.ndim == 1:
959+
arr = arr.reshape(1, -1)
960+
if arr.shape[1] < 2:
961+
raise RuntimeError(
962+
f"Expected at least two columns in {path}, got shape {arr.shape}."
963+
)
964+
rows.append(arr[:, :2])
965+
966+
values = np.concatenate(rows, axis=0)
967+
if values.shape[0] % self.task_dim != 0:
968+
raise RuntimeError(
969+
f"Could not reshape property detail rows {values.shape[0]} "
970+
f"into task_dim={self.task_dim}."
971+
)
972+
973+
values = values.reshape(-1, self.task_dim, 2)
974+
labels = values[:, :, 0]
975+
predictions = values[:, :, 1]
976+
if self.task_dim == 1:
977+
labels = labels.reshape(-1, 1)
978+
predictions = predictions.reshape(-1, 1)
979+
980+
metrics = DPATrainer._parse_test_output(combined)
981+
n_sys_match = re.search(
982+
r"number of systems\s*[:=]?\s*(\d+)", combined, re.IGNORECASE
983+
)
984+
n_systems = int(n_sys_match.group(1)) if n_sys_match else len(systems)
985+
return DotDict(
986+
{
987+
"predictions": predictions,
988+
"labels": labels,
989+
"mae": metrics["mae"],
990+
"rmse": metrics["rmse"],
991+
"n_frames": metrics["n_frames"],
992+
"n_systems": n_systems,
993+
"detail_prefix": str(detail_prefix),
994+
"_raw_stdout": combined,
995+
}
996+
)
997+
874998
# -------------------------------------------------------------------
875999
# fit (dispatch)
8761000
# -------------------------------------------------------------------
@@ -1048,10 +1172,12 @@ def _fit_sklearn(
10481172

10491173
def predict(self, data, fmt=None) -> DotDict:
10501174
"""
1051-
Extract features and run the fitted sklearn predictor.
1175+
Predict with the adapted model.
10521176
1053-
fparam is automatically read from ``set.*/fparam.npy`` when the
1054-
model was fit with ``fparam_dim > 0``.
1177+
``frozen_sklearn`` extracts features and runs the fitted sklearn
1178+
predictor. ``frozen_head`` and ``finetune`` run ``dp --pt test`` on
1179+
the latest ``model.ckpt-*.pt`` in ``output_dir`` and parse the
1180+
property predictions from DeepMD's detail files.
10551181
10561182
Parameters
10571183
----------
@@ -1065,6 +1191,9 @@ def predict(self, data, fmt=None) -> DotDict:
10651191
DotDict
10661192
``predictions`` : np.ndarray, shape (n_frames, task_dim)
10671193
"""
1194+
if self.strategy in {"frozen_head", "finetune"}:
1195+
return self._run_training_predict(data, fmt=fmt)
1196+
10681197
if not self._fitted:
10691198
raise RuntimeError(
10701199
"predict() was called before fit(). Train the model with fit() first."
@@ -1105,6 +1234,18 @@ def evaluate(self, data, fmt=None) -> DotDict:
11051234
predictions : np.ndarray, shape (n_frames, task_dim)
11061235
labels : np.ndarray, shape (n_frames, task_dim)
11071236
"""
1237+
if self.strategy in {"frozen_head", "finetune"}:
1238+
result = self._run_training_predict(data, fmt=fmt)
1239+
labels = result.labels
1240+
predictions = result.predictions
1241+
err = predictions - labels
1242+
ss_res = np.sum(err**2)
1243+
ss_tot = np.sum((labels - labels.mean()) ** 2)
1244+
result["r2"] = (
1245+
float(1.0 - ss_res / ss_tot) if ss_tot > 0 else float("nan")
1246+
)
1247+
return result
1248+
11081249
result = self.predict(data, fmt=fmt)
11091250
predictions = result.predictions
11101251

0 commit comments

Comments
 (0)