DPA-ADAPT (dpa-adapt, Python import dpa_adapt) is a toolkit for adapting pretrained DPA models to downstream atomistic property prediction tasks. The main CLI is dpa-adapt; the optional short alias is dpaad. No DeePMD-kit JSON configs or dp train pipelines to write.
pip install deepmd-kit[dpa-adapt]Installs scikit-learn, dpdata, ase, rdkit, and e3nn alongside DeePMD-kit. For GPU PyTorch, install your preferred PyTorch build first.
For a complete runnable example (QM9 HOMO–LUMO gap, ~5 min on CPU), see ../../examples/dpa_adapt/.
The strategy is the core choice. All four share the same pre-trained DPA backbone and differ in how much of it gets updated:
| Strategy | Core Mechanism | Target Data Size | Primary Use Case |
|---|---|---|---|
frozen_sklearn |
Frozen backbone + scikit-learn regressor | Small (<1k) | Ultra-fast benchmarking & prototyping |
frozen_head |
Frozen backbone + DeePMD property fitting head | Medium (1k–10k) | Train only the property head while keeping the pretrained DPA backbone frozen |
finetune |
End-to-end full parameter fine-tuning | Large (>10k) | Maximum accuracy on large datasets |
mft |
Multi-task co-training (property + force field) | Small / low-data | Mitigating representation collapse |
Freezes the DPA backbone as a feature extractor and fits a scikit-learn
regressor on the pooled descriptors. No GPU, no dp train — fastest path
for small datasets.
model = DPAFineTuner(
pretrained="DPA-3.1-3M",
strategy="frozen_sklearn",
predictor="rf", # "rf" | "linear" | "mlp"
pooling="mean", # "mean" | "sum" | "mean+std" | "mean+std+max+min"
model_branch=None, # multi-task branch for descriptor extraction
fparam_dim=0, # > 0 reads set.*/fparam.npy and concatenates to descriptor
seed=42,
)
model.fit(train_data="/data/train/*", target_key="homo")
pred = model.predict(data="/data/test")
metrics = model.evaluate(data="/data/test") # .mae, .rmse, .r2| Parameter | Type | Default | Description |
|---|---|---|---|
pretrained |
str |
"DPA-3.1-3M" |
Checkpoint path or built-in name |
predictor |
str |
"rf" |
"rf" (random forest), "linear" (Ridge), "mlp" (MLPRegressor) |
pooling |
str |
"mean" |
"mean", "sum", "mean+std", "mean+std+max+min" |
model_branch |
str or None |
None |
Multi-task branch for descriptor extraction (e.g. "Domains_Drug") |
fparam_dim |
int |
0 |
Dimension of per-frame context features; > 0 reads set.*/fparam.npy |
seed |
int |
42 |
Random seed for the sklearn head |
Both delegate to dp --pt train and accept the same parameters. The only
difference: frozen_head freezes the DPA backbone (train only the fitting
head), while finetune updates all parameters end-to-end.
frozen_head suits medium datasets (1k–10k); finetune targets large datasets (>10k, GPU required).
model = DPAFineTuner(
pretrained="DPA-3.1-3M",
strategy="frozen_head", # "frozen_head" | "finetune"
# ---- task ----
property_name="homo",
task_dim=1,
intensive=True, # True = intensive (mean-pooled), False = extensive
init_branch="SPICE2", # checkpoint branch for descriptor init
# ---- fitting net ----
fitting_net_params=None, # dict overriding fitting_net fields, e.g.
# { # {"neuron": [128,128,128], "activation_function": "relu"}
# "neuron": [128, 128], # (default: neuron=[240,240,240], tanh, resnet_dt=True)
# "activation_function": "relu",
# },
# ---- learning rate ----
learning_rate=1e-3, # start_lr
stop_lr=1e-5, # end_lr
decay_steps=None, # None → 1000; or explicit int
warmup_steps=0, # linear LR warmup (0 = disabled)
# ---- training ----
max_steps=100_000,
batch_size="auto:512", # deepmd-kit batch_size spec
loss_function="mse", # "mse" | "smooth_mae"
# ---- optional ----
fparam_dim=0, # > 0 reads set.*/fparam.npy → numb_fparam
seed=42,
# ---- output ----
output_dir="./dpa_output",
save_freq=10_000,
disp_freq=1_000,
)
model.fit(train_data="/data/train", valid_data="/data/valid")
pred = model.predict(data="/data/test")
metrics = model.evaluate(data="/data/test") # .mae, .rmse, .r2| Parameter | Type | Default | Description |
|---|---|---|---|
pretrained |
str |
"DPA-3.1-3M" |
Checkpoint path or built-in name |
strategy |
str |
"frozen_sklearn" |
"frozen_head" (freeze backbone) or "finetune" (full update) |
property_name |
str |
"property" |
Label key under set.*/, e.g. "homo" reads set.*/homo.npy |
task_dim |
int |
1 |
Output dimensionality of the property fitting net |
intensive |
bool |
True |
True = mean-pool over atoms (intensive); False = sum (extensive) |
init_branch |
str |
"SPICE2" |
Checkpoint branch used to initialise the descriptor |
fitting_net_params |
dict or None |
None |
Overrides for fitting-net fields (neuron, activation_function, resnet_dt, etc.) |
learning_rate |
float |
1e-3 |
Start learning rate (start_lr in deepmd-kit exp scheduler) |
stop_lr |
float |
1e-5 |
End learning rate |
decay_steps |
int or None |
None |
Steps between LR decays; None → 1000 |
warmup_steps |
int |
0 |
Linear LR warmup steps; 0 = disabled |
max_steps |
int |
100_000 |
Total training steps (numb_steps) |
batch_size |
str or int |
"auto:512" |
deepmd-kit batch_size spec (e.g. "auto:256" or 128) |
loss_function |
str |
"mse" |
"mse" or "smooth_mae" |
fparam_dim |
int |
0 |
Dimension of per-frame context features; > 0 reads set.*/fparam.npy |
seed |
int |
42 |
Random seed (descriptor, fitting net, training) |
output_dir |
str |
"./dpa_output" |
Directory for input.json, checkpoints, and logs |
save_freq |
int |
10_000 |
Checkpoint save interval in steps |
disp_freq |
int |
1_000 |
Log display interval in steps |
Jointly trains a downstream property head with an auxiliary force/energy head
on a shared DPA descriptor, preventing representation collapse on small
datasets. Requires GPU. Inherits all frozen_head/finetune parameters
plus the MFT-specific ones below.
model = DPAFineTuner(
pretrained="/path/to/DPA-3.1-3M.pt",
strategy="mft",
# ---- task (same as frozen_head/finetune) ----
property_name="homo",
task_dim=1,
intensive=True,
init_branch="SPICE2",
# ---- MFT-specific ----
aux_branch="MP_traj_v024_alldata_mixu", # checkpoint branch for aux force head
aux_prob=0.5, # aux sampling weight (downstream = 1 - aux_prob)
downstream_task_type="property", # "property" (default) | "ener" (legacy)
type_map=None, # global (shared) type map; must be union of
# both datasets' elements (auto-detect)
aux_batch_size=None, # batch size for aux head (None = auto)
downstream_batch_size=None, # batch size for downstream head (None = auto)
# ---- fitting net (aux head only; downstream uses property defaults) ----
fitting_net_params=None, # None = auto-read from checkpoint
# ---- learning rate ----
learning_rate=1e-3,
stop_lr=1e-5,
decay_steps=None, # None → 1000 (property) or 5000 (ener)
warmup_steps=0,
# ---- training ----
max_steps=50_000,
batch_size="auto:32",
# ---- optional ----
fparam_dim=0,
seed=42,
# ---- output ----
output_dir="./mft_output",
save_freq=10_000,
disp_freq=1_000,
)
model.fit(train_data="/data/train", aux_data="/data/spice2")
pred = model.predict(data="/data/test")
metrics = model.evaluate(data="/data/test") # .mae, .rmse, .r2Shared parameters — all frozen_head/finetune parameters above also apply to MFT.
MFT-specific parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
aux_branch |
str |
"MP_traj_v024_alldata_mixu" |
Checkpoint branch to initialize the auxiliary force/energy head. Use dp --pt show <ckpt> model-branch to list options. |
aux_prob |
float |
0.5 |
Sampling weight for the aux branch. Downstream weight = 1.0 - aux_prob. |
downstream_task_type |
str |
"property" |
"property" (intensive scalar head, e.g. HOMO/LUMO) or "ener" (force-field head, legacy mode) |
type_map |
list[str] or None |
None |
Global (shared) type map for MFT. Both branches share a single descriptor, so this must be the union of all elements appearing in either dataset. Auto-detected from the pretrained checkpoint if None. |
aux_batch_size |
str or None |
None |
Batch size for aux head; auto-selected if None |
downstream_batch_size |
int or None |
None |
Batch size for downstream head; auto-selected if None |
fitting_net_params |
dict or None |
None |
Overrides for the aux fitting net; downstream uses property defaults. None = auto-read from checkpoint. |
DPA-ADAPT trains on deepmd/npy data. Use dpa-adapt data convert (or the Python
convert helper) to route common inputs into the right conversion pipeline:
- SMILES CSV: a
.csvfile with aSMILES/smilescolumn. RDKit generates 3D conformers, or existing.mol/.sdf/.xyz/.pdbfiles can be supplied withmol_dir. - Structure files / trajectories: POSCAR, OUTCAR,
*.xyz,vasprun.xml, ABACUS, CP2K, Gaussian, LAMMPS, ASE,deepmd/raw,deepmd/npy, LMDB, and other dpdata formats. Omitfmtwhen dpdata can infer it; setfmtexplicitly for ambiguous inputs.
from dpa_adapt import convert
# Structure file / trajectory → dpdata → deepmd/npy
convert("POSCAR", "./npy")
convert("OUTCAR", "./npy", fmt="vasp/outcar")
# Glob patterns: one match is converted as one system; multiple matches are batched.
convert("calcs/**/OUTCAR", "./npy_root", fmt="vasp/outcar")
# CSV with a SMILES column → RDKit 3D conformers → deepmd/npy.
# property_col names the input target column and output label name.
convert(
"molecules.csv",
"./npy",
fmt="smiles", # optional when a SMILES/smiles column is present
smiles_col="SMILES",
property_col="HOMO",
train_ratio=0.9,
)
# CSV + pre-generated molecular structures: skip RDKit conformer generation.
convert(
"molecules.csv",
"./npy",
fmt="smiles",
smiles_col="SMILES",
property_col="GAP",
mol_dir="./mol_files",
mol_template="id{row}.sdf",
)CLI equivalents:
# SMILES table
dpa-adapt data convert --input molecules.csv --output ./npy \
--fmt smiles --smiles-col SMILES --property-col HOMO --train-ratio 0.9
# Structure file or glob of calculation outputs
dpa-adapt data convert --input POSCAR --output ./npy
dpa-adapt data convert --input "calcs/**/OUTCAR" --output ./npy_root --fmt vasp/outcarLower-level helpers:
from dpa_adapt import convert, attach_labels, check_data
convert("OUTCAR", "./npy", fmt="vasp/outcar")
convert("calcs/**/OUTCAR", "./npy_root", fmt="vasp/outcar")
# Single system
attach_labels("./npy/", head="bandgap", values=np.array([1.0, 2.0, 3.0]))
# Multiple systems: values[i] → sorted(glob("npy/*/"))[i]
labels = np.load("labels.npy") # shape (n_systems,)
attach_labels("./npy/", head="bandgap", values=labels)
check_data("/data/system") # → list[Issue]For the full option list and supported dpdata formats, see
input_formats.md.
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.
# works identically for frozen_sklearn, frozen_head, finetune, and mft
model = DPAFineTuner(strategy="frozen_sklearn", fparam_dim=2)
model.fit(train_data="data/train", target_key="property")
# fparam.npy is read automatically — no conditions= dict needed| Strategy | How fparam is used |
|---|---|
frozen_sklearn |
columns are standardized via ConditionManager and concatenated to the descriptor |
frozen_head / finetune / mft |
passed into the fitting net as numb_fparam |
After training, save a portable frozen bundle and load it with DPAPredictor — no training dependencies required:
model.freeze("model.pth")
from dpa_adapt import DPAPredictor
pred = DPAPredictor("model.pth")
result = pred.predict("/data/test") # DotDict: .predictions
metrics = pred.evaluate("/data/test") # DotDict: .mae, .rmse, .r2Uncertainty estimation is available for frozen_sklearn models:
- RF: native out-of-bag variance, always available
- MLP: committee of N independently-seeded clones; set
n_committeeat load time - Ridge: not supported
pred = DPAPredictor("model.pth", n_committee=5)
result = pred.predict("/data/test", return_uncertainty=True)
# result.predictions — shape (n,)
# result.uncertainty — shape (n,), std across committee membersUncertainty estimates can drive active learning (query most uncertain candidates) or feed into Bayesian optimization over composition space.
Formula-grouped splitting prevents same-composition leakage between folds.
group_by accepts "formula" (uses each system's directory name as the group
key — requires directories named by formula, e.g. H2O/, CH4/) or a list
of labels the same length as systems:
from dpa_adapt import cross_validate, train_test_split, load_dataset
systems = load_dataset("/data/root", label_key="energy")
# Case 1: directory names are formulas (e.g. data/H2O/, data/CH4/)
train, valid, test = train_test_split(systems, group_by="formula", seed=42)
# Case 2: directory names are not formulas (e.g. QM9's sys_0000, sys_0001, …)
formulas = ["H2O", "H2O", "CH4", "CH4", ...] # one label per system
train, valid, test = train_test_split(systems, group_by=formulas, seed=42)
# Cross-validate (same group_by options apply)
result = cross_validate(model, systems, label_key="energy", cv=5, group_by=formulas)
# → {"aggregate": {"mae_mean": ..., "rmse_std": ...}, ...}from dpa_adapt import (
DPAFineTuner, # fine-tune (strategies: frozen_sklearn, frozen_head, finetune, mft)
DPAPredictor, # inference from frozen bundles
extract_descriptors, # standalone descriptor extraction
cross_validate, # leak-proof cross-validation
train_test_split, # formula-grouped splitting
convert, # format-sniffing data conversion
smiles_to_npy, # CSV+SMILES → deepmd/npy
check_data, # data sanity checks
attach_labels, # inject label arrays
load_dataset, # label-filtered data loading
)Standalone descriptor extraction:
X = extract_descriptors(
"/data/systems",
pretrained="/path/to/DPA-3.1-3M.pt",
pooling="mean+std",
)
# → np.ndarray (n_frames, feat_dim * 2)| Command | Description |
|---|---|
dpa-adapt fit / dpaad fit |
Fine-tune (--strategy frozen_sklearn|frozen_head|finetune|mft) |
dpa-adapt predict / dpaad predict |
Predict with a frozen .pth bundle |
dpa-adapt evaluate / dpaad evaluate |
Evaluate against stored labels |
dpa-adapt extract-descriptors / dpaad extract-descriptors |
Extract pooled DPA descriptors to .npy |
dpa-adapt cv / dpaad cv |
Cross-validate |
dpa-adapt data convert / dpaad data convert |
Convert structure / CSV → deepmd/npy |
dpa-adapt data validate / dpaad data validate |
Sanity-check deepmd/npy directories |
dpa-adapt data attach-labels / dpaad data attach-labels |
Inject .npy label arrays |
# Data conversion
# Structure file
dpa-adapt data convert --input POSCAR --output ./npy
# SMILES CSV: --property-col names the input target column and output label name.
dpaad data convert --input data.csv --output ./npy --fmt smiles \
--property-col homo
# Fine-tune
dpa-adapt fit --train-data ./npy/train --pretrained DPA-3.1-3M \
--strategy frozen_sklearn --predictor rf --target-key homo --output model.pth
# MFT
dpaad fit --train-data /data/qm9 --aux-data /data/spice2 \
--pretrained /path/to/DPA-3.1-3M.pt --strategy mft --target-key homo
# Predict / evaluate
dpa-adapt predict --model model.pth --data ./npy/test --output pred.npy
dpa-adapt evaluate --model model.pth --data ./npy/testdpa-adapt --help and dpaad --help do not load torch — all heavy imports are lazy.