Skip to content

Commit 26fccd1

Browse files
authored
Merge pull request #21 from zirenjin/master
feat(dpa_adapt): auto-read fparam.npy for all strategies, remove condition={temperature...} for frozen_sklearn
2 parents 9237515 + 441d31d commit 26fccd1

7 files changed

Lines changed: 122 additions & 85 deletions

File tree

doc/dpa_adapt/README.md

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -149,22 +149,19 @@ For the full option list and supported dpdata formats, see
149149

150150
### Context features (fparam)
151151

152-
fparam lets you condition the model on system-level context such as temperature, pressure, or experimental conditions.
153-
154-
**frozen_sklearn** — pass a dict of numpy arrays at fit and predict time:
152+
fparam lets you condition the model on system-level context such as temperature, humidity, pressure, or any per-frame scalar. All strategies use the same interface: place `fparam.npy` of shape `(n_frames, fparam_dim)` in each `set.*/` directory alongside `coord.npy` and declare the dimension at construction.
155153

156154
```python
157-
model.fit(train_data, conditions={"temperature": T_train})
158-
model.predict(test_data, conditions={"temperature": T_test})
159-
# ConditionManager standardizes and concatenates values to the descriptor
155+
# works identically for frozen_sklearn, frozen_head, finetune, and mft
156+
model = DPAFineTuner(strategy="frozen_sklearn", fparam_dim=2)
157+
model.fit(train_data="data/train", target_key="property")
158+
# fparam.npy is read automatically — no conditions= dict needed
160159
```
161160

162-
**frozen_head / finetune / mft** — place `fparam.npy` of shape `(nframes, fparam_dim)` in each `set.*/` directory alongside `coord.npy`, then declare the dimension at construction:
163-
164-
```python
165-
model = DPAFineTuner(strategy="finetune", fparam_dim=2)
166-
model.fit(train_data) # reads fparam.npy automatically
167-
```
161+
| Strategy | How fparam is used |
162+
|---|---|
163+
| `frozen_sklearn` | columns are standardized via `ConditionManager` and concatenated to the descriptor |
164+
| `frozen_head` / `finetune` / `mft` | passed into the fitting net as `numb_fparam` |
168165

169166
## Inference and uncertainty
170167

@@ -197,15 +194,25 @@ Uncertainty estimates can drive active learning (query most uncertain candidates
197194

198195
## Cross-validation
199196

200-
Formula-grouped splitting prevents same-composition leakage between folds:
197+
Formula-grouped splitting prevents same-composition leakage between folds.
198+
`group_by` accepts `"formula"` (uses each system's directory name as the group
199+
key — requires directories named by formula, e.g. `H2O/`, `CH4/`) or a list
200+
of labels the same length as `systems`:
201201

202202
```python
203203
from dpa_adapt import cross_validate, train_test_split, load_dataset
204204

205205
systems = load_dataset("/data/root", label_key="energy")
206+
207+
# Case 1: directory names are formulas (e.g. data/H2O/, data/CH4/)
206208
train, valid, test = train_test_split(systems, group_by="formula", seed=42)
207209

208-
result = cross_validate(model, systems, label_key="energy", cv=5, group_by="formula")
210+
# Case 2: directory names are not formulas (e.g. QM9's sys_0000, sys_0001, …)
211+
formulas = ["H2O", "H2O", "CH4", "CH4", ...] # one label per system
212+
train, valid, test = train_test_split(systems, group_by=formulas, seed=42)
213+
214+
# Cross-validate (same group_by options apply)
215+
result = cross_validate(model, systems, label_key="energy", cv=5, group_by=formulas)
209216
# → {"aggregate": {"mae_mean": ..., "rmse_std": ...}, ...}
210217
```
211218

dpa_adapt/config/manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def _build_property_fitting_net(t) -> dict:
3333
}
3434
)
3535
if getattr(t, "fparam_dim", 0) > 0:
36-
fn["fparam_dim"] = t.fparam_dim
36+
fn["numb_fparam"] = t.fparam_dim
3737
return fn
3838

3939

