deepmd-torchsim 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rahul Verma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepmd-torchsim
3
+ Version: 0.1.0
4
+ Summary: DeePMD-kit (PyTorch backend) ModelInterface implementation for torch-sim
5
+ Author-email: rverma7@ncsu.edu
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: torch-sim-atomistic>=0.6.1
11
+ Requires-Dist: torch>=2
12
+ Requires-Dist: numpy
13
+ Requires-Dist: ase>=3.26
14
+ Provides-Extra: deepmd
15
+ Requires-Dist: deepmd-kit==3.1.3; extra == "deepmd"
16
+ Requires-Dist: torch==2.10.0; extra == "deepmd"
17
+ Requires-Dist: mpich; extra == "deepmd"
18
+ Requires-Dist: e3nn; extra == "deepmd"
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest>=8; extra == "test"
21
+ Dynamic: license-file
22
+
23
+ # deepmd-torchsim
24
+
25
+ A [torch-sim](https://github.com/TorchSim/torch-sim) `ModelInterface` implementation
26
+ for [DeePMD-kit](https://github.com/deepmodeling/deepmd-kit)'s PyTorch backend.
27
+
28
+ ## Install
29
+
30
+ The package (`DeepmdModel`, torch-sim `ModelInterface` wrapper) can be installed with either `pip` and `uv`:
31
+
32
+ ```bash
33
+ # from PyPI (once published) or a local checkout
34
+ pip install deepmd-torchsim
35
+ uv pip install deepmd-torchsim
36
+ uv add deepmd-torchsim
37
+
38
+ # editable, from a local checkout
39
+ pip install -e .
40
+ uv pip install -e .
41
+ ```
42
+
43
+ ### Getting a working `deepmd-kit` backend
44
+
45
+ ```bash
46
+ pip install "deepmd-torchsim[deepmd]"
47
+ uv pip install "deepmd-torchsim[deepmd]"
48
+ ```
49
+
50
+ The `deepmd` extra pins an working `deepmd-kit==3.1.3`, with `torch==2.10.0`.
51
+
52
+ ## Usage
53
+
54
+ ```python
55
+ import torch
56
+ from deepmd_torchsim import DeepmdModel
57
+ import torch_sim as ts
58
+ from ase.build import molecule
59
+
60
+ model = DeepmdModel(
61
+ model_path="frozen_model.pth",
62
+ device=torch.device("cuda" if torch.cuda.is_available() else "cpu"),
63
+ compute_forces=True,
64
+ compute_stress=True,
65
+ )
66
+
67
+ state = ts.io.atoms_to_state([molecule("H2O")], model.device, model.dtype)
68
+ results = model(state)
69
+ print(results["energy"]) # [n_systems]
70
+ print(results["forces"]) # [n_atoms, 3]
71
+ print(results["stress"]) # [n_systems, 3, 3]
72
+ ```
73
+
74
+ For multitask/multi-domain foundation checkpoints (e.g. DPA-3), pass `head=`
75
+ to select which trained domain to evaluate with:
76
+
77
+ ```python
78
+ model = DeepmdModel(model_path="DPA-3.1-3M.pt", head="Omat24")
79
+ ```
80
+ ## Tests
81
+
82
+ ```bash
83
+ pytest tests/
84
+ ```
85
+
86
+ ## License
87
+
88
+ This project is licensed under the [MIT License](https://github.com/rahulumrao/deepmd_torchsim/blob/main/LICENSE).
89
+
90
+ ## Author
91
+ Rahul Verma \
92
+ Email: rverma7@ncsu.edu
93
+
@@ -0,0 +1,71 @@
1
+ # deepmd-torchsim
2
+
3
+ A [torch-sim](https://github.com/TorchSim/torch-sim) `ModelInterface` implementation
4
+ for [DeePMD-kit](https://github.com/deepmodeling/deepmd-kit)'s PyTorch backend.
5
+
6
+ ## Install
7
+
8
+ The package (`DeepmdModel`, torch-sim `ModelInterface` wrapper) can be installed with either `pip` and `uv`:
9
+
10
+ ```bash
11
+ # from PyPI (once published) or a local checkout
12
+ pip install deepmd-torchsim
13
+ uv pip install deepmd-torchsim
14
+ uv add deepmd-torchsim
15
+
16
+ # editable, from a local checkout
17
+ pip install -e .
18
+ uv pip install -e .
19
+ ```
20
+
21
+ ### Getting a working `deepmd-kit` backend
22
+
23
+ ```bash
24
+ pip install "deepmd-torchsim[deepmd]"
25
+ uv pip install "deepmd-torchsim[deepmd]"
26
+ ```
27
+
28
+ The `deepmd` extra pins an working `deepmd-kit==3.1.3`, with `torch==2.10.0`.
29
+
30
+ ## Usage
31
+
32
+ ```python
33
+ import torch
34
+ from deepmd_torchsim import DeepmdModel
35
+ import torch_sim as ts
36
+ from ase.build import molecule
37
+
38
+ model = DeepmdModel(
39
+ model_path="frozen_model.pth",
40
+ device=torch.device("cuda" if torch.cuda.is_available() else "cpu"),
41
+ compute_forces=True,
42
+ compute_stress=True,
43
+ )
44
+
45
+ state = ts.io.atoms_to_state([molecule("H2O")], model.device, model.dtype)
46
+ results = model(state)
47
+ print(results["energy"]) # [n_systems]
48
+ print(results["forces"]) # [n_atoms, 3]
49
+ print(results["stress"]) # [n_systems, 3, 3]
50
+ ```
51
+
52
+ For multitask/multi-domain foundation checkpoints (e.g. DPA-3), pass `head=`
53
+ to select which trained domain to evaluate with:
54
+
55
+ ```python
56
+ model = DeepmdModel(model_path="DPA-3.1-3M.pt", head="Omat24")
57
+ ```
58
+ ## Tests
59
+
60
+ ```bash
61
+ pytest tests/
62
+ ```
63
+
64
+ ## License
65
+
66
+ This project is licensed under the [MIT License](https://github.com/rahulumrao/deepmd_torchsim/blob/main/LICENSE).
67
+
68
+ ## Author
69
+ Rahul Verma \
70
+ Email: rverma7@ncsu.edu
71
+
@@ -0,0 +1,12 @@
1
+ """DeePMD-kit (PyTorch backend) integration for torch-sim.
2
+
3
+ https://github.com/TorchSim/torch-sim
4
+
5
+ Exposes :class:`DeepmdModel`, a :class:`torch_sim.models.interface.ModelInterface`
6
+ implementation that wraps a frozen DeePMD-kit PyTorch-backend model
7
+ (``frozen_model.pth``, loaded through ``deepmd.infer.DeepPot``).
8
+ """
9
+
10
+ from deepmd_torchsim.model import DeepmdModel
11
+
12
+ __all__ = ["DeepmdModel"]
@@ -0,0 +1,316 @@
1
+ """DeePMD-kit (PyTorch backend) model wrapper for torch-sim.
2
+
3
+ Provides a :class:`~torch_sim.models.interface.ModelInterface` implementation for running a DeePMD-kit PyTorch-backend model (`frozen_model.pth`) within torch-sim. The wrapper follows the same packaging and integration conventions as torch-sim's other external model implementations.
4
+
5
+ Example::
6
+
7
+ from deepmd_torchsim import DeepmdModel
8
+
9
+ model = DeepmdModel(model_path="frozen_model.pth", device="cuda")
10
+ results = model(sim_state)
11
+ energy = results["energy"] # [n_systems]
12
+ forces = results["forces"] # [n_atoms, 3]
13
+ stress = results["stress"] # [n_systems, 3, 3]
14
+
15
+ References:
16
+ - DeePMD-kit: https://github.com/deepmodeling/deepmd-kit
17
+ - torch-sim ModelInterface: torch_sim/models/interface.py
18
+ """
19
+ #######################################################################################
20
+ from __future__ import annotations
21
+
22
+ import traceback
23
+ import warnings
24
+ from pathlib import Path
25
+ from typing import TYPE_CHECKING
26
+
27
+ import numpy as np
28
+ import torch
29
+ from ase.data import atomic_numbers as ase_atomic_numbers
30
+ from torch_sim.models.interface import ModelInterface
31
+
32
+ if TYPE_CHECKING:
33
+ from torch_sim.state import SimState
34
+
35
+ # Importing this module must not fail if `deepmd` isn't installed; the
36
+ # ImportError is deferred until someone constructs a DeepmdModel.
37
+ try:
38
+ from deepmd.infer import DeepPot
39
+
40
+ _IMPORT_ERROR: ImportError | None = None
41
+ except ImportError as exc:
42
+ warnings.warn(f"deepmd import failed: {traceback.format_exc()}", stacklevel=2)
43
+ _IMPORT_ERROR = exc
44
+ #######################################################################################
45
+
46
+ class DeepmdModel(ModelInterface):
47
+ """torch-sim wrapper for a frozen DeePMD-kit PyTorch model.
48
+
49
+ Loads ``frozen_model.pth`` using ``deepmd.infer.DeepPot`` and evaluates
50
+ :class:`~torch_sim.state.SimState` objects, returning batched energies,
51
+ forces, and stresses.
52
+
53
+ Forward evaluation:
54
+ Systems are grouped by atom count and species ordering. Since
55
+ ``DeepPot.eval`` can evaluate multiple frames in a single call only
56
+ when they share the same ``atom_types`` array, each group is evaluated
57
+ independently and the results are scattered back into the original
58
+ batch order.
59
+
60
+ Groups containing a single system require only one ``eval`` call, so
61
+ mixed-size batches have the same evaluation cost as an ungrouped
62
+ implementation. Grouping improves performance when batches contain
63
+ repeated systems, such as parallel replicas.
64
+
65
+ Species mapping:
66
+ DeePMD species indices are determined by their position in the model's
67
+ ``type_map`` (for example, ``["O", "H"]``), rather than by atomic
68
+ number. The mapping is read from ``DeepPot.get_type_map()`` when the
69
+ model is initialized and is not hardcoded.
70
+
71
+ Stress convention:
72
+ Stress is computed as
73
+
74
+ ``stress = -0.5 * (virial + virial.T) / volume``
75
+
76
+ corresponding to the Cauchy stress convention with tensile stress
77
+ positive. This matches the conventions used by DeePMD's ASE
78
+ calculator and torch-sim's ``pair_potential.py``.
79
+
80
+ Attributes:
81
+ type_map (list[str]): Element symbols in the species-index order
82
+ defined by the frozen DeePMD model.
83
+
84
+ Examples:
85
+ ```py
86
+ model = DeepmdModel(
87
+ model_path="frozen_model.pth",
88
+ device=torch.device("cuda"),
89
+ compute_forces=True,
90
+ compute_stress=True,
91
+ )
92
+ results = model(sim_state)
93
+ ```
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ model_path: str | Path,
99
+ device: torch.device | str | None = None,
100
+ dtype: torch.dtype = torch.float64,
101
+ *,
102
+ compute_forces: bool = True,
103
+ compute_stress: bool = True,
104
+ head: str | None = None,
105
+ ) -> None:
106
+ """Initialize the DeePMD-kit model wrapper.
107
+
108
+ Args:
109
+ model_path: Path to a frozen DeePMD-kit PyTorch-backend model
110
+ (``frozen_model.pth``).
111
+ device: Device the *output tensors* are placed on. Defaults to ``CUDA``
112
+ if available, else CPU. Note that ``DeepPot`` itself manages its
113
+ own internal device placement (it uses CUDA automatically if
114
+ available, independent of this argument); this argument only
115
+ controls where the torch tensors returned by :meth:`forward` live.
116
+ dtype: Floating-point dtype for the returned tensors. Defaults to
117
+ ``torch.float64`` to match the training precision of the example
118
+ se_e2_a water model this package was validated against.
119
+ compute_forces: Whether to compute and return atomic forces.
120
+ Defaults to True.
121
+ compute_stress: Whether to compute and return the stress tensor.
122
+ Defaults to True.
123
+ head: Task/domain head to select, for multitask models such as
124
+ DPA-3 foundation checkpoints (e.g. ``"Omat24"``). Ignored by
125
+ single-task models. Defaults to None, which lets ``DeepPot``
126
+ fall back to a model's own "Default" head if it has one, or
127
+ raise if the model is multitask and ambiguous.
128
+ """
129
+ if _IMPORT_ERROR is not None:
130
+ raise _IMPORT_ERROR
131
+ super().__init__()
132
+ self._device = (
133
+ torch.device(device)
134
+ if device is not None
135
+ else torch.device("cuda" if torch.cuda.is_available() else "cpu")
136
+ )
137
+ self._dtype = dtype
138
+ self._compute_forces = compute_forces
139
+ self._compute_stress = compute_stress
140
+ self._memory_scales_with = "n_atoms"
141
+
142
+ self.model_path = Path(model_path)
143
+ self._dp = DeepPot(str(self.model_path.resolve()), head=head)
144
+
145
+ # Read the type_map from the frozen model's own metadata rather than
146
+ # hardcoding an atomic-number -> DeePMD-type-index mapping.
147
+ self.type_map: list[str] = self._dp.get_type_map()
148
+ self._atomic_number_to_type_index: dict[int, int] = {
149
+ ase_atomic_numbers[symbol]: type_idx
150
+ for type_idx, symbol in enumerate(self.type_map)
151
+ }
152
+
153
+ def _atom_types_for_system(self, atomic_numbers: torch.Tensor) -> np.ndarray:
154
+ """Map a system's atomic numbers to DeePMD type-map indices.
155
+
156
+ Args:
157
+ atomic_numbers: Atomic numbers for the atoms in one system, shape
158
+ ``[n_atoms_in_system]``.
159
+
160
+ Returns:
161
+ np.ndarray: DeePMD type indices, shape ``[n_atoms_in_system]``, dtype
162
+ int.
163
+
164
+ Raises:
165
+ ValueError: If an atomic number is not present in the frozen model's
166
+ ``type_map`` (i.e. the model was not trained on that element).
167
+ """
168
+ numbers = atomic_numbers.detach().cpu().numpy().tolist()
169
+ try:
170
+ return np.array(
171
+ [self._atomic_number_to_type_index[z] for z in numbers], dtype=int
172
+ )
173
+ except KeyError as exc:
174
+ missing_z = exc.args[0]
175
+ raise ValueError(
176
+ f"Atomic number {missing_z} is not in the frozen model's type_map "
177
+ f"{self.type_map}; this model cannot evaluate that element."
178
+ ) from exc
179
+
180
+ def _eval_group(
181
+ self,
182
+ positions_list: list[np.ndarray],
183
+ cell_list: list[np.ndarray],
184
+ atom_types: np.ndarray,
185
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
186
+ """Evaluate a group of same-shape, same-species-order systems in one call.
187
+
188
+ All systems in the group share one ``atom_types`` array (same atom
189
+ count and species order) — see :meth:`forward` for how groups are
190
+ built. A group of size 1 is just a single-frame ``eval`` call.
191
+
192
+ Args:
193
+ positions_list: One ``[n_atoms, 3]`` position array (Angstrom) per
194
+ system in the group.
195
+ cell_list: One ``[3, 3]`` cell array per system, torch-sim's
196
+ column-vector convention (see :class:`~torch_sim.state.SimState`).
197
+ atom_types: DeePMD type indices shared by every system in the
198
+ group, shape ``[n_atoms]``.
199
+
200
+ Returns:
201
+ tuple[np.ndarray, np.ndarray, np.ndarray]: ``(energy, forces,
202
+ virial)`` where ``energy`` has shape ``[n_frames]``, ``forces`` has
203
+ shape ``[n_frames, n_atoms, 3]`` (eV/Angstrom), and ``virial`` has
204
+ shape ``[n_frames, 3, 3]`` (eV).
205
+ """
206
+ n_frames = len(positions_list)
207
+ coords = np.stack(
208
+ [p.astype(np.float64).reshape(-1) for p in positions_list], axis=0
209
+ )
210
+
211
+ # torch-sim stores cell column-vector-wise: [[a1,b1,c1],[a2,b2,c2],[a3,b3,c3]].
212
+ # DeepPot / ASE expect the row-vector convention
213
+ # [[a1,a2,a3],[b1,b2,b3],[c1,c2,c3]], i.e. the transpose.
214
+ cells = np.stack([c.astype(np.float64).T.reshape(-1) for c in cell_list], axis=0)
215
+
216
+ # atomic/fparam/aparam/mixed_type are passed explicitly to match one of
217
+ # DeepPot.eval's typed @overload stubs, which don't default them.
218
+ energy, force, virial = self._dp.eval(
219
+ coords=coords,
220
+ cells=cells,
221
+ atom_types=atom_types,
222
+ atomic=False,
223
+ fparam=None,
224
+ aparam=None,
225
+ mixed_type=False,
226
+ )[:3]
227
+ return energy[:, 0], force, virial.reshape(n_frames, 3, 3)
228
+
229
+ def forward(self, state: SimState, **_kwargs) -> dict[str, torch.Tensor]:
230
+ """Compute energy, forces, and stress for a (possibly batched) state.
231
+
232
+ Groups the systems present in ``state.system_idx`` by identical atom
233
+ count and species order, issues one ``DeepPot.eval`` call per group
234
+ (see :meth:`_eval_group` and the class docstring), and scatters the
235
+ per-system results back into batched output tensors.
236
+
237
+ Args:
238
+ state (SimState): Simulation state containing:
239
+ - positions: Atomic positions with shape [n_atoms, 3]
240
+ - cell: Unit cell vectors with shape [n_systems, 3, 3]
241
+ - system_idx: System indices for each atom with shape [n_atoms]
242
+ - atomic_numbers: Atomic numbers with shape [n_atoms]
243
+
244
+ Returns:
245
+ dict[str, torch.Tensor]: Computed properties:
246
+ - "energy": Potential energy, shape [n_systems] (eV)
247
+ - "forces": Atomic forces, shape [n_atoms, 3] (eV/Angstrom;
248
+ only if compute_forces=True)
249
+ - "stress": Cauchy stress, shape [n_systems, 3, 3]
250
+ (eV/Angstrom^3; only if compute_stress=True)
251
+ """
252
+ n_systems = int(state.system_idx.max().item()) + 1
253
+ n_atoms = state.positions.shape[0]
254
+
255
+ energies = torch.zeros(n_systems, dtype=self._dtype, device=self._device)
256
+ forces_out = (
257
+ torch.zeros((n_atoms, 3), dtype=self._dtype, device=self._device)
258
+ if self._compute_forces
259
+ else None
260
+ )
261
+ stress_out = (
262
+ torch.zeros((n_systems, 3, 3), dtype=self._dtype, device=self._device)
263
+ if self._compute_stress
264
+ else None
265
+ )
266
+
267
+ system_masks = [state.system_idx == sys_idx for sys_idx in range(n_systems)]
268
+ system_atom_types = [
269
+ self._atom_types_for_system(state.atomic_numbers[mask])
270
+ for mask in system_masks
271
+ ]
272
+
273
+ # Group systems by atom-type sequence; DeepPot.eval only batches frames
274
+ # that share one atom_types array. Unmatched systems form a group of 1.
275
+ groups: dict[tuple[int, ...], list[int]] = {}
276
+ for sys_idx, atype in enumerate(system_atom_types):
277
+ groups.setdefault(tuple(atype.tolist()), []).append(sys_idx)
278
+
279
+ for sys_indices in groups.values():
280
+ shared_atom_types = system_atom_types[sys_indices[0]]
281
+ positions_list = [
282
+ state.positions[system_masks[i]].detach().cpu().numpy()
283
+ for i in sys_indices
284
+ ]
285
+ cell_list = [state.cell[i].detach().cpu().numpy() for i in sys_indices]
286
+
287
+ energy, force, virial = self._eval_group(
288
+ positions_list, cell_list, shared_atom_types
289
+ )
290
+
291
+ for local_i, sys_idx in enumerate(sys_indices):
292
+ energies[sys_idx] = float(energy[local_i])
293
+ mask = system_masks[sys_idx]
294
+
295
+ if forces_out is not None:
296
+ forces_out[mask] = torch.tensor(
297
+ force[local_i], dtype=self._dtype, device=self._device
298
+ )
299
+
300
+ if stress_out is not None:
301
+ # cell is stored column-vector-wise; volume is basis-independent.
302
+ volume = torch.abs(torch.det(state.cell[sys_idx])).item()
303
+ stress = -0.5 * (virial[local_i] + virial[local_i].T) / volume
304
+ stress_out[sys_idx] = torch.tensor(
305
+ stress, dtype=self._dtype, device=self._device
306
+ )
307
+
308
+ results: dict[str, torch.Tensor] = {"energy": energies}
309
+ if forces_out is not None:
310
+ results["forces"] = forces_out
311
+ if stress_out is not None:
312
+ results["stress"] = stress_out
313
+ return results
314
+ #######################################################################################
315
+ # End of File
316
+ #######################################################################################
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepmd-torchsim
3
+ Version: 0.1.0
4
+ Summary: DeePMD-kit (PyTorch backend) ModelInterface implementation for torch-sim
5
+ Author-email: rverma7@ncsu.edu
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: torch-sim-atomistic>=0.6.1
11
+ Requires-Dist: torch>=2
12
+ Requires-Dist: numpy
13
+ Requires-Dist: ase>=3.26
14
+ Provides-Extra: deepmd
15
+ Requires-Dist: deepmd-kit==3.1.3; extra == "deepmd"
16
+ Requires-Dist: torch==2.10.0; extra == "deepmd"
17
+ Requires-Dist: mpich; extra == "deepmd"
18
+ Requires-Dist: e3nn; extra == "deepmd"
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest>=8; extra == "test"
21
+ Dynamic: license-file
22
+
23
+ # deepmd-torchsim
24
+
25
+ A [torch-sim](https://github.com/TorchSim/torch-sim) `ModelInterface` implementation
26
+ for [DeePMD-kit](https://github.com/deepmodeling/deepmd-kit)'s PyTorch backend.
27
+
28
+ ## Install
29
+
30
+ The package (`DeepmdModel`, torch-sim `ModelInterface` wrapper) can be installed with either `pip` and `uv`:
31
+
32
+ ```bash
33
+ # from PyPI (once published) or a local checkout
34
+ pip install deepmd-torchsim
35
+ uv pip install deepmd-torchsim
36
+ uv add deepmd-torchsim
37
+
38
+ # editable, from a local checkout
39
+ pip install -e .
40
+ uv pip install -e .
41
+ ```
42
+
43
+ ### Getting a working `deepmd-kit` backend
44
+
45
+ ```bash
46
+ pip install "deepmd-torchsim[deepmd]"
47
+ uv pip install "deepmd-torchsim[deepmd]"
48
+ ```
49
+
50
+ The `deepmd` extra pins an working `deepmd-kit==3.1.3`, with `torch==2.10.0`.
51
+
52
+ ## Usage
53
+
54
+ ```python
55
+ import torch
56
+ from deepmd_torchsim import DeepmdModel
57
+ import torch_sim as ts
58
+ from ase.build import molecule
59
+
60
+ model = DeepmdModel(
61
+ model_path="frozen_model.pth",
62
+ device=torch.device("cuda" if torch.cuda.is_available() else "cpu"),
63
+ compute_forces=True,
64
+ compute_stress=True,
65
+ )
66
+
67
+ state = ts.io.atoms_to_state([molecule("H2O")], model.device, model.dtype)
68
+ results = model(state)
69
+ print(results["energy"]) # [n_systems]
70
+ print(results["forces"]) # [n_atoms, 3]
71
+ print(results["stress"]) # [n_systems, 3, 3]
72
+ ```
73
+
74
+ For multitask/multi-domain foundation checkpoints (e.g. DPA-3), pass `head=`
75
+ to select which trained domain to evaluate with:
76
+
77
+ ```python
78
+ model = DeepmdModel(model_path="DPA-3.1-3M.pt", head="Omat24")
79
+ ```
80
+ ## Tests
81
+
82
+ ```bash
83
+ pytest tests/
84
+ ```
85
+
86
+ ## License
87
+
88
+ This project is licensed under the [MIT License](https://github.com/rahulumrao/deepmd_torchsim/blob/main/LICENSE).
89
+
90
+ ## Author
91
+ Rahul Verma \
92
+ Email: rverma7@ncsu.edu
93
+
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ deepmd_torchsim/__init__.py
5
+ deepmd_torchsim/model.py
6
+ deepmd_torchsim.egg-info/PKG-INFO
7
+ deepmd_torchsim.egg-info/SOURCES.txt
8
+ deepmd_torchsim.egg-info/dependency_links.txt
9
+ deepmd_torchsim.egg-info/requires.txt
10
+ deepmd_torchsim.egg-info/top_level.txt
11
+ tests/test_deepmd.py
@@ -0,0 +1,13 @@
1
+ torch-sim-atomistic>=0.6.1
2
+ torch>=2
3
+ numpy
4
+ ase>=3.26
5
+
6
+ [deepmd]
7
+ deepmd-kit==3.1.3
8
+ torch==2.10.0
9
+ mpich
10
+ e3nn
11
+
12
+ [test]
13
+ pytest>=8
@@ -0,0 +1 @@
1
+ deepmd_torchsim
@@ -0,0 +1,74 @@
1
+ [project]
2
+ name = "deepmd-torchsim"
3
+ version = "0.1.0"
4
+ description = "DeePMD-kit (PyTorch backend) ModelInterface implementation for torch-sim"
5
+ readme = "README.md"
6
+ authors = [{ email = "rverma7@ncsu.edu" }]
7
+ license = "MIT"
8
+ requires-python = ">=3.12"
9
+ dependencies = [
10
+ "torch-sim-atomistic>=0.6.1",
11
+ "torch>=2",
12
+ "numpy",
13
+ "ase>=3.26",
14
+ ]
15
+
16
+ [project.optional-dependencies]
17
+ deepmd = ["deepmd-kit==3.1.3", "torch==2.10.0", "mpich", "e3nn"]
18
+ test = ["pytest>=8"]
19
+
20
+ [tool.pytest.ini_options]
21
+ filterwarnings = [
22
+ # deepmd-kit loads its frozen model via torch.jit.load internally; these
23
+ # deprecation warnings are from deepmd-kit's own code.
24
+ "ignore:`torch.jit.script` is deprecated.*:DeprecationWarning",
25
+ "ignore:`torch.jit.script_method` is deprecated.*:DeprecationWarning",
26
+ "ignore:`torch.jit.load` is deprecated.*:DeprecationWarning",
27
+ ]
28
+
29
+ [build-system]
30
+ requires = ["setuptools>=68"]
31
+ build-backend = "setuptools.build_meta"
32
+
33
+ [tool.setuptools.packages.find]
34
+ include = ["deepmd_torchsim*"]
35
+
36
+ [tool.ruff]
37
+ target-version = "py312"
38
+ line-length = 90
39
+ output-format = "concise"
40
+
41
+ [tool.ruff.lint]
42
+ select = ["ALL"]
43
+ ignore = [
44
+ "ANN002",
45
+ "ANN003",
46
+ "ANN401",
47
+ "COM812",
48
+ "CPY001",
49
+ "D205",
50
+ "EM101",
51
+ "EM102",
52
+ "PLR0913",
53
+ "PLR2004",
54
+ "TD",
55
+ "FIX002",
56
+ "TRY003",
57
+ ]
58
+ pydocstyle.convention = "google"
59
+
60
+ [tool.ruff.lint.per-file-ignores]
61
+ "**/tests/*" = [
62
+ "ANN001",
63
+ "ANN201",
64
+ "ANN202",
65
+ "D",
66
+ "INP001",
67
+ "PTH100",
68
+ "PTH110",
69
+ "PTH118",
70
+ "PTH120",
71
+ "PTH123",
72
+ "S101",
73
+ "T201",
74
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,121 @@
1
+ """Tests for :class:`deepmd_torchsim.DeepmdModel`: energy/forces, CPU vs GPU.
2
+
3
+ Self-contained: a MLIP model for CH4 molecule stored ``tests/model/frozen_model.pth``,
4
+ and the coordinates are written in this file, so this test doesn't depend on anything
5
+ outside this package.
6
+
7
+ ``tests/model/reference.json`` is a checked-in reference. Every run computes fresh
8
+ energy/forces on the default device (CUDA if available, else CPU) and compares
9
+ them to that reference.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ import warnings
17
+
18
+ import numpy as np
19
+ import pytest
20
+ import torch
21
+ import torch_sim as ts
22
+ from ase import Atoms
23
+
24
+ #######################################################################################
25
+ TESTS_DIR = os.path.dirname(os.path.abspath(__file__))
26
+ MODEL_PATH = os.path.join(TESTS_DIR, "model", "frozen_model.pth")
27
+ REFERENCE = os.path.join(TESTS_DIR, "model", "reference.json")
28
+ FLOAT64_DTYPE = torch.float64
29
+ DEFAULT_DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
30
+ #######################################################################################
31
+
32
+ try:
33
+ from deepmd_torchsim import DeepmdModel
34
+
35
+ _IMPORT_ERROR: str | None = None
36
+ except ImportError as exc: # pragma: no cover - environment dependent
37
+ _IMPORT_ERROR = str(exc)
38
+
39
+ pytestmark = pytest.mark.skipif(
40
+ _IMPORT_ERROR is not None or not os.path.exists(MODEL_PATH),
41
+ reason=(
42
+ f"can not import 'deepmd' or frozen model missing at {MODEL_PATH} "
43
+ f"(import error: {_IMPORT_ERROR})"
44
+ ),)
45
+ #######################################################################################
46
+ def build_system() -> Atoms:
47
+ """A tetrahedral CH4 molecule, centered in a 10 A cubic box."""
48
+ symbols = ["C", "H", "H", "H", "H"]
49
+ positions = [
50
+ [0.000000, 0.000000, 0.000000],
51
+ [0.627581, 0.627581, 0.627581],
52
+ [0.627581, -0.627581, -0.627581],
53
+ [-0.627581, 0.627581, -0.627581],
54
+ [-0.627581, -0.627581, 0.627581],
55
+ ]
56
+ box_size = 10.0
57
+ atoms = Atoms(symbols=symbols, positions=positions, cell=[box_size] * 3, pbc=True)
58
+ atoms.positions += box_size / 2 # center in the box
59
+ return atoms
60
+
61
+ #######################################################################################
62
+ def compute(device: torch.device) -> dict:
63
+ """Load the MLIP model on ``device`` and return result."""
64
+ model = DeepmdModel(
65
+ model_path=MODEL_PATH,
66
+ device=device,
67
+ dtype=FLOAT64_DTYPE,
68
+ compute_forces=True,
69
+ compute_stress=False,
70
+ )
71
+ state = ts.io.atoms_to_state([build_system()], device, FLOAT64_DTYPE)
72
+ output = model.forward(state)
73
+ return {
74
+ "energy_eV": output["energy"][0].item(),
75
+ "forces_eV_per_A": output["forces"].detach().cpu().tolist(),
76
+ }
77
+
78
+ #######################################################################################
79
+ def compare_result(label: str, result: dict, reference: dict) -> None:
80
+ """Compare against the reference; warn on mismatch."""
81
+ tolerance = 1e-5
82
+ energy_diff = abs(result["energy_eV"] - reference["energy_eV"])
83
+ forces_diff = (
84
+ torch.tensor(result["forces_eV_per_A"])
85
+ - torch.tensor(reference["forces_eV_per_A"])
86
+ ).abs().max().item()
87
+
88
+ if energy_diff >= tolerance or forces_diff >= tolerance:
89
+ with np.printoptions(precision=4, suppress=True, floatmode="fixed"):
90
+ warnings.warn(
91
+ f"{label} energy/forces do not match reference (tol {tolerance:.0e}):\n"
92
+ f" energy: computed={result['energy_eV']:.4f} eV, "
93
+ f"reference={reference['energy_eV']:.4f} eV, diff={energy_diff:.4f} eV\n"
94
+ f" forces: computed=\n{np.array(result['forces_eV_per_A'])}\n"
95
+ f" forces: reference=\n{np.array(reference['forces_eV_per_A'])}\n"
96
+ f" max abs force diff={forces_diff:.4f} eV/A",
97
+ stacklevel=2,
98
+ )
99
+ #######################################################################################
100
+ def test_energy_forces() -> None:
101
+ """Model loads and produces finite energy/forces on the default device
102
+ (CUDA if available, else CPU) and the results are compared against the
103
+ checked-in reference.
104
+ """
105
+ with open(REFERENCE) as f:
106
+ reference = json.load(f)
107
+
108
+ default_result = compute(DEFAULT_DEVICE)
109
+ assert torch.isfinite(torch.tensor(default_result["energy_eV"]))
110
+ assert torch.isfinite(torch.tensor(default_result["forces_eV_per_A"])).all()
111
+ default_reference = reference.get(DEFAULT_DEVICE.type, reference["cpu"])
112
+ compare_result(DEFAULT_DEVICE.type, default_result, default_reference)
113
+
114
+ if DEFAULT_DEVICE.type == "cuda":
115
+ cpu_result = compute(torch.device("cpu"))
116
+ assert torch.isfinite(torch.tensor(cpu_result["energy_eV"]))
117
+ assert torch.isfinite(torch.tensor(cpu_result["forces_eV_per_A"])).all()
118
+ compare_result("cpu", cpu_result, reference["cpu"])
119
+ #######################################################################################
120
+ # END of File
121
+ #######################################################################################