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.
- deepmd_torchsim-0.1.0/LICENSE +21 -0
- deepmd_torchsim-0.1.0/PKG-INFO +93 -0
- deepmd_torchsim-0.1.0/README.md +71 -0
- deepmd_torchsim-0.1.0/deepmd_torchsim/__init__.py +12 -0
- deepmd_torchsim-0.1.0/deepmd_torchsim/model.py +316 -0
- deepmd_torchsim-0.1.0/deepmd_torchsim.egg-info/PKG-INFO +93 -0
- deepmd_torchsim-0.1.0/deepmd_torchsim.egg-info/SOURCES.txt +11 -0
- deepmd_torchsim-0.1.0/deepmd_torchsim.egg-info/dependency_links.txt +1 -0
- deepmd_torchsim-0.1.0/deepmd_torchsim.egg-info/requires.txt +13 -0
- deepmd_torchsim-0.1.0/deepmd_torchsim.egg-info/top_level.txt +1 -0
- deepmd_torchsim-0.1.0/pyproject.toml +74 -0
- deepmd_torchsim-0.1.0/setup.cfg +4 -0
- deepmd_torchsim-0.1.0/tests/test_deepmd.py +121 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -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,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
|
+
#######################################################################################
|