BUCToolkit 1.0b0__py3-none-any.whl
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.
- BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
- BUCToolkit/BatchGenerate/__init__.py +6 -0
- BUCToolkit/BatchGenerate/coords_interp.py +127 -0
- BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
- BUCToolkit/BatchGenerate/random_replace.py +88 -0
- BUCToolkit/BatchMC/MetropolisMC.py +210 -0
- BUCToolkit/BatchMC/_BaseMC.py +863 -0
- BUCToolkit/BatchMC/__init__.py +8 -0
- BUCToolkit/BatchMD/BiasedMD.py +276 -0
- BUCToolkit/BatchMD/NVE.py +92 -0
- BUCToolkit/BatchMD/NVT.py +510 -0
- BUCToolkit/BatchMD/_BaseMD.py +1251 -0
- BUCToolkit/BatchMD/__init__.py +18 -0
- BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
- BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
- BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
- BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
- BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
- BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
- BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
- BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
- BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
- BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
- BUCToolkit/BatchOptim/TS/__init__.py +10 -0
- BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
- BUCToolkit/BatchOptim/__init__.py +22 -0
- BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
- BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
- BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
- BUCToolkit/BatchOptim/frequency.py +496 -0
- BUCToolkit/BatchOptim/minimize/CG.py +256 -0
- BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
- BUCToolkit/BatchOptim/minimize/QN.py +205 -0
- BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
- BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
- BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
- BUCToolkit/BatchStructures/__init__.py +15 -0
- BUCToolkit/Preprocessing/__init__.py +7 -0
- BUCToolkit/Preprocessing/load_files.py +1680 -0
- BUCToolkit/Preprocessing/preprocessing.py +923 -0
- BUCToolkit/Preprocessing/sampling.py +299 -0
- BUCToolkit/Preprocessing/supercells.py +140 -0
- BUCToolkit/Preprocessing/write_files.py +524 -0
- BUCToolkit/StandardTemplate.py +417 -0
- BUCToolkit/__init__.py +33 -0
- BUCToolkit/_version.py +7 -0
- BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
- BUCToolkit/api/DataLoaders.py +321 -0
- BUCToolkit/api/Losses.py +170 -0
- BUCToolkit/api/Metrics.py +84 -0
- BUCToolkit/api/ModelOptims.py +296 -0
- BUCToolkit/api/MolecularDynamics.py +282 -0
- BUCToolkit/api/MonteCarlo.py +280 -0
- BUCToolkit/api/NEB.py +325 -0
- BUCToolkit/api/Predictor.py +292 -0
- BUCToolkit/api/StructureOptimization.py +484 -0
- BUCToolkit/api/Trainer.py +635 -0
- BUCToolkit/api/VibrationAnalysis.py +279 -0
- BUCToolkit/api/__init__.py +38 -0
- BUCToolkit/api/_io.py +1026 -0
- BUCToolkit/cli/__init__.py +20 -0
- BUCToolkit/cli/cli.py +934 -0
- BUCToolkit/cli/input_stub.py +140 -0
- BUCToolkit/cli/main.py +368 -0
- BUCToolkit/cli/print_logo.py +149 -0
- BUCToolkit/cli/template_model.py +6 -0
- BUCToolkit/io.py +30 -0
- BUCToolkit/utils/AtomSelector.py +64 -0
- BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
- BUCToolkit/utils/ElemListReduce.py +52 -0
- BUCToolkit/utils/Ewald.py +210 -0
- BUCToolkit/utils/IrregularTensorReformat.py +59 -0
- BUCToolkit/utils/_CheckModules.py +30 -0
- BUCToolkit/utils/_Element_info.py +527 -0
- BUCToolkit/utils/__init__.py +22 -0
- BUCToolkit/utils/_para_flatt_list.py +131 -0
- BUCToolkit/utils/_print_formatter.py +59 -0
- BUCToolkit/utils/check_structures.py +204 -0
- BUCToolkit/utils/function_utils.py +147 -0
- BUCToolkit/utils/grad_functions.py +69 -0
- BUCToolkit/utils/index_ops.py +432 -0
- BUCToolkit/utils/setup_loggers.py +137 -0
- buctoolkit-1.0b0.dist-info/METADATA +28 -0
- buctoolkit-1.0b0.dist-info/RECORD +88 -0
- buctoolkit-1.0b0.dist-info/WHEEL +5 -0
- buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
- buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
- buctoolkit-1.0b0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
""" Molecular Dynamics base framework with constrains """
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2024-2025.7.4, BUCToolkit.
|
|
4
|
+
# Authors: Pu Pengxin, Song Xin
|
|
5
|
+
# Version: 0.9a
|
|
6
|
+
# File: NVE.py
|
|
7
|
+
# Environment: Python 3.12
|
|
8
|
+
|
|
9
|
+
# ruff: noqa: E701, E702, E703
|
|
10
|
+
from typing import Iterable, Dict, Any, List, Literal, Optional, Callable, Sequence, Tuple # noqa: F401
|
|
11
|
+
|
|
12
|
+
import torch as th
|
|
13
|
+
from torch import nn
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from .._BaseMD import _BaseMD
|
|
17
|
+
from BUCToolkit.utils._print_formatter import FLOAT_ARRAY_FORMAT, SCIENTIFIC_ARRAY_FORMAT
|
|
18
|
+
from BUCToolkit.utils.grad_functions import bjvp, bhvp
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _rConstrBase(_BaseMD):
|
|
22
|
+
"""
|
|
23
|
+
Constrained Base Dynamics
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
time_step: float, time per step (ps).
|
|
27
|
+
max_step: int, maximum steps.
|
|
28
|
+
T_init: float, initial temperature, only to generate initial velocities of atoms by Maxwell-Boltzmann distribution. If V_init is given, T_init will be ignored.
|
|
29
|
+
constr_func: Callable, a tuple of Python functions as the constraint functions s_k(X) that map R^n -> R^k. It takes one or more arguments, one of which must be a Tensor, and returns one Tensor with shape (k, ). `None` for identity function. see example below.
|
|
30
|
+
constr_val: Callable[th.Tensor[1], th.Tensor] | th.Tensor, the constraint value of `constr_func`, i.e., constraints are `constr_func(X) = constr_val`.
|
|
31
|
+
By defining it as a callable constr_val = constr_val(t) where `t` is a scalar Tensor, it can be set to the time-dependent constraints.
|
|
32
|
+
constr_threshold: float, the threshold of constraint convergence (error of manifold violation)
|
|
33
|
+
output_structures_per_step: int, output structures per output_structures_per_step steps.
|
|
34
|
+
device: str|torch.device, device that program rum on.
|
|
35
|
+
verbose: int, control the detailed degree of output information. 0 for silence, 1 for output Energy and Forces per step, 2 for output all structures.
|
|
36
|
+
|
|
37
|
+
Examples for constrains:
|
|
38
|
+
def constr_func(X):
|
|
39
|
+
y = list()
|
|
40
|
+
# X: shape(N, D), Note the batch dimension would NOT be considered in constraints calculation of X.
|
|
41
|
+
# fix the distance between atoms (2, 4), (3, 7), (5, 8) into corresponding `constr_val[:3]`
|
|
42
|
+
y.append(th.linalg.norm(X[[2, 3, 5]] - X[[4, 7, 8]], dim=-1))
|
|
43
|
+
|
|
44
|
+
# fix the angle of atom7-atom5-atom8 and atom11-atom9-atom12 into corresponding `constr_val[3:6]`
|
|
45
|
+
x1 = X[[5, 9]]
|
|
46
|
+
x2 = X[[7, 11]]
|
|
47
|
+
x3 = X[[8, 12]]
|
|
48
|
+
y.append(
|
|
49
|
+
(
|
|
50
|
+
th.sum((x2 - x1) * (x3 - x1))
|
|
51
|
+
) / (th.linalg.norm(x2 - x1) * th.linalg.norm(x3 - x1))
|
|
52
|
+
)
|
|
53
|
+
z = th.cat(y)
|
|
54
|
+
return z
|
|
55
|
+
|
|
56
|
+
Methods:
|
|
57
|
+
run: run BatchMD.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
time_step: float,
|
|
63
|
+
max_step: int,
|
|
64
|
+
T_init: float = 298.15,
|
|
65
|
+
constr_func: Callable | None = None,
|
|
66
|
+
constr_val: Callable[[th.Tensor], th.Tensor|Tuple[th.Tensor]] | th.Tensor | None = None,
|
|
67
|
+
constr_threshold: float = 1e-5,
|
|
68
|
+
output_file: str | None = None,
|
|
69
|
+
output_structures_per_step: int = 1,
|
|
70
|
+
device: str | th.device = 'cpu',
|
|
71
|
+
verbose: int = 2
|
|
72
|
+
):
|
|
73
|
+
super().__init__(
|
|
74
|
+
time_step,
|
|
75
|
+
max_step,
|
|
76
|
+
T_init,
|
|
77
|
+
output_file,
|
|
78
|
+
output_structures_per_step,
|
|
79
|
+
device,
|
|
80
|
+
verbose
|
|
81
|
+
)
|
|
82
|
+
if constr_func is None:
|
|
83
|
+
constr_func = lambda X: th.tensor(0.)
|
|
84
|
+
self._lazy_calc_constr_val = False # use the value of constr_func(X_init) to determine constr_val (and fixed).
|
|
85
|
+
if isinstance(constr_val, th.Tensor):
|
|
86
|
+
self.is_const_constr = True
|
|
87
|
+
self.constr_val_func_raw = None
|
|
88
|
+
self.constr_val_now = constr_val.to(self.device)
|
|
89
|
+
elif callable(constr_val):
|
|
90
|
+
self.is_const_constr = False
|
|
91
|
+
self.constr_val_func_raw = constr_val
|
|
92
|
+
self.constr_val_now = constr_val(self.time_now)
|
|
93
|
+
elif constr_val is None:
|
|
94
|
+
self.is_const_constr = True
|
|
95
|
+
self.constr_val_func_raw = None
|
|
96
|
+
self._lazy_calc_constr_val = True
|
|
97
|
+
else:
|
|
98
|
+
raise TypeError(f'Expected `constr_val` is Callable or torch.Tensor, but got {type(constr_val)}')
|
|
99
|
+
|
|
100
|
+
if not isinstance(constr_func, Callable):
|
|
101
|
+
raise TypeError(f"`constr_func` must consist of callable, but got {type(constr_func)}")
|
|
102
|
+
self.constr_func = constr_func
|
|
103
|
+
self.Q = th.tensor([], device=self.device, dtype=th.float32) # Q(n_batch, n_atoms * n_dim, n_constr)
|
|
104
|
+
self.jac = None # Jacobian Matrix
|
|
105
|
+
self.R = th.tensor([], device=self.device, dtype=th.float32) # R(n_batch, n_constr, n_constr)
|
|
106
|
+
self.q = th.tensor([], device=self.device, dtype=th.float32) # (n_batch, n_constr, 1), solution of `R^T q = d/dt constr_val(t)`
|
|
107
|
+
self.sqrtM = None # M^1/2, (n_batch, n_atoms, n_dim)
|
|
108
|
+
self.negsqrtM = None # M^-1/2
|
|
109
|
+
self.max_proj_iter = 10
|
|
110
|
+
self.constr_thres = constr_threshold
|
|
111
|
+
self.lamb = None # constr force, i.e., the mu of Lagrange multipler
|
|
112
|
+
|
|
113
|
+
def initialize(
|
|
114
|
+
self,
|
|
115
|
+
func: Any | nn.Module,
|
|
116
|
+
X: th.Tensor,
|
|
117
|
+
Element_list: List[List[str]] | List[List[int]],
|
|
118
|
+
masses: th.Tensor,
|
|
119
|
+
V_init: th.Tensor | None = None,
|
|
120
|
+
grad_func: Any | nn.Module = None,
|
|
121
|
+
func_args: Sequence = tuple(),
|
|
122
|
+
func_kwargs: Dict | None = None,
|
|
123
|
+
grad_func_args: Sequence = tuple(),
|
|
124
|
+
grad_func_kwargs: Dict | None = None,
|
|
125
|
+
is_grad_func_contain_y: bool = True,
|
|
126
|
+
require_grad: bool = False,
|
|
127
|
+
batch_indices: List[int] | Tuple[int, ...] | th.Tensor | np.ndarray | None = None,
|
|
128
|
+
fixed_atom_tensor: Optional[th.Tensor] = None,
|
|
129
|
+
is_fix_mass_center: bool = False
|
|
130
|
+
):
|
|
131
|
+
self.sqrtM = th.sqrt(masses) # M^1/2, (n_batch, n_atoms, n_dim)
|
|
132
|
+
self.negsqrtM = 1 / th.sqrt(masses) # M^-1/2
|
|
133
|
+
_y_check = th.vmap(self.constr_func)(X)
|
|
134
|
+
if self._lazy_calc_constr_val:
|
|
135
|
+
self.constr_val_now = _y_check
|
|
136
|
+
# check constr_val shape
|
|
137
|
+
if _y_check.shape != self.constr_val_now.shape:
|
|
138
|
+
raise RuntimeError(
|
|
139
|
+
f'`constr_val` must have the same shape as what constr_func returned {self.constr_val_now.shape}, but got {_y_check.shape}.'
|
|
140
|
+
)
|
|
141
|
+
jac, y = self._jacobian(X)
|
|
142
|
+
if self.verbose:
|
|
143
|
+
self.logger.info(
|
|
144
|
+
f'Constraint values are now {np.array2string(self.constr_val_now.squeeze().numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}'
|
|
145
|
+
)
|
|
146
|
+
if y.ndim != 2:
|
|
147
|
+
raise ValueError(f'`constr_func` must return a 2D tensor of shape (n_batch, n_constr), but got {y.shape}.')
|
|
148
|
+
self._do_qr(jac)
|
|
149
|
+
ProjV = self._project1(V_init)
|
|
150
|
+
Ek = th.sum(
|
|
151
|
+
masses * V_init ** 2,
|
|
152
|
+
dim=(-2, -1),
|
|
153
|
+
keepdim=True
|
|
154
|
+
)
|
|
155
|
+
Ek_p = th.sum(
|
|
156
|
+
masses * ProjV ** 2,
|
|
157
|
+
dim=(-2, -1),
|
|
158
|
+
keepdim=True
|
|
159
|
+
)
|
|
160
|
+
V_init.copy_(th.where(Ek_p < 1e-5, 0., th.sqrt(Ek/Ek_p) * ProjV))
|
|
161
|
+
self.free_degree -= jac.shape[1] # reduce the constr. free deg.
|
|
162
|
+
# recalculate target Ek under constraints
|
|
163
|
+
_, n_atom, n_dim = X.shape
|
|
164
|
+
# target kinetic energy for NVT|NPT ensembles
|
|
165
|
+
if batch_indices: # Unit: eV/atom. Boltzmann constant kB = 8.6173332621e-5 eV/K
|
|
166
|
+
self.EK_TARGET = th.tensor(
|
|
167
|
+
[((self.free_degree / 2.) * 8.617333262145e-5 * self.T_init) for _n_atom in batch_indices],
|
|
168
|
+
dtype=X.dtype,
|
|
169
|
+
device=self.device
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
self.EK_TARGET = (self.free_degree / 2.) * 8.617333262145e-5 * self.T_init
|
|
173
|
+
# calc. constr. intensity
|
|
174
|
+
n_batch, n_constr, _ = self.R.shape
|
|
175
|
+
self.lamb = th.zeros((n_batch, n_constr), device=self.device)
|
|
176
|
+
# debug
|
|
177
|
+
#self._debug_X_check = list()
|
|
178
|
+
#self._debug_V_check = list()
|
|
179
|
+
# ...
|
|
180
|
+
|
|
181
|
+
def _constr_func_wrapped(self, X, constr_val_now):
|
|
182
|
+
"""
|
|
183
|
+
Manage the constraint functions.
|
|
184
|
+
Converting constraint functions into s_k(X) = constr_func(X) - constr_val, thereby constraints are s_k(X) = 0.
|
|
185
|
+
Returns:
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
y = self.constr_func(X) - constr_val_now # (n_batch, n_constr, )
|
|
189
|
+
y = th.atleast_1d(y)
|
|
190
|
+
return y, y # repeat the output to use `has_aux` in th.func.jacrev to return the function values
|
|
191
|
+
|
|
192
|
+
def _constr_func_for_hvp(self, X):
|
|
193
|
+
"""
|
|
194
|
+
Manage the constraint functions used for HVP.
|
|
195
|
+
Converting constraint functions into s_k(X) = constr_func(X) - constr_val, thereby constraints are s_k(X) = 0.
|
|
196
|
+
Returns:
|
|
197
|
+
|
|
198
|
+
"""
|
|
199
|
+
y = self.constr_func(X) - self.constr_val_now # (n_batch, n_constr, )
|
|
200
|
+
y = th.atleast_1d(y)
|
|
201
|
+
return y
|
|
202
|
+
|
|
203
|
+
def _constr_val_wrapped(self, t):
|
|
204
|
+
"""
|
|
205
|
+
Manage the time-dependent constraint values.
|
|
206
|
+
Args:
|
|
207
|
+
t:
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
|
|
211
|
+
"""
|
|
212
|
+
if self.is_const_constr:
|
|
213
|
+
return None
|
|
214
|
+
else:
|
|
215
|
+
y = self.constr_val_func_raw(t)
|
|
216
|
+
if isinstance(y, (Tuple, List)):
|
|
217
|
+
y = th.vstack(y).mT
|
|
218
|
+
else:
|
|
219
|
+
y = y.reshape(-1, 1)
|
|
220
|
+
|
|
221
|
+
return y, y
|
|
222
|
+
|
|
223
|
+
def _update_constr(self):
|
|
224
|
+
"""
|
|
225
|
+
Update constraint function value.
|
|
226
|
+
Returns:
|
|
227
|
+
|
|
228
|
+
"""
|
|
229
|
+
if not self.is_const_constr:
|
|
230
|
+
self.d_constr, self.constr_val_now = th.func.jacrev(self._constr_val_wrapped, has_aux=True)(self.time_now) # d s_k(r)/dt
|
|
231
|
+
|
|
232
|
+
def _jacobian(self, X: th.Tensor) -> Tuple[th.Tensor, th.Tensor]:
|
|
233
|
+
"""
|
|
234
|
+
Compute the Jacobian of all constrains.
|
|
235
|
+
Args:
|
|
236
|
+
X:
|
|
237
|
+
|
|
238
|
+
Returns: Jacobian (n_batch, n_constr, n_atoms, n_dim), and the constraint values.
|
|
239
|
+
|
|
240
|
+
"""
|
|
241
|
+
self._update_constr()
|
|
242
|
+
with th.no_grad():
|
|
243
|
+
jac, y = th.vmap(th.func.jacrev(self._constr_func_wrapped, has_aux=True))(X, self.constr_val_now)
|
|
244
|
+
self.jac = jac
|
|
245
|
+
return jac, y
|
|
246
|
+
|
|
247
|
+
def _do_qr(self, jac: th.Tensor):
|
|
248
|
+
"""
|
|
249
|
+
Do QR factorization and save/update self.Q.
|
|
250
|
+
Args:
|
|
251
|
+
jac: Jacobian matrix of s_k at X.
|
|
252
|
+
|
|
253
|
+
Returns: None
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
# weighted jac: J_w = J @ M^-1/2,
|
|
257
|
+
# (n_batch, n_constr, n_atoms, n_dim) * (n_batch, 1, n_atoms, n_dim) -flatten-> (n_batch, n_constr, n_atoms * n_dim)
|
|
258
|
+
_jac = th.flatten(jac * self.negsqrtM.unsqueeze(1), -2, -1)
|
|
259
|
+
_jacT = _jac.mT.contiguous()
|
|
260
|
+
th.linalg.qr(_jacT, out=(self.Q, self.R)) # Q(n_batch, n_atoms * n_dim, n_constr) , R(n_batch, n_constr, n_constr)
|
|
261
|
+
|
|
262
|
+
def _project1(self, X:th.Tensor) -> th.Tensor:
|
|
263
|
+
"""
|
|
264
|
+
Project X into the manifold defined by all constrains by QR factorization.
|
|
265
|
+
Usually used for update velocity V.
|
|
266
|
+
for Jacobian matrix J of constraints s_k at X, let J^T M^-1/2 = QR, thus s_k(X + v * dt) = s_k(X) + R^T Q^T M^1/2 v * dt + O(dt^2)
|
|
267
|
+
define the projector: P = M^-1/2 Q_2 Q_2^T M^1/2 = M^-1/2 (I - Q_1 Q_1^T) M^1/2, where Q = [Q_1, Q_2], Q_1 is from the financial QRF.
|
|
268
|
+
Args:
|
|
269
|
+
X: the input tensor of shape (n_batch, n_atom, n_dim). It might be coordinates, velocity, and higher deviations.
|
|
270
|
+
|
|
271
|
+
Returns: th.Tensor, the projected X.
|
|
272
|
+
|
|
273
|
+
"""
|
|
274
|
+
n_batch, n_atoms, n_dim = X.shape
|
|
275
|
+
sqrtM = self.sqrtM # M^1/2, (n_batch, n_atoms, n_dim)
|
|
276
|
+
negsqrtM = self.negsqrtM # M^-1/2
|
|
277
|
+
# P = M^-1/2 @ (I - Q Q^T) @ M^1/2 = I - M^-1/2 @ Q @ Q^T @ M^1/2
|
|
278
|
+
Q = self.Q # Q(n_batch, n_atoms * n_dim, n_constr)
|
|
279
|
+
Px = (sqrtM * X).reshape(n_batch, n_atoms * n_dim, 1).contiguous() # (n_batch, n_atoms * n_dim, 1)
|
|
280
|
+
Px = (Q @ (Q.mT.contiguous() @ Px)).reshape(n_batch, n_atoms, n_dim) # (n_batch, n_atoms, n_dim)
|
|
281
|
+
Px.mul_(negsqrtM)
|
|
282
|
+
Px = X - Px
|
|
283
|
+
# manifold veloc. correction
|
|
284
|
+
if not self.is_const_constr: # time-dependent constraints
|
|
285
|
+
th.linalg.solve_triangular(self.R.mT.contiguous(), self.d_constr.unsqueeze(-1), upper=False, out=self.q)
|
|
286
|
+
Px.add_(self.negsqrtM * (self.Q @ self.q).reshape(n_batch, n_atoms, n_dim))
|
|
287
|
+
|
|
288
|
+
return Px
|
|
289
|
+
|
|
290
|
+
def _project_norm(self, X:th.Tensor) -> th.Tensor:
|
|
291
|
+
"""
|
|
292
|
+
Project X to normal space.
|
|
293
|
+
for Jacobian matrix J of constraints s_k at X, let J^T M^-1/2 = QR, thus s_k(X + v * dt) = s_k(X) + R^T Q^T M^1/2 v * dt + O(dt^2)
|
|
294
|
+
define the projector: P = M^-1/2 Q_2 Q_2^T M^1/2 = M^-1/2 (I - Q_1 Q_1^T) M^1/2, where Q = [Q_1, Q_2], Q_1 is from the financial QRF.
|
|
295
|
+
Args:
|
|
296
|
+
X: the input tensor of shape (n_batch, n_atom, n_dim). It might be coordinates, velocity, and higher deviations.
|
|
297
|
+
|
|
298
|
+
Returns: th.Tensor, the projected X.
|
|
299
|
+
|
|
300
|
+
"""
|
|
301
|
+
n_batch, n_atoms, n_dim = X.shape
|
|
302
|
+
sqrtM = self.sqrtM # M^1/2, (n_batch, n_atoms, n_dim)
|
|
303
|
+
negsqrtM = self.negsqrtM # M^-1/2
|
|
304
|
+
# P = M^-1/2 @ (I - Q Q^T) @ M^1/2 = I - M^-1/2 @ Q @ Q^T @ M^1/2
|
|
305
|
+
Q = self.Q # Q(n_batch, n_atoms * n_dim, n_constr)
|
|
306
|
+
Px = (sqrtM * X).reshape(n_batch, n_atoms * n_dim, 1).contiguous() # (n_batch, n_atoms * n_dim, 1)
|
|
307
|
+
Px = (Q @ (Q.mT.contiguous() @ Px)).reshape(n_batch, n_atoms, n_dim) # (n_batch, n_atoms, n_dim)
|
|
308
|
+
Px.mul_(negsqrtM)
|
|
309
|
+
|
|
310
|
+
return Px
|
|
311
|
+
|
|
312
|
+
def _project2(self, X:th.Tensor) -> th.Tensor:
|
|
313
|
+
r"""
|
|
314
|
+
Continuously project the Jacobian of all constrains to the exact manifold (by Newton-like iteration).
|
|
315
|
+
Update X in-place.
|
|
316
|
+
Args:
|
|
317
|
+
X: the input tensor of shape (n_batch, n_atom, n_dim). It might be coordinates, velocity, and higher deviations.
|
|
318
|
+
|
|
319
|
+
Returns: th.Tensor, the constraint forces \lambda.
|
|
320
|
+
|
|
321
|
+
"""
|
|
322
|
+
n_batch, n_atoms, n_dim = X.shape
|
|
323
|
+
_lamb = th.zeros(n_batch, self.R.shape[-1], 1, device=self.device)
|
|
324
|
+
|
|
325
|
+
# MAIN loop
|
|
326
|
+
for i in range(0, self.max_proj_iter):
|
|
327
|
+
# update Jacobian
|
|
328
|
+
jac, y = self._jacobian(X)
|
|
329
|
+
# print
|
|
330
|
+
constr_err = th.max(th.abs(y)).item()
|
|
331
|
+
if self.verbose > 0:
|
|
332
|
+
self.logger.info(f'{i: < 3d} Constraint errors are now: {constr_err:.4e}')
|
|
333
|
+
# threshold
|
|
334
|
+
if constr_err < self.constr_thres:
|
|
335
|
+
# constr. forces lambda
|
|
336
|
+
Fc = th.linalg.solve_triangular(self.R, _lamb, upper=True)
|
|
337
|
+
# unit conversion
|
|
338
|
+
Fc *= 207.28617 / (self.time_step ** 2) # convert g/mol Angstrom^2 fs^-2 to eV/Atom
|
|
339
|
+
return Fc
|
|
340
|
+
# QR factor.
|
|
341
|
+
self._do_qr(jac)
|
|
342
|
+
# P = M^1/2 @ (I - Q Q^T) @ M^-1/2
|
|
343
|
+
# Q(n_batch, n_atoms * n_dim, n_constr)
|
|
344
|
+
_lamb = th.linalg.solve_triangular(self.R.mT.contiguous(), y.unsqueeze(-1) , upper=False) # (n_batch, n_constr, 1)
|
|
345
|
+
# (n_batch, n_atoms, n_dim) * (n_batch, n_atoms * n_dim, n_constr) @ (n_batch, n_constr, 1)
|
|
346
|
+
corr = self.negsqrtM * (self.Q @ _lamb).reshape(n_batch, n_atoms, n_dim)
|
|
347
|
+
X.add_(corr, alpha=-1.)
|
|
348
|
+
#V.add_(corr/self.time_step, alpha=-1.)
|
|
349
|
+
|
|
350
|
+
# if not converged
|
|
351
|
+
self.logger.warning("Projection of X to the manifold is not converged.")
|
|
352
|
+
# constr. forces lambda
|
|
353
|
+
Fc = th.linalg.solve_triangular(self.R, _lamb, upper=True)
|
|
354
|
+
# unit conversion
|
|
355
|
+
Fc *= 207.28617 / (self.time_step ** 2) # convert g/mol Angstrom^2 fs^-2 to eV/Atom
|
|
356
|
+
return Fc
|
|
357
|
+
|
|
358
|
+
def _project1_std(self, X:th.Tensor) -> th.Tensor:
|
|
359
|
+
"""
|
|
360
|
+
Compute the projector for 1st-order quantity (e.g. velocity) of all constrains by QR factorization.
|
|
361
|
+
for Jacobian matrix J of constraints s_k at X, let J^T M^-1/2 = QR, thus s_k(X + v * dt) = s_k(X) + R^T Q^T M^1/2 v * dt + O(dt^2)
|
|
362
|
+
define the projector: P = M^1/2 Q_2 Q_2^T M^-1/2 = M^1/2 (I - Q_1 Q_1^T) M^-1/2, where Q = [Q_1, Q_2], Q_1 is from the financial QRF.
|
|
363
|
+
Args:
|
|
364
|
+
X: the input tensor of shape (n_batch, n_atom, n_dim). It might be coordinates, velocity, and higher deviations.
|
|
365
|
+
|
|
366
|
+
Returns: th.Tensor, the projected X.
|
|
367
|
+
|
|
368
|
+
"""
|
|
369
|
+
n_batch, n_atoms, n_dim = X.shape
|
|
370
|
+
sqrtM = self.sqrtM # M^1/2, (n_batch, n_atoms, n_dim)
|
|
371
|
+
negsqrtM = self.negsqrtM # M^-1/2
|
|
372
|
+
# P = M^-1/2 @ (I - Q Q^T) @ M^1/2 = I - M^-1/2 @ Q @ Q^T @ M^1/2
|
|
373
|
+
Q = self.Q # Q(n_batch, n_atoms * n_dim, n_constr)
|
|
374
|
+
Px = th.diag_embed(self.sqrtM.flatten(-2, -1), dim1=1, dim2=2) @ X.reshape(n_batch, n_atoms * n_dim, 1).contiguous() # (n_batch, n_atoms * n_dim, 1)
|
|
375
|
+
Px = (Q @ (Q.mT.contiguous() @ Px)) # (n_batch, n_atoms, n_dim)
|
|
376
|
+
Px = th.diag_embed(self.negsqrtM.flatten(-2, -1), dim1=1, dim2=2) @ Px
|
|
377
|
+
Px = X - Px.reshape(n_batch, n_atoms, n_dim)
|
|
378
|
+
# manifold veloc. correction
|
|
379
|
+
if not self.is_const_constr: # time-dependent constraints
|
|
380
|
+
th.linalg.solve_triangular(self.R.mT.contiguous(), self.d_constr, upper=False, out=self.q)
|
|
381
|
+
Px.add_(self.negsqrtM * (self.Q @ self.q.unsqueeze(-1)).reshape(n_batch, n_atoms, n_dim))
|
|
382
|
+
|
|
383
|
+
return Px
|
|
384
|
+
|
|
385
|
+
def _project2_std(self, X:th.Tensor) -> None:
|
|
386
|
+
"""
|
|
387
|
+
Compute position corrections by directly solving JM^-1J^T q = s(X)
|
|
388
|
+
Args:
|
|
389
|
+
X:
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
|
|
393
|
+
"""
|
|
394
|
+
n_batch, n_atoms, n_dim = X.shape
|
|
395
|
+
for i in range(0, self.max_proj_iter):
|
|
396
|
+
# update Jacobian
|
|
397
|
+
jac, y = self._jacobian(X) # jac: (n_batch, n_constr, n_atom, n_dim)
|
|
398
|
+
# print
|
|
399
|
+
constr_err = th.max(th.abs(y)).item()
|
|
400
|
+
lamb = 0.
|
|
401
|
+
if self.verbose > 0:
|
|
402
|
+
self.logger.info(f'{i: < 3d} Constraint errors are now: {constr_err:.4e}')
|
|
403
|
+
# threshold
|
|
404
|
+
if constr_err < self.constr_thres:
|
|
405
|
+
return
|
|
406
|
+
# solve J M^-1 J^T q = s
|
|
407
|
+
Z = jac*self.negsqrtM # TODO
|
|
408
|
+
# Q(n_batch, n_atoms * n_dim, n_constr)
|
|
409
|
+
corr = th.linalg.solve_triangular(self.R.mT.contiguous(), y.unsqueeze(-1) , upper=False) # (n_batch, n_constr, 1)
|
|
410
|
+
# (n_batch, n_atoms, n_dim) * (n_batch, n_atoms * n_dim, n_constr) @ (n_batch, n_constr, 1)
|
|
411
|
+
corr = self.negsqrtM * (self.Q @ corr).reshape(n_batch, n_atoms, n_dim)
|
|
412
|
+
X.add_(corr, alpha=-1.)
|
|
413
|
+
#V.add_(corr/self.time_step, alpha=-1.)
|
|
414
|
+
|
|
415
|
+
# if not converged
|
|
416
|
+
self.logger.warning("Projection of X to the manifold is not converged.")
|
|
417
|
+
|
|
418
|
+
def _jacobian_derivative(self, X, v: th.Tensor) -> th.Tensor:
|
|
419
|
+
"""
|
|
420
|
+
Compute `dJ/dt = v^T H` by auto-differentiation.
|
|
421
|
+
Args:
|
|
422
|
+
X:
|
|
423
|
+
v:
|
|
424
|
+
|
|
425
|
+
Returns: v^T H with shape (n_batch, n_constr, n_atom, n_dim)
|
|
426
|
+
|
|
427
|
+
"""
|
|
428
|
+
vH: th.Tensor = bhvp(self._constr_func_for_hvp, (X,), (v,), False) # (n_batch, n_constr, n_atom, n_dim)
|
|
429
|
+
return vH.contiguous()
|
|
430
|
+
|
|
431
|
+
def christoffel_solver(self, X: th.Tensor, V:th.Tensor, max_iter=20, tol=1e-6, omega=0.7):
|
|
432
|
+
"""
|
|
433
|
+
iteratively solve the Christoffel symbol term by implicit midpoint method.
|
|
434
|
+
function implicit_midpoint_step(r_n, v_n, h, tol=1e-8, max_iter=10)
|
|
435
|
+
"""
|
|
436
|
+
raise NotImplementedError(f"Not implemented yet.")
|
|
437
|
+
n_batch, n_atom, n_dim = X.shape
|
|
438
|
+
V_old = V.clone()
|
|
439
|
+
V_new = V
|
|
440
|
+
X_old = X.clone()
|
|
441
|
+
X_new = X
|
|
442
|
+
X_new.add_(V_old, alpha=self.time_step)
|
|
443
|
+
|
|
444
|
+
for _ in range(max_iter):
|
|
445
|
+
X_mid = (X_new + X_old)/2. # r_{1/2} = (r_n + r_{n+1})/2
|
|
446
|
+
V_mid = (V_new + V_old)/2. # v_{1/2} = (v_n + v_{n+1})/2
|
|
447
|
+
# calc. Christoffel. Christoffel symbol is `M^-1/2 Q R^-T J' v`
|
|
448
|
+
dJ = self._jacobian_derivative(X_mid, V_mid) # the J', which J' = v^T H, shape: (n_batch, n_constr, n_atom, n_dim)
|
|
449
|
+
djr = th.einsum('ijkl, ikl -> ij', dJ, V_mid) # the `J' v`, (n_batch, n_constr)
|
|
450
|
+
# The `R^-T J' v`, solving (n_batch, n_constr, n_constr) x = (n_batch, n_constr, 1) -> x(n_batch, n_constr, 1)
|
|
451
|
+
_RTjr = th.linalg.solve_triangular(self.R.mT.contiguous(), djr.unsqueeze(-1), upper=False)
|
|
452
|
+
# Christoffel symbol: (B, N, N) @ (B, N, K) @ (B, K, 1) -> (n_batch, n_free, 1) -> (n_batch, n_atom, n_dim)
|
|
453
|
+
Christoffel = self.negsqrtM * (self.Q @ _RTjr).reshape(n_batch, n_atom, n_dim)
|
|
454
|
+
# residuals
|
|
455
|
+
resi_x = X_new - th.add(X_old, V_mid, alpha=self.time_step)
|
|
456
|
+
resi_v = V_new - th.add(V_old, Christoffel, alpha=-self.time_step)
|
|
457
|
+
converge_mask_x = th.linalg.norm(resi_x, dim=(-2, -1)) < self.constr_thres
|
|
458
|
+
converge_mask_v = th.linalg.norm(resi_v, dim=(-2, -1)) < self.constr_thres
|
|
459
|
+
if th.all(converge_mask_x & converge_mask_v):
|
|
460
|
+
return X_new, V_new
|
|
461
|
+
|
|
462
|
+
# denote B = M^-1/2 Q R^-T, B(n_batch, n_free, n_constr), n_free = n_atom * n_dim.
|
|
463
|
+
# B H v = B j': (n_batch, n_constr, n_free) @ (n_batch, n_constr, n_free)
|
|
464
|
+
# Here Newton algo. is used to solve the implicit midpoint eq.:
|
|
465
|
+
# X_new = X_old + dt * V_mid
|
|
466
|
+
# V_new = V_old + dt * B V_mid^T H V_mid
|
|
467
|
+
# and corresp. residuals:
|
|
468
|
+
# R_X = X_new - X_old - dt * V_mid
|
|
469
|
+
# R_V = V_new - V_old - dt * B V_mid^T H V_mid
|
|
470
|
+
#
|
|
471
|
+
# The Newton iter. formulae:
|
|
472
|
+
# [X_new, V_new]^T = [X_new, V_new]^T - J([R_X, R_V])^-1 [R_X, R_V]^T
|
|
473
|
+
# where J([R_X, R_V]) =
|
|
474
|
+
# [[I, -0.5 * dt * I],
|
|
475
|
+
# [DH, I + dt * B H V_mid]]
|
|
476
|
+
# DH = \partial R_V / \partial X_new, that requires the complicated derivative of H. Here we ignore this term.
|
|
477
|
+
# Hence J([R_X, R_V]) =
|
|
478
|
+
# [[I, -0.5 * dt * I],
|
|
479
|
+
# [O, I + dt * B H V_mid]]
|
|
480
|
+
# and J^-1 =
|
|
481
|
+
# [[I, 0.5 * dt * P],
|
|
482
|
+
# [O, P]]
|
|
483
|
+
# where P is the inverse of `I + dt * B H V_mid`.
|
|
484
|
+
#
|
|
485
|
+
# Hence, the iteration formulae are:
|
|
486
|
+
# * X_new = X_new - (R_X + 0.5 * dt * P @ R_V)
|
|
487
|
+
# * V_new = V_new - P @ R_V
|
|
488
|
+
# For solving P, Woodbury identity: (I + U V^T)^-1 = I - U (I + V^T U)^-1 V^T can be applied:
|
|
489
|
+
# denote `H V_mid` with dJ^T, (I + dt * B H V_mid)^-1 = (I + dt * B dJ^T)^-1
|
|
490
|
+
# (I + dt * B dJ^T)^-1 = I - dt * B (I + dJ^T B*dt)^-1 dJ^T
|
|
491
|
+
# Hence P @ R_V can be calculated as :
|
|
492
|
+
# P @ R_V = RV - dt * B @ linalg.solve(I + dJ^T @ B*dt, dJ^T @ R_V)
|
|
493
|
+
#
|
|
494
|
+
# Fully expand:
|
|
495
|
+
# * P @ R_V = RV - dt * M^-1/2 @ Q @ R^-T @ linalg.solve(I + dJ^T @ M^-1/2 @ Q @ R^-T * dt, dJ^T @ R_V)
|
|
496
|
+
|
|
497
|
+
# #######################################################################3
|
|
498
|
+
# J_mid, y = self._jacobian(r_mid) # (n_constr, n_atom * n_dim)
|
|
499
|
+
# self._do_qr(J_mid) # (n_f, n_constr), (n_constr, n_constr)
|
|
500
|
+
# J_dot_mid = self._jacobian_derivative(r_mid, v_mid).reshape(1, -1) # (n_constr, n_atom * n_dim)
|
|
501
|
+
# _v = v_mid.reshape(-1, 1)
|
|
502
|
+
# _y = J_dot_mid @ _v # (n_constr, n_f) @ (n_atom*n_dim, 1) = (n_constr, 1)
|
|
503
|
+
# _y = np.linalg.solve(R_mid.T, _y) # (n_constr, n_constr) @ (n_constr, 1) = (n_constr, 1)
|
|
504
|
+
# # (n_f, n_f) @ (n_f, n_constr) @ (n_constr, 1) = (n_f, 1)
|
|
505
|
+
# Gamma = M_sqrt_inv @ Q_mid @ _y
|
|
506
|
+
#
|
|
507
|
+
# # 带松弛的更新
|
|
508
|
+
# v_candidate = v_old - dt * Gamma.reshape(n_atom, n_dim)
|
|
509
|
+
# v_next = omega * v_candidate + (1 - omega) * v_new
|
|
510
|
+
#
|
|
511
|
+
# eps = np.linalg.norm(v_next - v_new)
|
|
512
|
+
# print(f'{_ + 1} Residuals: {eps}')
|
|
513
|
+
# if eps < tol:
|
|
514
|
+
# return v_next
|
|
515
|
+
#
|
|
516
|
+
# v_new = v_next
|
|
517
|
+
#
|
|
518
|
+
#print('Not Converged !')
|
|
519
|
+
#return v_new
|
|
520
|
+
|
|
521
|
+
def implicit_midpoint_step(
|
|
522
|
+
self,
|
|
523
|
+
X0,
|
|
524
|
+
X,
|
|
525
|
+
V0,
|
|
526
|
+
V,
|
|
527
|
+
F,
|
|
528
|
+
lamb,
|
|
529
|
+
func, grad_func_, func_args, func_kwargs, grad_func_args, grad_func_kwargs,
|
|
530
|
+
masses, atom_masks, is_grad_func_contain_y,
|
|
531
|
+
):
|
|
532
|
+
"""
|
|
533
|
+
Solve manifold dynamics by implicit midpoint method.
|
|
534
|
+
Here the input X0, V0 are at the endpoint in last step,
|
|
535
|
+
X, V are the predicted endpoint in this step,
|
|
536
|
+
input F (the potential forces) and lamb (the constraint forces) are at the midpoint of last step,
|
|
537
|
+
and returned X_mid, V_mid, Energy, Force and lamb are all at the **midpoint**.
|
|
538
|
+
The X, V in the end point are updated **in-place** to the input variables.
|
|
539
|
+
Args:
|
|
540
|
+
X:
|
|
541
|
+
X0:
|
|
542
|
+
V:
|
|
543
|
+
V0:
|
|
544
|
+
F:
|
|
545
|
+
lamb:
|
|
546
|
+
func:
|
|
547
|
+
grad_func_:
|
|
548
|
+
func_args:
|
|
549
|
+
func_kwargs:
|
|
550
|
+
grad_func_args:
|
|
551
|
+
grad_func_kwargs:
|
|
552
|
+
masses:
|
|
553
|
+
atom_masks:
|
|
554
|
+
is_grad_func_contain_y:
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
|
|
558
|
+
"""
|
|
559
|
+
n_batch, n_atoms, n_dim = X.shape
|
|
560
|
+
# MAIN LOOP
|
|
561
|
+
is_converged = False
|
|
562
|
+
for i in range(self.max_proj_iter):
|
|
563
|
+
X_mid = 0.5 * (X0 + X)
|
|
564
|
+
V_mid = 0.5 * (V0 + V)
|
|
565
|
+
# update Jacobian & QR factor.
|
|
566
|
+
jac, y = self._jacobian(X_mid) # (n_batch, n_constr, n_atoms, n_dim)
|
|
567
|
+
self._do_qr(jac)
|
|
568
|
+
# 1st forces eval
|
|
569
|
+
with th.set_grad_enabled(self.require_grad):
|
|
570
|
+
X_mid.requires_grad_(self.require_grad)
|
|
571
|
+
E_mid = func(X_mid, *func_args, **func_kwargs)
|
|
572
|
+
if is_grad_func_contain_y:
|
|
573
|
+
F_mid = - grad_func_(X_mid, E_mid, *grad_func_args, **grad_func_kwargs) * atom_masks
|
|
574
|
+
else:
|
|
575
|
+
F_mid = - grad_func_(X_mid, *grad_func_args, **grad_func_kwargs) * atom_masks
|
|
576
|
+
# residuals
|
|
577
|
+
f_constr = th.einsum('bijk, bi -> bjk', jac, lamb)
|
|
578
|
+
RX = X - X0 - V_mid * self.time_step
|
|
579
|
+
RV = V - V0 - (F_mid + f_constr) / masses * self.time_step * 9.64853329045427e-3
|
|
580
|
+
# check thres.
|
|
581
|
+
norm_X, norm_V, norm_s = th.linalg.norm(RX), th.linalg.norm(RV), th.linalg.norm(y)
|
|
582
|
+
if self.verbose > 0:
|
|
583
|
+
self.logger.info(
|
|
584
|
+
f'{i} residuals X, V, constraints: {norm_X.item():.4e}, {norm_V.item():.4e}, {norm_s.item():.4e}'
|
|
585
|
+
)
|
|
586
|
+
if (norm_X < self.constr_thres) and (norm_V < self.constr_thres) and (norm_s < self.constr_thres):
|
|
587
|
+
is_converged = True
|
|
588
|
+
break
|
|
589
|
+
|
|
590
|
+
# update V & X by quasi-Newton
|
|
591
|
+
tinvR_y = th.linalg.solve_triangular(self.R.mT.contiguous(), y.unsqueeze(-1) , upper=False) # R^-T y : (n_batch, n_constr, 1)
|
|
592
|
+
d_lambda = (
|
|
593
|
+
- 4./self.time_step * tinvR_y.squeeze(-1)
|
|
594
|
+
+ 2./self.time_step * th.einsum('bij, bi -> bj', self.Q, (self.sqrtM * RX).flatten(-2, -1))
|
|
595
|
+
+ th.einsum('bij, bi -> bj', self.Q, (self.sqrtM * (RV - 2*V_mid)).flatten(-2, -1))
|
|
596
|
+
)
|
|
597
|
+
d_lambda = 1./(self.time_step * 9.64853329045427e-3) * th.linalg.solve_triangular(self.R, d_lambda.unsqueeze(-1), upper=True).squeeze(-1)
|
|
598
|
+
ds = 4./self.time_step * self.negsqrtM * (self.Q @ tinvR_y).reshape(n_batch, n_atoms, n_dim)
|
|
599
|
+
dV = - ds - self._project1(RV) + 2./self.time_step * self._project_norm(RX)
|
|
600
|
+
dX = 0.5 * self.time_step * dV - RX
|
|
601
|
+
lamb.add_(d_lambda, alpha=1.)
|
|
602
|
+
X.add_(dX, alpha=1.)
|
|
603
|
+
V.add_(dV, alpha=1.)
|
|
604
|
+
if not is_converged:
|
|
605
|
+
self.logger.warning('Implicit midpoint method did not converge!')
|
|
606
|
+
|
|
607
|
+
return X_mid, V_mid, E_mid, F_mid, lamb
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Batched Verlet Molecular Dynamics by pytorch models or functions.
|
|
3
|
+
|
|
4
|
+
2024/7/30 PPX
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Copyright (c) 2024-2025.7.4, BUCToolkit.
|
|
8
|
+
# Authors: Pu Pengxin, Song Xin
|
|
9
|
+
# Version: 0.9a
|
|
10
|
+
# File: __init__.py
|
|
11
|
+
# Environment: Python 3.12
|
|
12
|
+
|
|
13
|
+
# ruff: noqa: E701, E702, E703
|
|
14
|
+
from .ConstrNVE import ConstrNVE
|
|
15
|
+
from .ConstrNVT import ConstrNVT
|
|
16
|
+
#from .BiasedMD import BiasedMD
|
|
17
|
+
|
|
18
|
+
__all__ = ['ConstrNVE', 'ConstrNVT']
|