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,462 @@
1
+ """ Canonical ensemble (NVT) Molecular Dynamics via Verlet algo. """
2
+
3
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
4
+ # Authors: Pu Pengxin, Song Xin
5
+ # Version: 0.9a
6
+ # File: NVT.py
7
+ # Environment: Python 3.12
8
+
9
+ from typing import Iterable, Dict, Any, List, Literal, Optional, Callable, Sequence, Tuple # noqa: F401
10
+ import warnings
11
+
12
+ import torch as th
13
+ from torch import nn
14
+ import numpy as np
15
+
16
+ from BUCToolkit.utils.index_ops import index_reduce
17
+ from ._ConstrBaseMD import _rConstrBase
18
+ from BUCToolkit.utils._print_formatter import FLOAT_ARRAY_FORMAT, SCIENTIFIC_ARRAY_FORMAT
19
+
20
+
21
+ class ConstrNVT(_rConstrBase):
22
+ """
23
+ Canonical ensemble (NVT) molecular dynamics with constraints.
24
+
25
+ Parameters:
26
+ time_step: float, time per step (ps).
27
+ max_step: maxmimum steps.
28
+ thermostat: str, the thermostat of NVT ensemble.
29
+ thermostat_config: Dict|None, configs of thermostat. {'damping_coeff': float} for Langevin, {'time_const': float} for CSVR, {'virt_mass': float} for Nose-Hoover.
30
+ constr_func:
31
+ constr_val:
32
+ constr_threshold:
33
+ T_init: initial temperature, only to generate initial velocities of atoms by Maxwell-Boltzmann distribution. If V_init is given, T_init will be ignored.
34
+ output_structures_per_step: int, output structures per output_structures_per_step steps.
35
+ device: device that the program rum on.
36
+ verbose: control the detailed degree of output information. 0 for silence, 1 for output Energy and Forces per step, 2 for output all structures.
37
+
38
+ Methods:
39
+ run: run the NVT ensemble BatchMD.
40
+ """
41
+
42
+ def __init__(self,
43
+ time_step: float,
44
+ max_step: int,
45
+ thermostat: Literal['Langevin', 'VR', 'Nose-Hoover', 'CSVR'],
46
+ thermostat_config: Dict | None = None,
47
+ constr_func: Callable[[th.Tensor], th.Tensor] = None,
48
+ constr_val: Callable[[th.Tensor], th.Tensor | Tuple[th.Tensor]] | th.Tensor = None,
49
+ constr_threshold: float = 1e-5,
50
+ T_init: float = 298.15,
51
+ output_file: str | None = None,
52
+ dump_path: str | None = None,
53
+ output_structures_per_step: int = 1,
54
+ device: str | th.device = 'cpu',
55
+ verbose: int = 2) -> None:
56
+ """
57
+ Parameters:
58
+ time_step: float, time per step (ps).
59
+ max_step: maxmimum steps.
60
+ thermostat: str, the thermostat of NVT ensemble.
61
+ thermostat_config: Dict|None, configs of thermostat. {'damping_coeff': float} for Langevin, {'time_const': float} for CSVR, {'virt_mass': float} for Nose-Hoover.
62
+ T_init: initial temperature, only to generate initial velocities of atoms by Maxwell-Boltzmann distribution. If V_init is given, T_init will be ignored.
63
+ output_structures_per_step: int, output structures per output_structures_per_step steps.
64
+ device: device that program run on.
65
+ verbose: control the detailed degree of output information. 0 for silence, 1 for output Energy and Forces per step, 2 for output all structures.
66
+ """
67
+ super().__init__(
68
+ time_step,
69
+ max_step,
70
+ T_init,
71
+ constr_func,
72
+ constr_val,
73
+ constr_threshold,
74
+ output_file,
75
+ dump_path,
76
+ output_structures_per_step,
77
+ device,
78
+ verbose
79
+ )
80
+ __ENSEMBLES_DICT = {'Langevin': None, 'VR': None, 'Nose-Hoover': None, 'CSVR': None}
81
+ if thermostat not in {'Langevin', 'Langevin_old', 'test', 'VR', 'CSVR', 'Nose-Hoover'}: raise ValueError(f'Unknown Thermostat {thermostat}')
82
+ self.thermostat = thermostat
83
+ if thermostat_config is None:
84
+ thermostat_config = dict()
85
+ self.update_scheme = self.__resolve_update_scheme()
86
+ self.thermostat_config = thermostat_config
87
+
88
+ def initialize(
89
+ self,
90
+ func: Any | nn.Module,
91
+ X: th.Tensor,
92
+ Element_list: List[List[str]] | List[List[int]],
93
+ masses: th.Tensor,
94
+ V_init: th.Tensor | None = None,
95
+ grad_func: Any | nn.Module = None,
96
+ func_args: Sequence = tuple(),
97
+ func_kwargs: Dict | None = None,
98
+ grad_func_args: Sequence = tuple(),
99
+ grad_func_kwargs: Dict | None = None,
100
+ is_grad_func_contain_y: bool = True,
101
+ require_grad: bool = False,
102
+ batch_indices: List[int] | Tuple[int, ...] | th.Tensor | np.ndarray | None = None,
103
+ fixed_atom_tensor: Optional[th.Tensor] = None,
104
+ is_fix_mass_center: bool = False
105
+ ):
106
+ super().initialize(
107
+ func,
108
+ X,
109
+ Element_list,
110
+ masses,
111
+ V_init,
112
+ grad_func,
113
+ func_args,
114
+ func_kwargs,
115
+ grad_func_args,
116
+ grad_func_kwargs,
117
+ is_grad_func_contain_y=is_grad_func_contain_y,
118
+ require_grad=require_grad,
119
+ batch_indices=batch_indices,
120
+ fixed_atom_tensor=fixed_atom_tensor,
121
+ is_fix_mass_center=is_fix_mass_center
122
+ )
123
+ # recalculate E_vir
124
+ self.Ekt_vir = 0.5 * th.sum(masses * V_init ** 2, dim=(-2, -1)) * 103.642696562621738
125
+
126
+ def __resolve_update_scheme(self):
127
+ """
128
+ resolve different iteration scheme
129
+ Returns:
130
+
131
+ """
132
+ if self.thermostat == "Langevin":
133
+ damp_coeff = self.thermostat_config.get('damping_coeff', 0.01) # Unit: fs^-1
134
+ self.alpha = th.exp(- damp_coeff * self.time_step)
135
+ return self.__Langevin
136
+ elif self.thermostat == "VR":
137
+ return self.__VR
138
+ elif self.thermostat == "Nose-Hoover":
139
+ # read thermostat config
140
+ smass = self.thermostat_config.get('virt_mass', self.free_degree * 8.617333262145e-5 * self.T_init * 40 ** 2)
141
+ if isinstance(smass, float):
142
+ smass = th.as_tensor(smass, device=self.device).view(1, 1, 1)
143
+ self.smass = smass
144
+ return self.__NoseHoover
145
+ elif self.thermostat == "CSVR":
146
+ # read thermostat configs
147
+ self.time_const = self.thermostat_config.get('time_const', 10 * self.time_step) # Unit: fs^-1
148
+ return self.__CSVR
149
+ else:
150
+ raise NotImplementedError("Unknown Thermostat Type.")
151
+
152
+ def __Langevin(
153
+ self,
154
+ X,
155
+ V,
156
+ Force,
157
+ func,
158
+ grad_func_,
159
+ func_args,
160
+ func_kwargs,
161
+ grad_func_args,
162
+ grad_func_kwargs,
163
+ masses,
164
+ atom_masks,
165
+ is_grad_func_contain_y,
166
+ )-> Tuple[th.Tensor, th.Tensor, th.Tensor, th.Tensor]:
167
+ # read thermostat configs
168
+ X = X.detach()
169
+ with th.no_grad():
170
+ alpha = self.alpha
171
+ # half-step
172
+ V.addcdiv_(Force, masses, value=0.5 * self.time_step * 9.64853329045427e-3)
173
+ self._project2(X)
174
+ # stochastic update velocity
175
+ V.mul_(alpha)
176
+ V.add_(th.sqrt((8314.462618 * self.T_init * (1 - alpha ** 2)) / masses) * 1e-5 * th.randn_like(V))
177
+ # V = alpha * V + th.sqrt((8314.462618 * self.T_init * (1 - alpha ** 2)) / masses) * 1e-5 * th.randn_like(V)
178
+ # the rest half-step
179
+ X.add_(V, alpha=0.5 * self.time_step)
180
+ V.addcdiv_(Force, masses, value=0.5 * self.time_step * 9.64853329045427e-3)
181
+ # retrack & proj
182
+ X.add_(V, alpha=0.5 * self.time_step)
183
+ V.copy_(self._project1(V))
184
+ # update energy & forces
185
+ with th.set_grad_enabled(self.require_grad):
186
+ X.requires_grad_(self.require_grad)
187
+ Energy = func(X, *func_args, **func_kwargs)
188
+ if is_grad_func_contain_y:
189
+ Force = - grad_func_(X, Energy, *grad_func_args, **grad_func_kwargs) * atom_masks
190
+ else:
191
+ Force = - grad_func_(X, *grad_func_args, **grad_func_kwargs) * atom_masks
192
+
193
+ return X, V, Energy, Force
194
+
195
+ def __VR(
196
+ self,
197
+ X,
198
+ V,
199
+ Force,
200
+ func,
201
+ grad_func_,
202
+ func_args,
203
+ func_kwargs,
204
+ grad_func_args,
205
+ grad_func_kwargs,
206
+ masses,
207
+ atom_masks,
208
+ is_grad_func_contain_y,
209
+ batch_indices,
210
+ )-> Tuple[th.Tensor, th.Tensor, th.Tensor, th.Tensor]:
211
+ # NVE Step
212
+ X = X.detach()
213
+ with th.no_grad():
214
+ # X = X + V * self.time_step + (Force / (2. * masses)) * self.time_step ** 2 * 9.64853329045427e-3
215
+ V.addcdiv_(Force, masses, value=0.5 * self.time_step * 9.64853329045427e-3)
216
+ X.add_(V, alpha=self.time_step)
217
+ Fc = self._project2(X)
218
+ if self.verbose > 0:
219
+ self.logger.info(f'Constraint forces \\lambda: {np.array2string(Fc.squeeze().numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}')
220
+ # V = V + (Force / (2. * masses)) * self.time_step * 9.64853329045427e-3 # half-step veloc. update, to avoid saving 2 Forces Tensors.
221
+ # Update V
222
+ with th.set_grad_enabled(self.require_grad):
223
+ X.requires_grad_(self.require_grad)
224
+ Energy = func(X, *func_args, **func_kwargs)
225
+ if is_grad_func_contain_y:
226
+ Force = - grad_func_(X, Energy, *grad_func_args, **grad_func_kwargs) * atom_masks
227
+ else:
228
+ Force = - grad_func_(X, *grad_func_args, **grad_func_kwargs) * atom_masks
229
+
230
+ # V = V + (Force / (2. * masses)) * self.time_step * 9.64853329045427e-3
231
+ V.addcdiv_(Force, masses, value=0.5 * self.time_step * 9.64853329045427e-3)
232
+ V.copy_(self._project1(V))
233
+ if batch_indices is not None:
234
+ # Rescaling factor
235
+ alpha = th.sqrt(self.EK_TARGET / self.Ek).unsqueeze(-1).unsqueeze(-1) # (n_batch, 1, 1) | (irregular n_batch, 1, 1)
236
+ V *= alpha.transpose(0, 1)[:, self.batch_scatter, :]
237
+ else:
238
+ # Rescaling factor
239
+ alpha = th.sqrt(self.EK_TARGET / self.Ek).unsqueeze(-1).unsqueeze(-1) # (n_batch, 1, 1) | (irregular n_batch, 1, 1)
240
+ V *= alpha # (n_batch, n_atom, n_dim) * (n_batch, 1, 1)
241
+
242
+ return X, V, Energy, Force
243
+
244
+ def __CSVR(
245
+ self,
246
+ X: th.Tensor,
247
+ V: th.Tensor,
248
+ Force,
249
+ func,
250
+ grad_func_,
251
+ func_args,
252
+ func_kwargs,
253
+ grad_func_args,
254
+ grad_func_kwargs,
255
+ masses,
256
+ atom_masks,
257
+ is_grad_func_contain_y,
258
+ batch_indices
259
+ ) -> Tuple[th.Tensor, th.Tensor, th.Tensor, th.Tensor]:
260
+ """
261
+ The Analytic solution of CSVR that uses Chi^2 distribution and exp.
262
+ Returns:
263
+
264
+ """
265
+ n_batch, n_atom, n_dim = X.shape
266
+ # read thermostat configs
267
+ time_const = self.time_const
268
+ # NVE Step
269
+ dtT = self.time_step
270
+ tauT = time_const
271
+
272
+ # c = exp(-dt/tau) (scalar)
273
+ c = th.exp(th.as_tensor(-dtT / tauT, device=self.device, dtype=V.dtype))
274
+ sqrt_c = th.sqrt(c)
275
+
276
+ # avoid divide-by-zero in K
277
+ epsK = th.as_tensor(1e-12, device=self.device, dtype=V.dtype)
278
+
279
+ with th.no_grad():
280
+ # X = X + V * self.time_step + (Force / (2. * masses)) * self.time_step ** 2 * 9.64853329045427e-3
281
+ V.addcdiv_(Force, masses, value=0.5 * self.time_step * 9.64853329045427e-3)
282
+ X.add_(V, alpha=self.time_step)
283
+ Fc = self._project2(X)
284
+ if self.verbose > 0:
285
+ self.logger.info(f'Constraint forces \\lambda: {np.array2string(Fc.squeeze().numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}')
286
+ # V = V + (Force / (2. * masses)) * self.time_step * 9.64853329045427e-3 # half-step veloc. update, to avoid saving 2 Forces Tensors.
287
+ # Update V
288
+ with th.set_grad_enabled(self.require_grad):
289
+ X.requires_grad_(self.require_grad)
290
+ Energy = func(X, *func_args, **func_kwargs)
291
+ if is_grad_func_contain_y:
292
+ Force = - grad_func_(X, Energy, *grad_func_args, **grad_func_kwargs) * atom_masks
293
+ else:
294
+ Force = - grad_func_(X, *grad_func_args, **grad_func_kwargs) * atom_masks
295
+
296
+ # V = V + (Force / (2. * masses)) * self.time_step * 9.64853329045427e-3
297
+ V.addcdiv_(Force, masses, value=0.5 * self.time_step * 9.64853329045427e-3)
298
+ V.copy_(self._project1(V))
299
+
300
+ if batch_indices is not None:
301
+ # Nf per (irregular) configuration; keep your convention (3*N-3)
302
+ Nf = self.free_degree # shape (n_batch,)
303
+ K = th.clamp(self.Ek, min=epsK) # shape (n_batch,)
304
+ K0 = self.EK_TARGET # scalar or shape-compatible
305
+
306
+ # f = (1-c) * K0 / (Nf*K)
307
+ f = (1.0 - c) * K0 / (Nf * K)
308
+
309
+ # R ~ N(0,1)
310
+ R = th.randn_like(K)
311
+
312
+ # S ~ Chi2(df=Nf-1)
313
+ df = th.clamp(Nf - 1.0, min=1.0)
314
+ S = th.distributions.chi2.Chi2(df=df).sample() # shape (n_batch,)
315
+
316
+ # alpha^2 = (sqrt(c)+sqrt(f)R)^2 + f S
317
+ sqrt_f = th.sqrt(th.clamp(f, min=0.0))
318
+ alpha2 = (sqrt_c + sqrt_f * R) ** 2 + f * S
319
+ alpha2 = th.clamp(alpha2, min=epsK)
320
+
321
+ # (optional bookkeeping: post-thermostat kinetic energy)
322
+ self.Ekt_vir = alpha2 * K
323
+
324
+ alpha = th.sqrt(alpha2).unsqueeze(-1).unsqueeze(-1) # (n_batch, 1, 1)
325
+ V *= alpha.transpose(0, 1)[:, self.batch_scatter, :]
326
+
327
+ else:
328
+ # Nf is same for all batches; keep your convention (3*N-3)
329
+ Nf = th.as_tensor(3 * n_atom - 3, device=self.device, dtype=V.dtype) # scalar tensor
330
+ K = th.clamp(self.Ek, min=epsK) # (n_batch,)
331
+ K0 = self.EK_TARGET # scalar or (n_batch,)
332
+
333
+ f = (1.0 - c) * K0 / (Nf * K)
334
+
335
+ R = th.randn_like(K)
336
+ df = th.clamp(Nf - 1.0, min=1.0)
337
+ # sample per-batch
338
+ S = th.distributions.chi2.Chi2(df=df).sample(sample_shape=K.shape)
339
+
340
+ sqrt_f = th.sqrt(th.clamp(f, min=0.0))
341
+ alpha2 = (sqrt_c + sqrt_f * R) ** 2 + f * S
342
+ alpha2 = th.clamp(alpha2, min=epsK)
343
+
344
+ self.Ekt_vir = alpha2 * K
345
+
346
+ alpha = th.sqrt(alpha2).unsqueeze(-1).unsqueeze(-1) # (n_batch, 1, 1)
347
+ V *= alpha
348
+
349
+ return X, V, Energy, Force
350
+
351
+ def __NoseHoover(
352
+ self,
353
+ X,
354
+ V,
355
+ Force,
356
+ func,
357
+ grad_func_,
358
+ func_args,
359
+ func_kwargs,
360
+ grad_func_args,
361
+ grad_func_kwargs,
362
+ masses,
363
+ atom_masks,
364
+ is_grad_func_contain_y,
365
+ batch_indices
366
+ ) -> Tuple[th.Tensor, th.Tensor, th.Tensor, th.Tensor]:
367
+ n_batch, n_atom, n_dim = X.shape
368
+ smass = self.smass
369
+ if batch_indices is not None:
370
+ smass = smass.unsqueeze(0).expand((1, len(batch_indices)))
371
+ else:
372
+ smass = smass.unsqueeze(-1).expand(n_batch, 1)
373
+ # Main update
374
+ with th.no_grad():
375
+ if batch_indices is not None:
376
+ _iota = self.p_iota[:, self.batch_scatter, :]
377
+ else:
378
+ _iota = self.p_iota
379
+ V.addcdiv_(Force, masses, value=0.5 * self.time_step * 9.64853329045427e-3)
380
+ V.mul_(th.exp(- _iota * 0.5 * self.time_step))
381
+ X.add_(V, alpha=self.time_step)
382
+ Fc = self._project2(X)
383
+ if self.verbose > 0:
384
+ self.logger.info(f'Constraint forces \\lambda: {np.array2string(Fc.squeeze().numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}')
385
+
386
+ with th.set_grad_enabled(self.require_grad):
387
+ X.requires_grad_(self.require_grad)
388
+ Energy = func(X, *func_args, **func_kwargs)
389
+ if is_grad_func_contain_y:
390
+ Force = - grad_func_(X, Energy, *grad_func_args, **grad_func_kwargs) * atom_masks
391
+ else:
392
+ Force = - grad_func_(X, *grad_func_args, **grad_func_kwargs) * atom_masks
393
+
394
+ if batch_indices is not None: # for cuda, it would be further optimized by Graph.replay
395
+ reduced_Ek = th.sum(index_reduce(masses * V ** 2 * 103.642696562621738, self.batch_scatter, 1), dim=-1, keepdim=True)
396
+ # self.p_iota = p_iota + 0.5 * dt * (reducedEk - Nf * T)/smass
397
+ self.p_iota.addcdiv_(
398
+ th.sub(reduced_Ek, self.free_degree, alpha=self.T_init * 8.617333262145e-5),
399
+ smass, value=self.time_step * 0.5
400
+ )
401
+ _iota = self.p_iota[:, self.batch_scatter, :] # (1, n_batch*n_atom, 1)
402
+ V.addcdiv_(Force, masses, value=0.5 * self.time_step * 9.64853329045427e-3)
403
+ V.mul_(th.exp(- _iota * 0.5 * self.time_step))
404
+ reduced_Ek = th.sum(index_reduce(masses * V ** 2 * 103.642696562621738, self.batch_scatter, 1), dim=-1, keepdim=True)
405
+ # self.p_iota = p_iota + 0.5 * dt * (reducedEk - Nf * T)/smass
406
+ self.p_iota.addcdiv_(
407
+ th.sub(reduced_Ek, self.free_degree, alpha=self.T_init * 8.617333262145e-5),
408
+ smass, value=self.time_step * 0.5
409
+ )
410
+ else:
411
+ reduced_Ek = th.sum(masses * V ** 2 * 103.642696562621738, dim=(-2, -1), keepdim=True)
412
+ # self.p_iota = p_iota + 0.5 * dt * (reducedEk - Nf * T)/smass
413
+ self.p_iota.addcdiv_(
414
+ th.sub(reduced_Ek, self.free_degree, alpha=self.T_init * 8.617333262145e-5),
415
+ smass, value=self.time_step * 0.5
416
+ )
417
+ V.addcdiv_(Force, masses, value=0.5 * self.time_step * 9.64853329045427e-3)
418
+ V.mul_(th.exp(- _iota * 0.5 * self.time_step))
419
+ reduced_Ek = th.sum(masses * V ** 2 * 103.642696562621738, dim=(-2, -1), keepdim=True)
420
+ # self.p_iota = p_iota + 0.5 * dt * (reducedEk - Nf * T)/smass
421
+ self.p_iota.addcdiv_(
422
+ th.sub(reduced_Ek, self.free_degree, alpha=self.T_init * 8.617333262145e-5),
423
+ smass, value=self.time_step * 0.5
424
+ )
425
+ V.copy_(self._project1(V))
426
+
427
+ return X, V, Energy, Force
428
+
429
+
430
+ def _updateXV(
431
+ self,
432
+ X,
433
+ V,
434
+ Force,
435
+ func,
436
+ grad_func_,
437
+ func_args,
438
+ func_kwargs,
439
+ grad_func_args,
440
+ grad_func_kwargs,
441
+ masses,
442
+ atom_masks,
443
+ is_grad_func_contain_y,
444
+ batch_indices,
445
+ ) -> Tuple[th.Tensor, th.Tensor, th.Tensor, th.Tensor]:
446
+
447
+ x, v, energy, forces = self.update_scheme(
448
+ X,
449
+ V,
450
+ Force,
451
+ func,
452
+ grad_func_,
453
+ func_args,
454
+ func_kwargs,
455
+ grad_func_args,
456
+ grad_func_kwargs,
457
+ masses,
458
+ atom_masks,
459
+ is_grad_func_contain_y,
460
+ )
461
+
462
+ return x, v, energy, forces
@@ -0,0 +1,78 @@
1
+ """ Micro canonical ensemble (NVE) Molecular Dynamics via Verlet algo. """
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
+
14
+ from ._BaseMD import _rBaseMD
15
+
16
+
17
+ class NVE(_rBaseMD):
18
+ """
19
+ Micro canonical ensemble (NVE) molecular dynamics implemented via velocity Verlet algo.
20
+
21
+ Parameters:
22
+ time_step: float, time per step (ps).
23
+ max_step: int, maximum steps.
24
+ 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.
25
+ output_structures_per_step: int, output structures per output_structures_per_step steps.
26
+ device: str|torch.device, device that program rum on.
27
+ 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.
28
+
29
+ Methods:
30
+ run: run BatchMD.
31
+
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ time_step: float,
37
+ max_step: int,
38
+ T_init: float = 298.15,
39
+ output_file: str | None = None,
40
+ output_structures_per_step: int = 1,
41
+ device: str | th.device = 'cpu',
42
+ verbose: int = 2
43
+ ):
44
+ super().__init__(
45
+ time_step,
46
+ max_step,
47
+ T_init,
48
+ output_file,
49
+ output_structures_per_step,
50
+ device,
51
+ verbose
52
+ )
53
+
54
+ def _updateXV(
55
+ self, X, V, Force,
56
+ func, grad_func_, func_args, func_kwargs, grad_func_args, grad_func_kwargs,
57
+ masses, atom_masks, is_grad_func_contain_y,
58
+ ) -> (th.Tensor, th.Tensor, th.Tensor):
59
+ """ Update X, V, Force, and return X, V, Energy, Force. """
60
+ X: th.Tensor = X.detach()
61
+ with th.no_grad():
62
+ # V = V + (Force / (2. * masses)) * self.time_step * 9.64853329045427e-3 # half-step veloc. update, to avoid saving 2 Forces Tensors.
63
+ V.add_(Force / (2. * masses), alpha=self.time_step * 9.64853329045427e-3)
64
+ # X = X + V * self.time_step + (Force / (2. * masses)) * self.time_step ** 2 * 9.64853329045427e-3
65
+ X.add_(V, alpha=self.time_step)
66
+ # Update F
67
+ with th.set_grad_enabled(self.require_grad):
68
+ X.requires_grad_(self.require_grad)
69
+ Energy = func(X, *func_args, **func_kwargs)
70
+ if is_grad_func_contain_y:
71
+ Force = - grad_func_(X, Energy, *grad_func_args, **grad_func_kwargs) * atom_masks
72
+ else:
73
+ Force = - grad_func_(X, *grad_func_args, **grad_func_kwargs) * atom_masks
74
+
75
+ # V = V + (Force / (2. * masses)) * self.time_step * 9.64853329045427e-3
76
+ V.add_(Force / (2. * masses), alpha=self.time_step * 9.64853329045427e-3)
77
+
78
+ return X, V, Energy, Force