dpa_adapt/finetuner.py

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,33 @@ def _load_labels(
112112
return np.column_stack(columns)
113113

114114

115+
def _read_fparam_from_systems(
116+
systems: list[dpdata.System],
117+
) -> dict[str, np.ndarray] | None:
118+
"""Auto-read fparam.npy from each system's ``set.*/`` directories.
119+
120+
Returns a dict mapping ``"fparam_0"``, ``"fparam_1"``, ... to 1-D
121+
arrays of length ``n_frames_total``, suitable for passing as
122+
``conditions=`` to :meth:`ConditionManager.fit_transform`.
123+
124+
Returns ``None`` when no system has a ``set.*/fparam.npy`` file.
125+
"""
126+
all_fparams = []
127+
for system in systems:
128+
source = _get_source(system)
129+
if source is None:
130+
continue
131+
fps = sorted(Path(source).glob("set.*/fparam.npy"))
132+
if not fps:
133+
continue
134+
arrs = [np.load(str(fp)) for fp in fps]
135+
all_fparams.append(np.concatenate(arrs, axis=0))
136+
if not all_fparams:
137+
return None
138+
combined = np.concatenate(all_fparams, axis=0) # (n_frames, fparam_dim)
139+
return {f"fparam_{i}": combined[:, i] for i in range(combined.shape[1])}
140+
141+
115142
def _read_data_type_map(system) -> list[str]:
116143
"""Read element symbols from a dpdata System's ``atom_names``.
117144
@@ -560,10 +587,12 @@ class DPAFineTuner:
560587
loss_function : str
561588
``"mse"`` or ``"smooth_mae"`` (training paradigms).
562589
fparam_dim : int
563-
(frozen_head / finetune / mft only) Dimensionality of per-frame
564-
condition inputs (e.g. temperature, pressure). Requires
565-
set.*/fparam.npy of shape (n_frames, fparam_dim) in every
566-
training system. Default 0 (disabled).
590+
Dimension of per-frame context features (e.g. temperature,
591+
humidity). When > 0, ``set.*/fparam.npy`` of shape
592+
``(n_frames, fparam_dim)`` is read automatically for all
593+
strategies. For ``frozen_sklearn``, fparam columns are
594+
standardized and concatenated to the descriptor via
595+
``ConditionManager``. Default 0 (disabled).
567596
output_dir : str
568597
Directory for ``input.json``, checkpoints, and logs.
569598
save_freq, disp_freq : int
@@ -854,7 +883,6 @@ def fit(
854883
target_key=None,
855884
labels=None,
856885
fmt=None,
857-
conditions=None,
858886
aux_data=None,
859887
):
860888
"""Train the model.
@@ -879,15 +907,13 @@ def fit(
879907
(frozen_sklearn) Pre-computed labels.
880908
fmt : str, optional
881909
Reserved for future format support.
882-
conditions : dict[str, np.ndarray], optional
883-
(frozen_sklearn) Named condition arrays.
884910
aux_data : str | list[str], optional
885911
(mft only) Auxiliary training system directories. Required when
886912
``strategy='mft'``; must be absent otherwise.
887913
"""
888914
if self.strategy == "frozen_sklearn":
889915
return self._fit_sklearn(
890-
train_data, type_map, target_key, labels, fmt, conditions
916+
train_data, type_map, target_key, labels, fmt
891917
)
892918

893919
if self.strategy == "mft":
@@ -951,7 +977,6 @@ def _fit_sklearn(
951977
target_key=None,
952978
labels=None,
953979
fmt=None,
954-
conditions=None,
955980
):
956981
"""Fit the frozen-sklearn pipeline (delegates to ``_FrozenSklearnPipeline``).
957982
@@ -978,10 +1003,12 @@ def _fit_sklearn(
9781003
features = self._extract_features_cached(systems)
9791004

9801005
self._condition_manager = None
981-
if conditions is not None:
982-
self._condition_manager = ConditionManager()
983-
X_cond = self._condition_manager.fit_transform(conditions)
984-
features = np.concatenate([features, X_cond], axis=1)
1006+
if self.fparam_dim > 0:
1007+
conditions = _read_fparam_from_systems(systems)
1008+
if conditions is not None:
1009+
self._condition_manager = ConditionManager()
1010+
X_cond = self._condition_manager.fit_transform(conditions)
1011+
features = np.concatenate([features, X_cond], axis=1)
9851012

9861013
if labels is not None:
9871014
y = np.asarray(labels)
@@ -1019,19 +1046,19 @@ def _fit_sklearn(
10191046
p._condition_manager = self._condition_manager
10201047
p._fitted = True
10211048

1022-
def predict(self, data, fmt=None, conditions=None) -> DotDict:
1049+
def predict(self, data, fmt=None) -> DotDict:
10231050
"""
10241051
Extract features and run the fitted sklearn predictor.
10251052
1053+
fparam is automatically read from ``set.*/fparam.npy`` when the
1054+
model was fit with ``fparam_dim > 0``.
1055+
10261056
Parameters
10271057
----------
10281058
data : str | list[str]
10291059
Path(s) to deepmd/npy system directories.
10301060
fmt : str, optional
10311061
Reserved for future format support.
1032-
conditions : dict[str, np.ndarray], optional
1033-
Named condition arrays. Required when the model was fit with
1034-
conditions; must be absent otherwise.
10351062
10361063
Returns
10371064
-------
@@ -1047,20 +1074,20 @@ def predict(self, data, fmt=None, conditions=None) -> DotDict:
10471074
features = self._extract_features(systems)
10481075

10491076
if self._condition_manager is not None:
1077+
conditions = _read_fparam_from_systems(systems)
10501078
if conditions is None:
10511079
raise DPAConditionError(
1052-
"This model was fit with conditions. Pass conditions= to predict()."
1080+
"This model was fit with fparam but set.*/fparam.npy "
1081+
"was not found in the test data."
10531082
)
10541083
X_cond = self._condition_manager.transform(conditions)
10551084
features = np.concatenate([features, X_cond], axis=1)
1056-
elif conditions is not None:
1057-
raise DPAConditionError("This model was fit without conditions.")
10581085

10591086
raw = self.predictor.predict(features)
10601087
predictions = np.asarray(raw).reshape(-1, self._task_dim)
10611088
return DotDict({"predictions": predictions})
10621089

1063-
def evaluate(self, data, fmt=None, conditions=None) -> DotDict:
1090+
def evaluate(self, data, fmt=None) -> DotDict:
10641091
"""
10651092
Predict on ``data`` and compute evaluation metrics against stored labels.
10661093
@@ -1070,9 +1097,6 @@ def evaluate(self, data, fmt=None, conditions=None) -> DotDict:
10701097
Path(s) to deepmd/npy system directories with label files.
10711098
fmt : str, optional
10721099
Reserved for future format support.
1073-
conditions : dict[str, np.ndarray], optional
1074-
Named condition arrays. Required when the model was fit with
1075-
conditions; must be absent otherwise.
10761100
10771101
Returns
10781102
-------
@@ -1081,7 +1105,7 @@ def evaluate(self, data, fmt=None, conditions=None) -> DotDict:
10811105
predictions : np.ndarray, shape (n_frames, task_dim)
10821106
labels : np.ndarray, shape (n_frames, task_dim)
10831107
"""
1084-
result = self.predict(data, fmt=fmt, conditions=conditions)
1108+
result = self.predict(data, fmt=fmt)
10851109
predictions = result.predictions
10861110

10871111
systems = load_data(data, fmt=fmt)

dpa_adapt/predictor.py

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def __init__(self, model_path: str, n_committee: int = 1):
120120
pooling=self._pooling,
121121
)
122122

123-
def fit(self, data, target_key=None, labels=None, fmt=None, conditions=None):
123+
def fit(self, data, target_key=None, labels=None, fmt=None):
124124
"""Train committee members for uncertainty estimation.
125125
126126
Only valid when *n_committee* > 1. Clones the frozen sklearn
@@ -140,6 +140,7 @@ def fit(self, data, target_key=None, labels=None, fmt=None, conditions=None):
140140

141141
from dpa_adapt.finetuner import (
142142
_load_labels,
143+
_read_fparam_from_systems,
143144
)
144145

145146
if target_key is not None and labels is not None:
@@ -154,14 +155,14 @@ def fit(self, data, target_key=None, labels=None, fmt=None, conditions=None):
154155
features = self._extractor._extract_features(systems)
155156

156157
if self._condition_manager is not None:
158+
conditions = _read_fparam_from_systems(systems)
157159
if conditions is None:
158160
raise DPAConditionError(
159-
"This model was fit with conditions. Pass conditions= to fit()."
161+
"This model was fit with fparam but set.*/fparam.npy "
162+
"was not found in the data."
160163
)
161164
X_cond = self._condition_manager.transform(conditions)
162165
features = np.concatenate([features, X_cond], axis=1)
163-
elif conditions is not None:
164-
raise DPAConditionError("This model was fit without conditions.")
165166

166167
if labels is not None:
167168
y = np.asarray(labels)
@@ -184,44 +185,45 @@ def fit(self, data, target_key=None, labels=None, fmt=None, conditions=None):
184185
preds = preds.reshape(self.n_committee, -1, self._task_dim)
185186
self.uncertainty_threshold_ = float(np.percentile(np.std(preds, axis=0), 95))
186187

187-
def _extract_and_condition(self, data, fmt, conditions):
188-
"""Shared feature extraction + condition concatenation."""
188+
def _extract_and_condition(self, data, fmt):
189+
"""Shared feature extraction + fparam auto-read."""
190+
from dpa_adapt.finetuner import (
191+
_read_fparam_from_systems,
192+
)
193+
189194
systems = load_data(data, fmt=fmt)
190-
# Load the model first so the checkpoint type_map is available, then
191-
# validate before extracting features (extraction relies on the data
192-
# type_map being a subset of the checkpoint's).
193195
if self._extractor._model is None:
194196
self._extractor._model = self._extractor._load_descriptor_model()
195197
self._extractor._validate_type_map(self._type_map, systems)
196198
features = self._extractor._extract_features(systems)
197199

198200
if self._condition_manager is not None:
201+
conditions = _read_fparam_from_systems(systems)
199202
if conditions is None:
200203
raise DPAConditionError(
201-
"This model was fit with conditions. Pass conditions= to predict()."
204+
"This model was fit with fparam but set.*/fparam.npy "
205+
"was not found in the data."
202206
)
203207
X_cond = self._condition_manager.transform(conditions)
204208
features = np.concatenate([features, X_cond], axis=1)
205-
elif conditions is not None:
206-
raise DPAConditionError("This model was fit without conditions.")
207209

208210
return features
209211

210212
def predict(
211-
self, data, fmt=None, conditions=None, return_uncertainty=False
213+
self, data, fmt=None, return_uncertainty=False
212214
) -> DotDict:
213215
"""
214216
Run inference on ``data``.
215217
218+
fparam is automatically read from ``set.*/fparam.npy`` when the
219+
model was fit with fparam.
220+
216221
Parameters
217222
----------
218223
data : str | list[str]
219224
Path(s) to deepmd/npy system directories.
220225
fmt : str, optional
221226
Reserved for future format support.
222-
conditions : dict[str, np.ndarray], optional
223-
Named condition arrays. Required when the model was fit with
224-
conditions; must be absent otherwise.
225227
return_uncertainty : bool
226228
When True, include ``"uncertainty"`` (per-sample std) in the
227229
result. Behaviour depends on estimator type and committee
@@ -233,7 +235,7 @@ def predict(
233235
``predictions`` : np.ndarray, shape (n_frames, task_dim)
234236
``uncertainty`` : np.ndarray, shape (n_frames, task_dim) (if requested)
235237
"""
236-
features = self._extract_and_condition(data, fmt, conditions)
238+
features = self._extract_and_condition(data, fmt)
237239

238240
if return_uncertainty:
239241
return self._predict_with_uncertainty(features)
@@ -291,7 +293,7 @@ def _predict_with_uncertainty(self, features):
291293
f"with n_committee={self.n_committee}."
292294
)
293295

294-
def evaluate(self, data, fmt=None, conditions=None) -> DotDict:
296+
def evaluate(self, data, fmt=None) -> DotDict:
295297
"""
296298
Predict on ``data`` and compute evaluation metrics against stored labels.
297299
@@ -301,9 +303,6 @@ def evaluate(self, data, fmt=None, conditions=None) -> DotDict:
301303
Path(s) to deepmd/npy system directories with label files.
302304
fmt : str, optional
303305
Reserved for future format support.
304-
conditions : dict[str, np.ndarray], optional
305-
Named condition arrays. Required when the model was fit with
306-
conditions; must be absent otherwise.
307306
308307
Returns
309308
-------
@@ -319,7 +318,7 @@ def evaluate(self, data, fmt=None, conditions=None) -> DotDict:
319318
_load_labels,
320319
)
321320

322-
result = self.predict(data, fmt=fmt, conditions=conditions)
321+
result = self.predict(data, fmt=fmt)
323322
predictions = result.predictions
324323

325324
systems = load_data(data, fmt=fmt)

dpa_adapt/trainer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ def _build_fitting_net(self) -> dict:
327327
# property head at [128, 240], so there is no [159, 240] checkpoint
328328
# head to size-match against. An explicit user value still wins.
329329
if self.fparam_dim > 0:
330-
fn["fparam_dim"] = self.fparam_dim
330+
fn["numb_fparam"] = self.fparam_dim
331331
if self.fitting_net_params:
332332
fn.update(self.fitting_net_params)
333333
return fn

0 commit comments

Comments
 (0)