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.
Files changed (88) hide show
  1. BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
  2. BUCToolkit/BatchGenerate/__init__.py +6 -0
  3. BUCToolkit/BatchGenerate/coords_interp.py +127 -0
  4. BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
  5. BUCToolkit/BatchGenerate/random_replace.py +88 -0
  6. BUCToolkit/BatchMC/MetropolisMC.py +210 -0
  7. BUCToolkit/BatchMC/_BaseMC.py +863 -0
  8. BUCToolkit/BatchMC/__init__.py +8 -0
  9. BUCToolkit/BatchMD/BiasedMD.py +276 -0
  10. BUCToolkit/BatchMD/NVE.py +92 -0
  11. BUCToolkit/BatchMD/NVT.py +510 -0
  12. BUCToolkit/BatchMD/_BaseMD.py +1251 -0
  13. BUCToolkit/BatchMD/__init__.py +18 -0
  14. BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
  15. BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
  16. BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
  17. BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
  18. BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
  19. BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
  20. BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
  21. BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
  22. BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
  23. BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
  24. BUCToolkit/BatchOptim/TS/__init__.py +10 -0
  25. BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
  26. BUCToolkit/BatchOptim/__init__.py +22 -0
  27. BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
  28. BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
  29. BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
  30. BUCToolkit/BatchOptim/frequency.py +496 -0
  31. BUCToolkit/BatchOptim/minimize/CG.py +256 -0
  32. BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
  33. BUCToolkit/BatchOptim/minimize/QN.py +205 -0
  34. BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
  35. BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
  36. BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
  37. BUCToolkit/BatchStructures/__init__.py +15 -0
  38. BUCToolkit/Preprocessing/__init__.py +7 -0
  39. BUCToolkit/Preprocessing/load_files.py +1680 -0
  40. BUCToolkit/Preprocessing/preprocessing.py +923 -0
  41. BUCToolkit/Preprocessing/sampling.py +299 -0
  42. BUCToolkit/Preprocessing/supercells.py +140 -0
  43. BUCToolkit/Preprocessing/write_files.py +524 -0
  44. BUCToolkit/StandardTemplate.py +417 -0
  45. BUCToolkit/__init__.py +33 -0
  46. BUCToolkit/_version.py +7 -0
  47. BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
  48. BUCToolkit/api/DataLoaders.py +321 -0
  49. BUCToolkit/api/Losses.py +170 -0
  50. BUCToolkit/api/Metrics.py +84 -0
  51. BUCToolkit/api/ModelOptims.py +296 -0
  52. BUCToolkit/api/MolecularDynamics.py +282 -0
  53. BUCToolkit/api/MonteCarlo.py +280 -0
  54. BUCToolkit/api/NEB.py +325 -0
  55. BUCToolkit/api/Predictor.py +292 -0
  56. BUCToolkit/api/StructureOptimization.py +484 -0
  57. BUCToolkit/api/Trainer.py +635 -0
  58. BUCToolkit/api/VibrationAnalysis.py +279 -0
  59. BUCToolkit/api/__init__.py +38 -0
  60. BUCToolkit/api/_io.py +1026 -0
  61. BUCToolkit/cli/__init__.py +20 -0
  62. BUCToolkit/cli/cli.py +934 -0
  63. BUCToolkit/cli/input_stub.py +140 -0
  64. BUCToolkit/cli/main.py +368 -0
  65. BUCToolkit/cli/print_logo.py +149 -0
  66. BUCToolkit/cli/template_model.py +6 -0
  67. BUCToolkit/io.py +30 -0
  68. BUCToolkit/utils/AtomSelector.py +64 -0
  69. BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
  70. BUCToolkit/utils/ElemListReduce.py +52 -0
  71. BUCToolkit/utils/Ewald.py +210 -0
  72. BUCToolkit/utils/IrregularTensorReformat.py +59 -0
  73. BUCToolkit/utils/_CheckModules.py +30 -0
  74. BUCToolkit/utils/_Element_info.py +527 -0
  75. BUCToolkit/utils/__init__.py +22 -0
  76. BUCToolkit/utils/_para_flatt_list.py +131 -0
  77. BUCToolkit/utils/_print_formatter.py +59 -0
  78. BUCToolkit/utils/check_structures.py +204 -0
  79. BUCToolkit/utils/function_utils.py +147 -0
  80. BUCToolkit/utils/grad_functions.py +69 -0
  81. BUCToolkit/utils/index_ops.py +432 -0
  82. BUCToolkit/utils/setup_loggers.py +137 -0
  83. buctoolkit-1.0b0.dist-info/METADATA +28 -0
  84. buctoolkit-1.0b0.dist-info/RECORD +88 -0
  85. buctoolkit-1.0b0.dist-info/WHEEL +5 -0
  86. buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
  87. buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
  88. 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']