66
77import logging
88import os
9+ import re
10+ import subprocess
911from 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