Skip to content

Commit 799252d

Browse files
refactor(model): unify dpmodel backend factories (deepmodeling#5786)
## Summary - introduce BackendModelFactory to bind backend descriptor, fitting, model, atomic-model, pair-table, and ZBL registries once - centralize descriptor/fitting parameter injection, standard-model selection, ZBL assembly, legacy model routing, plugin fallback, and spin preprocessing - make JAX and TF2 export the bound shared factory methods directly; keep only genuine backend special cases in dpmodel and pt_expt - reuse the shared component path for pt_expt DPA4/SeZM and linear submodels - register/export all dpmodel model types through the package, removing unused side-effect imports - preserve configured ZBL smin_alpha values and route pt_expt use_srtab configurations to its backend-native DPZBLModel - use descriptor-derived cutoff and selection values for JAX/TF2 ZBL pair tables, carrying the normalized deepmodeling#4339 behavior beyond dpmodel ## Validation - ruff check . - ruff format . - all commit hooks, including isort, Ruff, Velin, and pylint - shared routing, construction, input-immutability, and unsupported-variant tests - dpmodel standard/spin construction tests - JAX standard and ZBL factory tests - pt_expt standard, spin, ZBL, DPA4/SeZM, and linear-model tests - isolated TF2 standard-model construction smoke test Coding agent: Codex Codex version: codex-cli 0.144.4 Model: gpt-5.6-sol Reasoning effort: xhigh <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added unified, backend-agnostic model construction with routing for standard, spin, ZBL, linear-energy, and registered variants. * Expanded the public model API to include linear-energy, DPA4 energy, native-spin, ZBL, and additional property models (DOS, Dipole, Polar). * **Bug Fixes** * Improved configuration handling so caller-provided settings are preserved. * Ensured tabulated-potential (`use_srtab`) configurations correctly produce ZBL models. * **Tests** * Added coverage for model routing, unsupported variants, shared construction behavior, and configuration preservation. * Added a dedicated pt-expt ZBL selection test using `use_srtab`. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: njzjz-bot <njzjz.bot@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent f23c2bc commit 799252d

10 files changed

Lines changed: 845 additions & 520 deletions

File tree

deepmd/dpmodel/model/__init__.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,36 @@
1212
Models generated by `make_model` have already done it.
1313
"""
1414

15+
from .dipole_model import (
16+
DipoleModel,
17+
)
18+
from .dos_model import (
19+
DOSModel,
20+
)
21+
from .dp_linear_model import (
22+
LinearEnergyModel,
23+
)
1524
from .dp_model import (
1625
DPModelCommon,
1726
)
27+
from .dp_zbl_model import (
28+
DPZBLModel,
29+
)
30+
from .dpa4_model import (
31+
DPA4EnergyModel,
32+
)
1833
from .ener_model import (
1934
EnergyModel,
2035
)
2136
from .make_model import (
2237
make_model,
2338
)
39+
from .native_spin_model import (
40+
NativeSpinEnergyModel,
41+
)
42+
from .polar_model import (
43+
PolarModel,
44+
)
2445
from .property_model import (
2546
PropertyModel,
2647
)
@@ -29,8 +50,15 @@
2950
)
3051

3152
__all__ = [
53+
"DOSModel",
54+
"DPA4EnergyModel",
3255
"DPModelCommon",
56+
"DPZBLModel",
57+
"DipoleModel",
3358
"EnergyModel",
59+
"LinearEnergyModel",
60+
"NativeSpinEnergyModel",
61+
"PolarModel",
3462
"PropertyModel",
3563
"SpinModel",
3664
"make_model",

deepmd/dpmodel/model/model.py

Lines changed: 53 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
# SPDX-License-Identifier: LGPL-3.0-or-later
22
import copy
3-
from typing import (
4-
Any,
5-
)
63

74
from deepmd.dpmodel.atomic_model.dp_atomic_model import (
85
DPAtomicModel,
@@ -16,36 +13,21 @@
1613
from deepmd.dpmodel.fitting.base_fitting import (
1714
BaseFitting,
1815
)
19-
from deepmd.dpmodel.fitting.ener_fitting import (
20-
EnergyFittingNet,
21-
)
2216
from deepmd.dpmodel.model.base_model import (
2317
BaseModel,
2418
)
25-
from deepmd.dpmodel.model.dipole_model import (
26-
DipoleModel,
27-
)
28-
from deepmd.dpmodel.model.dos_model import (
29-
DOSModel,
30-
)
3119
from deepmd.dpmodel.model.dp_zbl_model import (
3220
DPZBLModel,
3321
)
34-
from deepmd.dpmodel.model.dpa4_model import (
35-
DPA4EnergyModel,
22+
from deepmd.dpmodel.model.model_factory import (
23+
BackendModelFactory,
3624
)
37-
from deepmd.dpmodel.model.ener_model import (
38-
EnergyModel,
25+
from deepmd.dpmodel.model.model_factory import (
26+
get_spin_model as get_spin_model_from_factory,
3927
)
4028
from deepmd.dpmodel.model.native_spin_model import (
4129
NativeSpinEnergyModel,
4230
)
43-
from deepmd.dpmodel.model.polar_model import (
44-
PolarModel,
45-
)
46-
from deepmd.dpmodel.model.property_model import (
47-
PropertyModel,
48-
)
4931
from deepmd.dpmodel.model.spin_model import (
5032
SpinModel,
5133
)
@@ -54,48 +36,29 @@
5436
normalize_spin_use_spin,
5537
)
5638

57-
_DPA4_SEZM_DESCRIPTOR_TYPES = ("dpa4", "DPA4", "sezm", "SeZM")
58-
39+
_model_factory = BackendModelFactory(
40+
descriptor_base=BaseDescriptor,
41+
fitting_base=BaseFitting,
42+
model_base=BaseModel,
43+
backend_name="DP",
44+
atomic_model=DPAtomicModel,
45+
pairtab_model=PairTabAtomicModel,
46+
zbl_model=DPZBLModel,
47+
)
48+
get_zbl_model = _model_factory.get_zbl_model
5949

60-
def _get_standard_model_components(
61-
data: dict[str, Any], ntypes: int
62-
) -> tuple[BaseDescriptor, BaseFitting, str]:
63-
# descriptor
64-
data["descriptor"]["ntypes"] = ntypes
65-
data["descriptor"]["type_map"] = copy.deepcopy(data["type_map"])
66-
descriptor = BaseDescriptor(**data["descriptor"])
67-
# fitting
68-
fitting_net = data.get("fitting_net", {})
69-
fitting_net["type"] = fitting_net.get("type", "ener")
70-
fitting_net["ntypes"] = descriptor.get_ntypes()
71-
fitting_net["type_map"] = copy.deepcopy(data["type_map"])
72-
fitting_net["mixed_types"] = descriptor.mixed_types()
73-
if fitting_net["type"] in ["dipole", "polar"]:
74-
fitting_net["embedding_width"] = descriptor.get_dim_emb()
75-
fitting_net["dim_descrpt"] = descriptor.get_dim_out()
76-
grad_force = "direct" not in fitting_net["type"]
77-
if not grad_force:
78-
fitting_net["out_dim"] = descriptor.get_dim_emb()
79-
if "ener" in fitting_net["type"]:
80-
fitting_net["return_energy"] = True
81-
fitting = BaseFitting(**fitting_net)
82-
return descriptor, fitting, fitting_net["type"]
50+
_DPA4_SEZM_DESCRIPTOR_TYPES = ("dpa4", "DPA4", "sezm", "SeZM")
8351

8452

85-
def get_standard_model(data: dict) -> EnergyModel:
86-
"""Get a EnergyModel from a dictionary.
53+
def get_standard_model(data: dict) -> BaseModel:
54+
"""Get a standard model from a dictionary.
8755
8856
Parameters
8957
----------
9058
data : dict
9159
The data to construct the model.
9260
"""
93-
if "type_embedding" in data:
94-
raise ValueError(
95-
"In the DP backend, type_embedding is not at the model level, but within the descriptor. See type embedding documentation for details."
96-
)
9761
data = copy.deepcopy(data)
98-
ntypes = len(data["type_map"])
9962
# Analytical bridging (e.g. ZBL): the radii feed the DESCRIPTOR's
10063
# InnerClamp/BridgingSwitch (mirrors pt's builder); the method builds the
10164
# atomic model's InterPotential below.
@@ -104,105 +67,42 @@ def get_standard_model(data: dict) -> EnergyModel:
10467
if bridging_enabled:
10568
data["descriptor"]["inner_clamp_r_inner"] = data.get("bridging_r_inner", 0.5)
10669
data["descriptor"]["inner_clamp_r_outer"] = data.get("bridging_r_outer", 0.8)
107-
descriptor, fitting, fitting_net_type = _get_standard_model_components(data, ntypes)
70+
model = _model_factory.get_standard_model(data)
71+
if not bridging_enabled:
72+
return model
73+
74+
descriptor = model.atomic_model.descriptor
10875
atom_exclude_types = data.get("atom_exclude_types", [])
10976
pair_exclude_types = data.get("pair_exclude_types", [])
110-
111-
if fitting_net_type == "dipole":
112-
modelcls = DipoleModel
113-
elif fitting_net_type == "polar":
114-
modelcls = PolarModel
115-
elif fitting_net_type == "dos":
116-
modelcls = DOSModel
117-
elif fitting_net_type in ["ener", "direct_force_ener"]:
118-
modelcls = EnergyModel
119-
elif fitting_net_type in ["dpa4_ener", "sezm_ener"]:
120-
modelcls = DPA4EnergyModel
121-
elif fitting_net_type == "property":
122-
modelcls = PropertyModel
123-
else:
124-
raise RuntimeError(f"Unknown fitting type: {fitting_net_type}")
125-
126-
model = modelcls(
127-
descriptor=descriptor,
128-
fitting=fitting,
129-
type_map=data["type_map"],
130-
atom_exclude_types=atom_exclude_types,
131-
pair_exclude_types=pair_exclude_types,
77+
# Composition, not a flag (first-principles design): the analytical
78+
# bridging term is its own atomic model, summed with the learned one by the
79+
# existing linear composition machinery.
80+
from deepmd.dpmodel.atomic_model.inter_potential import (
81+
InterPotentialAtomicModel,
82+
)
83+
from deepmd.dpmodel.atomic_model.linear_atomic_model import (
84+
LinearEnergyAtomicModel,
85+
)
86+
from deepmd.dpmodel.model.dp_linear_model import (
87+
LinearEnergyModel,
13288
)
133-
if bridging_enabled:
134-
# Composition, not a flag (first-principles design): the analytical
135-
# bridging term is its own atomic model, summed with the learned one
136-
# by the existing linear composition machinery.
137-
from deepmd.dpmodel.atomic_model.inter_potential import (
138-
InterPotentialAtomicModel,
139-
)
140-
from deepmd.dpmodel.atomic_model.linear_atomic_model import (
141-
LinearEnergyAtomicModel,
142-
)
143-
from deepmd.dpmodel.model.dp_linear_model import (
144-
LinearEnergyModel,
145-
)
146-
147-
zbl_atomic = InterPotentialAtomicModel(
148-
type_map=data["type_map"],
149-
mode=bridging_method,
150-
rcut=descriptor.get_rcut(),
151-
sel=descriptor.get_sel(),
152-
)
153-
composed = LinearEnergyAtomicModel(
154-
models=[model.atomic_model, zbl_atomic],
155-
type_map=data["type_map"],
156-
weights="sum",
157-
# Both exclusions belong to the composition: its children share one
158-
# graph, so "excluded" must cover the analytical term too.
159-
atom_exclude_types=atom_exclude_types,
160-
pair_exclude_types=pair_exclude_types,
161-
)
162-
return LinearEnergyModel(atomic_model_=composed)
163-
return model
164-
165-
166-
def get_zbl_model(data: dict) -> DPZBLModel:
167-
data = copy.deepcopy(data)
168-
data["descriptor"]["ntypes"] = len(data["type_map"])
169-
data["descriptor"]["type_map"] = data["type_map"]
170-
descriptor = BaseDescriptor(**data["descriptor"])
171-
fitting_type = data["fitting_net"].pop("type")
172-
data["fitting_net"]["type_map"] = data["type_map"]
173-
if fitting_type == "ener":
174-
fitting = EnergyFittingNet(
175-
ntypes=descriptor.get_ntypes(),
176-
dim_descrpt=descriptor.get_dim_out(),
177-
mixed_types=descriptor.mixed_types(),
178-
**data["fitting_net"],
179-
)
180-
else:
181-
raise ValueError(f"Unknown fitting type {fitting_type}")
18289

183-
dp_model = DPAtomicModel(descriptor, fitting, type_map=data["type_map"])
184-
# pairtab
185-
filepath = data["use_srtab"]
186-
pt_model = PairTabAtomicModel(
187-
filepath,
188-
descriptor.get_rcut(),
189-
descriptor.get_sel(),
90+
zbl_atomic = InterPotentialAtomicModel(
19091
type_map=data["type_map"],
92+
mode=bridging_method,
93+
rcut=descriptor.get_rcut(),
94+
sel=descriptor.get_sel(),
19195
)
192-
193-
rmin = data["sw_rmin"]
194-
rmax = data["sw_rmax"]
195-
atom_exclude_types = data.get("atom_exclude_types", [])
196-
pair_exclude_types = data.get("pair_exclude_types", [])
197-
return DPZBLModel(
198-
dp_model,
199-
pt_model,
200-
rmin,
201-
rmax,
96+
composed = LinearEnergyAtomicModel(
97+
models=[model.atomic_model, zbl_atomic],
20298
type_map=data["type_map"],
99+
weights="sum",
100+
# Both exclusions belong to the composition: its children share one
101+
# graph, so "excluded" must cover the analytical term too.
203102
atom_exclude_types=atom_exclude_types,
204103
pair_exclude_types=pair_exclude_types,
205104
)
105+
return LinearEnergyModel(atomic_model_=composed)
206106

207107

208108
def get_spin_model(data: dict) -> SpinModel:
@@ -218,30 +118,11 @@ def get_spin_model(data: dict) -> SpinModel:
218118
"the virtual-atom (deepspin) scheme is not supported for "
219119
"DPA4/SeZM; use spin scheme 'native'"
220120
)
221-
data = copy.deepcopy(data)
222-
# include virtual spin and placeholder types
223-
data["type_map"] += [item + "_spin" for item in data["type_map"]]
224-
spin = Spin(
225-
use_spin=data["spin"]["use_spin"],
226-
virtual_scale=data["spin"]["virtual_scale"],
227-
)
228-
pair_exclude_types = spin.get_pair_exclude_types(
229-
exclude_types=data.get("pair_exclude_types", None)
121+
return get_spin_model_from_factory(
122+
data,
123+
standard_model_factory=get_standard_model,
124+
spin_model=SpinModel,
230125
)
231-
data["pair_exclude_types"] = pair_exclude_types
232-
# for descriptor data stat
233-
data["descriptor"]["exclude_types"] = pair_exclude_types
234-
atom_exclude_types = spin.get_atom_exclude_types(
235-
exclude_types=data.get("atom_exclude_types", None)
236-
)
237-
data["atom_exclude_types"] = atom_exclude_types
238-
if "env_protection" not in data["descriptor"]:
239-
data["descriptor"]["env_protection"] = 1e-6
240-
if data["descriptor"]["type"] in ["se_e2_a"]:
241-
# only expand sel for se_e2_a
242-
data["descriptor"]["sel"] += data["descriptor"]["sel"]
243-
backbone_model = get_standard_model(data)
244-
return SpinModel(backbone_model=backbone_model, spin=spin)
245126

246127

247128
def get_native_spin_model(data: dict) -> NativeSpinEnergyModel:
@@ -315,15 +196,9 @@ def get_model(data: dict) -> BaseModel:
315196
data : dict
316197
The data to construct the model.
317198
"""
318-
model_type = data.get("type", "standard")
319-
if model_type == "standard":
320-
if "spin" in data:
321-
if data["spin"].get("scheme", "deepspin") == "native":
322-
return get_native_spin_model(data)
323-
return get_spin_model(data)
324-
elif "use_srtab" in data:
325-
return get_zbl_model(data)
326-
else:
327-
return get_standard_model(data)
328-
else:
329-
return BaseModel.get_class_by_type(model_type).get_model(data)
199+
return _model_factory.get_model(
200+
data,
201+
standard_model_factory=get_standard_model,
202+
spin_model_factory=get_spin_model,
203+
native_spin_model_factory=get_native_spin_model,
204+
)

0 commit comments

Comments
 (0)