feat(dpa-adapt): add grouped embedding for system-level property prediction#5741
feat(dpa-adapt): add grouped embedding for system-level property prediction#5741zirenjin wants to merge 282 commits into
Conversation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…flow-cleanup Update dpa_tools
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…flow-cleanup Fix CI dependency installation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fix dpa tools CI test stability
Docs/dpa tools readme
dpa_tools quickstart demo bundle, convert() glob multi-match, docs cleanup
update readme and demo notebook
…R to deepmd/npy - New module data/formula.py: parse_formula, infer_base_element, random_doping, formula_to_npy - auto_convert gains fmt='formula' branch with 5 new keyword-only parameters (poscar, formula_col, property_col, base_element, sets) - CLI: --poscar, --base-element, --formula-col, --property-col, --sets - Exported via data/__init__.py and top-level dpa_tools/__init__.py - Tests: 8 passing (parse, dopant, CSV conversion, header-skip) - README updated with formula_to_npy examples
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #5741 +/- ##
==========================================
- Coverage 81.29% 78.63% -2.67%
==========================================
Files 990 1058 +68
Lines 111018 122339 +11321
Branches 4234 4411 +177
==========================================
+ Hits 90250 96197 +5947
- Misses 19240 24565 +5325
- Partials 1528 1577 +49 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
CodeRabbit (correctness / data integrity): - group_property_model: forward charge_spin into the descriptor; derive the default pool_mask from atype>=0 so padded/virtual atoms are excluded from frame embeddings; add deserialize() to match serialize(). - dataloader: partition grouped batches across ranks with an epoch-independent seed and reshuffle only each rank's own batch order, so ranks that wrap epochs at different times can no longer duplicate/drop groups; grouped.py rejects setups with fewer groups than ranks. - grouped/_core: clear stale system dirs on overwrite; write fparam.npy for every group (schema order, defaults filled) when the manifest advertises a uniform fparam field. - grouped/_convert: regenerate/remove stale pool_mask.npy on overwrite; drop unused loop var (Ruff B007). - grouped/_offline: discover systems recursively (match mark_groups); thread the pool mask into offline descriptor pooling. - finetuner: exclude virtual atoms from offline pooling; grouped evaluate() uses group-level labels. - trainer: infer grouped numb_fparam before the fparam preflight so grouped fparam.npy files are validated. CodeQL / pre-commit: - remove deprecated dpa_adapt.data grouped shims and unused imports; break the finetuner<->_offline import cycle; drop unnecessary del and redundant annotation quotes; guard possibly-uninitialized test vars; isort/ruff/ ruff-format/mdformat.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
dpa_adapt/grouped/_core.py (2)
222-229: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winRestrict overrides to
ComponentSpecdata fields.
hasattr(component, key)also accepts methods likevalidate, so a typo or bad override can replace behavior instead of being rejected.Proposed fix
+from dataclasses import ( + dataclass, + field, + fields, +) ... def add_component( self, component: ComponentSpec, **overrides: Any ) -> ComponentSpec: + valid_fields = {item.name for item in fields(ComponentSpec)} for key, value in overrides.items(): - if not hasattr(component, key): + if key not in valid_fields: raise TypeError(f"Unknown ComponentSpec field: {key}") setattr(component, key, value)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@dpa_adapt/grouped/_core.py` around lines 222 - 229, The add_component method is allowing overrides for any attribute that exists on ComponentSpec, including methods like validate, which can overwrite behavior instead of rejecting bad keys. Update add_component to validate overrides only against ComponentSpec’s data fields (for example, its declared dataclass fields) and keep the TypeError for any unknown or non-field key so only real component data can be overridden.
350-358: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winHonor
fparam_schemaeven when all values come from defaults.
_has_fparam()ignoresself.fparam_schema, so callingset_fparam_schema()with defaults but no per-groupfparamleavestensor_fields.fparamasNoneand skipsfparam.npyentirely.Proposed fix
def _has_fparam(self) -> bool: - return any(group.fparam for group in self.groups) + return bool(self.fparam_schema) or any(group.fparam for group in self.groups)Also applies to: 383-405
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@dpa_adapt/grouped/_core.py` around lines 350 - 358, The grouped adaptation path is dropping fparam data whenever only defaults are provided because `_has_fparam()` does not account for `self.fparam_schema`. Update the logic in the grouped core flow (the `tensor_fields` setup and the related `fparam_schema` handling in the same class, including the later save/load path) so `set_fparam_schema()` alone is enough to enable `fparam` processing and `fparam.npy` generation. Use the existing `fparam_schema` and `_has_fparam()` symbols in `dpa_adapt/grouped/_core.py` to ensure `tensor_fields["fparam"]` is populated whenever the schema is present, even if no per-group `fparam` values exist.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@deepmd/pt/model/model/group_property_model.py`:
- Around line 242-251: The neighbor-list construction in group_property_model’s
input handling is hard-coding mixed neighbor lists, which breaks descriptors
that need type-distinguished neighbors. Update the
extend_input_and_build_neighbor_list call inside the relevant model method to
use the descriptor’s mixed_types() result from the descriptor/model contract, or
add an explicit initialization-time check in the group_property_model path to
reject unsupported non-mixed descriptors. Keep the fix centered on the
mixed_types() behavior so the selected descriptor and neighbor-list mode stay
consistent.
In `@dpa_adapt/grouped/_core.py`:
- Around line 326-343: Sanitized group system paths can collide when different
group.key values map to the same _safe_name(), causing _write_group_system and
the manifest entries built in the self.groups loop to target the same directory.
Update the grouped/_core.py logic around _safe_name, systems_root,
_write_group_system, and _group_manifest so each group gets a unique system path
even after sanitization, for example by incorporating group_idx or another
stable disambiguator into the directory name before writing and recording the
manifest.
---
Outside diff comments:
In `@dpa_adapt/grouped/_core.py`:
- Around line 222-229: The add_component method is allowing overrides for any
attribute that exists on ComponentSpec, including methods like validate, which
can overwrite behavior instead of rejecting bad keys. Update add_component to
validate overrides only against ComponentSpec’s data fields (for example, its
declared dataclass fields) and keep the TypeError for any unknown or non-field
key so only real component data can be overridden.
- Around line 350-358: The grouped adaptation path is dropping fparam data
whenever only defaults are provided because `_has_fparam()` does not account for
`self.fparam_schema`. Update the logic in the grouped core flow (the
`tensor_fields` setup and the related `fparam_schema` handling in the same
class, including the later save/load path) so `set_fparam_schema()` alone is
enough to enable `fparam` processing and `fparam.npy` generation. Use the
existing `fparam_schema` and `_has_fparam()` symbols in
`dpa_adapt/grouped/_core.py` to ensure `tensor_fields["fparam"]` is populated
whenever the schema is present, even if no per-group `fparam` values exist.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 56fdff0e-02f8-4de0-aadb-a2b1ec2d5869
📒 Files selected for processing (25)
deepmd/pt/loss/__init__.pydeepmd/pt/loss/group_property.pydeepmd/pt/model/model/group_property_model.pydeepmd/pt/model/task/group_property.pydeepmd/pt/train/training.pydeepmd/pt/utils/dataloader.pydeepmd/pt/utils/grouped.pydoc/dpa_adapt/assembly_scenarios.mddpa_adapt/finetuner.pydpa_adapt/grouped/_aggregation.pydpa_adapt/grouped/_convert.pydpa_adapt/grouped/_core.pydpa_adapt/grouped/_offline.pydpa_adapt/grouped/_polymer.pydpa_adapt/trainer.pysource/tests/dpa_adapt/test_assemblies.pysource/tests/dpa_adapt/test_grouped_convert.pysource/tests/dpa_adapt/test_grouped_dataset.pysource/tests/dpa_adapt/test_grouped_end_to_end.pysource/tests/dpa_adapt/test_grouped_finetuner.pysource/tests/dpa_adapt/test_grouped_hardening.pysource/tests/dpa_adapt/test_polymer_builder.pysource/tests/dpa_adapt/test_pooling.pysource/tests/pt/test_group_property.pysource/tests/pt/test_group_property_hardening.py
✅ Files skipped from review due to trivial changes (1)
- doc/dpa_adapt/assembly_scenarios.md
🚧 Files skipped from review as they are similar to previous changes (22)
- deepmd/pt/loss/init.py
- source/tests/dpa_adapt/test_grouped_finetuner.py
- source/tests/pt/test_group_property.py
- deepmd/pt/train/training.py
- source/tests/dpa_adapt/test_pooling.py
- source/tests/dpa_adapt/test_grouped_hardening.py
- source/tests/dpa_adapt/test_grouped_dataset.py
- source/tests/dpa_adapt/test_assemblies.py
- source/tests/dpa_adapt/test_polymer_builder.py
- dpa_adapt/grouped/_offline.py
- deepmd/pt/utils/dataloader.py
- source/tests/dpa_adapt/test_grouped_end_to_end.py
- deepmd/pt/loss/group_property.py
- dpa_adapt/trainer.py
- deepmd/pt/utils/grouped.py
- source/tests/dpa_adapt/test_grouped_convert.py
- dpa_adapt/grouped/_polymer.py
- deepmd/pt/model/task/group_property.py
- source/tests/pt/test_group_property_hardening.py
- dpa_adapt/finetuner.py
- dpa_adapt/grouped/_aggregation.py
- dpa_adapt/grouped/_convert.py
- isort: reorder imports in group_property_model.py and finetuner.py - disallow-caps: spell the product name "DeePMD" (not "DeepMD") in the grouped module/doc/test comments and docstrings
- test_grouped_end_to_end: use pytest.importorskip so the backend names are always bound (resolves py/uninitialized-local-variable; try/except + skip isn't modelled as terminating by CodeQL). - finetuner._fit_sklearn: return without a value in the grouped branch so the None-returning method no longer mixes implicit/explicit returns (py/mixed-returns).
The grouped property head pools an un-normalized frame/group embedding concatenated with fparam (no descriptor-style input statistics). At that input scale tanh saturates, so the head learns only its bias and collapses predictions to the global label mean. Direct-overfit evidence: on the same few samples, tanh plateaus at ~30 RMSE (constant output) while GELU reaches <0.5 and fits distinct labels. - deepmd/pt/model/task/group_property.py: default activation_function -> "gelu" - deepmd/utils/argcheck.py: fitting_group_property defaults activation to gelu (property head keeps tanh) - dpa_adapt/trainer.py: grouped config emits activation_function="gelu" (a user fitting_net_params override still wins) - test: assert the generated grouped config uses gelu
njzjz-bot
left a comment
There was a problem hiding this comment.
Thanks for the large addition. I focused on the grouped-training integration and found several correctness issues that should be fixed before merge:
-
Grouped frozen-sklearn extraction ignores
real_atom_types.npy.
The grouped writer intentionally writestype.rawas a uniform placeholder and stores the real per-frame atom types, including virtual-1padding, inreal_atom_types.npy. The frozen-sklearn descriptor extraction path still tilessystem.data["atom_types"]for every frame, so heterogeneous grouped systems are described as if every atom had the first type.pool_maskonly affects final pooling and cannot fix descriptor typing or neighbor-list construction. -
Masked offline pooling is not NaN-safe.
The offline maskedmean/sum/stdpaths multiply descriptors by the mask. If an excluded virtual atom has a non-finite descriptor,0 * NaNremainsNaN, and the laternan_to_numcan zero/corrupt the whole pooled feature instead of dropping only the masked atoms. Please use the sametorch.where(mask > 0, descriptor, 0)-style masking pattern as the grouped PT model path before reduction. -
Descriptor cache keys miss grouped inputs.
The descriptor cache currently fingerprints coordinates/types/cells, but grouped frozen-sklearn features also depend onpool_mask.npyand, after fixing item 1,real_atom_types.npy. Changing group markers or mixed-type real atom types can silently reuse stale cached descriptors. -
Missing/HDF5
group_idhandling is inconsistent between sampler and loss.
The grouped dataloader falls back to an all-zero group id when filesystemgroup_id.npycannot be loaded, treating the whole system as one group. The loss, however, falls back to one group per frame whengroup_idis absent. This can ignore the configured batch size, break DDP when the implicit single group is fewer than the number of ranks, and incorrectly treats HDF5 systems with valid in-datagroup_idas missing because the loader only scansset.*files. -
Grouped-mode detection only checks the first set of the first system.
dpa_adaptdecides whether to switch togroup_propertyfrom a single set directory. If later systems/sets contain markers but the first one does not, grouped training is not enabled; if the first is grouped but later sets are missing markers, training fails later or becomes inconsistent. Please scan all train/valid set dirs and reject mixed marker presence with a clear error. -
group_reduceis implemented but not exposed in config validation.
The fitting head supportsgroup_reduce="mean"|"sum", but the argcheck schema reuses the property schema without adding this option, so users cannot setsumthrough normal input JSON even though direct Python tests cover it. -
Grouped frozen-sklearn does not support the CLI's comma-separated multi-target API.
Non-grouped_load_labels()acceptslist[str], and the CLI turns--target-key a,binto a list. The grouped route passes that list intoGroupedDataset, which resolves a single string key and will fail on a list target. -
Several property-fitting options are accepted but ignored.
The grouped fitting arg schema inherits property options, butGroupPropertyFittingNetswallows many of them via**kwargs(seed,numb_aparam,default_fparam,intensive,distinguish_types,resnet_dt, etc.).trainablelist semantics are also collapsed toall(trainable). This makes normal configs look accepted while silently changing behavior.
I would recommend adding integration tests for: trained serialize/deserialize prediction equality, JSON config construction with group_reduce, grouped frozen-sklearn on mixed real_atom_types.npy, cache invalidation when masks/types change, HDF5 or in-data group_id, DDP/missing-group fallback, and grouped multi-target CLI input.
Reviewed by OpenClaw (model: custom-chat-jinzhezeng-group/gpt-5.5)
FinetuneRuleBuilder already auto-selects a multitask pretrained checkpoints only branch (and re-initializes the fitting net) when a single-task target like GroupPropertyModel finetunes with no --model-branch/finetune_head set, but this exact path -- direct DPA foundation-model checkpoint to GroupPropertyModel -- had no test coverage. Add two tests: the auto-pick + random-fitting case, and documenting the deterministic first-branch pick when several branches are available with no explicit selection.
…e group system names - GroupPropertyModel.forward hard-coded mixed_types=True when building the neighbor list instead of calling self.mixed_types(), so descriptors that require type-distinguished neighbor lists silently got a mixed one. - Assembly.write() sanitized group keys into system directory names without checking for collisions; two keys that sanitize to the same name (e.g. differing only in stripped punctuation) silently overwrote one another and the manifest recorded a duplicate path. Addresses the two still-open threads from the PR deepmodeling#5741 review.
…, group_id fallback Four correctness issues flagged in review, all specific to heterogeneous grouped systems (padded/virtual atoms, uniform type.raw placeholder): - frozen-sklearn extract_features() tiled the single, uniform atom_types array for every frame instead of reading the real, per-frame local types (with -1 padding) grouped systems store in real_atom_types.npy, so every atom in every frame was described as if it had the placeholder type. Added _real_atom_types_for_system() + remap_atom_types_preserving_padding() (which, unlike remap_atom_types, does not wrap -1 to the last checkpoint type via numpy negative-index fancy indexing). - _pool_descriptor() multiplied the raw descriptor by the pool mask before reducing (mean/sum/std); a non-finite value on a masked/virtual atom kept 0 * NaN == NaN and poisoned the whole frames pooled feature. Sanitize masked rows via torch.where(mask > 0, descrpt, 0) before any reduction, matching the pattern already used in GroupPropertyModel.forward. - The descriptor cache fingerprinted coords/types/cells but not pool_mask.npy or real_atom_types.npy, so changing group markers or real atom types on otherwise-identical padded geometry silently reused a stale cached descriptor. - GroupCompleteBatchSampler/GroupDistributedBatchSampler fell back to one giant group (all-zero group_id) when group_id.npy is missing, while GroupPropertyLoss falls back to one group per frame (torch.arange). The mismatch let the sampler ignore batch_size and could break DDP by handing an implicit single group to more ranks than it has frames for. Also rewrote load_group_ids_for_system() to read set.* through the systems own DPPath dirs (already backend-resolved by DeepmdData) instead of a raw pathlib glob, so HDF5-backed systems are checked the same way as on-disk ones instead of an in-data group_id being silently reported as missing.
…st the first _systems_are_grouped() only looked at the first set.* directory of the first train system to decide whether to switch the fitting/loss to group_property. If a later system had markers while the first did not, grouped mode stayed off and that systems group labels were silently dropped; if the first had markers but a later one did not, grouped mode turned on and then failed (or trained inconsistently) once that system was reached. valid_systems was not checked at all, so a grouped/ungrouped mismatch between train and valid went unnoticed too. Scan every set.* directory across both train_systems and valid_systems, and raise a clear DPADataError on any mix of grouped/ungrouped sets or a partially-marked set (some but not all of group_id/weight/pool_mask) instead of guessing.
GroupPropertyFittingNet supports group_reduce="mean"|"sum" (the frame-embedding -> group-embedding reduction), but fitting_group_property() reused the property schema verbatim aside from the activation-function default, with no group_reduce field. dargs strict-mode input.json validation (the same check dp --pt train runs) rejected it as an unknown key, so group_reduce="sum" was only reachable by constructing the model in Python directly, never through normal config files.
…rouped fit Non-grouped _load_labels() already accepts target_key as a list (the CLI turns --target-key a,b into ["a", "b"]), stacking each keys column into a (n_frames, n_keys) label array. GroupedDataset only accepted a single str and resolved/loaded one property.npy-style file, so passing the same list through the grouped route (_fit_sklearn_grouped already forwards whatever target_key it was given) failed. GroupedDataset.target_keys is now always a list; _read_system_group_rows reads one label file per key from each set.* dir and stacks them per frame, same convention as _load_labels (single key keeps its original shape, multiple keys are column-stacked) -- aggregate_weighted_groups already supported (n_items, task_dim) labels, so this was the only missing piece.
… options GroupPropertyFittingNet took **kwargs and accepted the full property-schema field list (numb_aparam, default_fparam, dim_case_embd, resnet_dt, intensive, distinguish_types, seed, ...) since it is a standalone MLP that never went through GeneralFitting, the base class that actually implements those. A config setting any of them passed validation and construction, then silently had no effect. trainable as a per-layer list was also collapsed to all(trainable), losing the per-layer freeze the property schema documents. - Replace **kwargs with two explicit, named, no-op parameters (type, mixed_types) for the two fields the generic model-building path always injects; any other unrecognized field now fails construction immediately with a normal TypeError instead of vanishing into kwargs. - fitting_group_property() drops numb_aparam/default_fparam/dim_case_embd/ resnet_dt/intensive/distinguish_types from the schema entirely, so setting one is a dargs strict-mode validation error, not a silently accepted no-op. - seed is now used: layer init is deterministic when given (scoped via fork_rng so it does not perturb the caller's global RNG state), and unseeded construction still draws from the global stream as before. - trainable=[...] now freezes each Linear layer individually instead of collapsing to all(trainable), matching the documented per-layer semantics and raising if the list length does not match len(neuron)+1.
…ring GroupPropertyFittingNet had no case-embedding support at all (dim_case_embd was explicitly stripped from its argcheck schema), so a group_property head could never share a descriptor with another branch in multi-task training: deepmd-kit's trainer requires every model_dict branch to declare the same dim_case_embd (deepmd.pt.train.training.get_case_embd_config), and a real finetune of an existing multi-branch checkpoint needs it to match that checkpoint's own branch count. - GroupPropertyFittingNet gains dim_case_embd/case_embd/set_case_embd, mirroring GeneralFitting's pattern: a one-hot buffer concatenated onto the aggregated group embedding (after the existing fparam/fparam_neuron fusion), sized into the first Linear layer's input width. - GroupPropertyModel gains a set_case_embd proxy to its fitting net: it does not go through make_model() like PropertyModel/EnergyModel do, so it does not inherit the generic model -> atomic_model -> fitting_net proxy chain the multi-task trainer calls on every branch. - fitting_group_property() un-strips dim_case_embd from the argcheck schema (it was previously grouped with fields group_property genuinely has no wiring for; it now does).
Wires MFTFineTuner/MFTConfigManager to build a group_property downstream head, jointly trained with an aux ener branch on a shared descriptor (preventing representation collapse per arXiv:2601.08486), so grouped/ assembly targets (e.g. an OER O*/OH*/OOH* overpotential, or the polymer cloud-point task) can use MFT instead of plain single-task finetuning. - MFTFineTuner.__init__ accepts downstream_task_type='group_property' alongside 'property'/'ener', plus a group_reduce param; property_name/ task_dim validation now applies to both property-like modes. - MFTConfigManager gets its own _build_group_property_fitting_net/_loss, independent of the property builders: GroupPropertyFittingNet is a small standalone MLP, not built on GeneralFitting, so several property-schema fields (resnet_dt, intensive, distinguish_types, numb_aparam) don't exist on it and dargs strict mode rejects them outright rather than ignoring them -- reusing/trimming the property builder would have silently carried one over. - dim_case_embd on the downstream head is read from the aux branch's own (checkpoint-derived) fitting_net_params via a shared _aux_dim_case_embd helper, not hardcoded: it must equal the branch count of whatever multi-task checkpoint the aux branch was itself pretrained as part of (31 for DPA-3.1-3M, but e.g. 23 for the OMol25/Organic_Reactions/ODAC23 checkpoint used for the cloud-point OOD run) -- hardcoding 31 silently mismatches every checkpoint that isn't DPA-3.1-3M. - evaluate()/predict() gain _check_no_multi_frame_groups: dp --pt test never threads group_id/weight/pool_mask through its evaluation path, so a frozen group_property head silently scores every test frame as its own one-frame group. Harmless for single-frame groups, silently wrong for genuine multi-frame assemblies -- refuse instead of returning numbers that look plausible but average over the wrong rows.
58bf292 to
6026dbb
Compare
|
Possible reviewers based on changed lines, exact file history, and exact-file review history:
No review request was made automatically. Coding agent: Codex |
Summary
This PR adds grouped embedding support to DPA-ADAPT, enabling frame-level atomistic representations to be pooled into group-level embeddings for system-level property prediction.
The main use case is downstream adaptation where one training label corresponds to a collection of structures rather than a single frame, such as polymers, mixtures, reaction assemblies, or catalyst configurations.
Main Changes
group_id,weight, andpool_maskdata fields for grouping frames and controlling atom-level pooling.Notes
This feature is intended for DPA-ADAPT tasks where the prediction target is defined at the system/sample level, while the input information is provided by multiple atomistic structures.
Summary by CodeRabbit
mark-groupsCLI command and grouped polymer dataset generation.fparamconsistency; distributed sampling now keeps groups intact.