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,1251 @@
1
+ """ Molecular Dynamics via Verlet algo. """
2
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
3
+ # Authors: Pu Pengxin, Song Xin
4
+ # Version: 0.9a
5
+ # File: _BaseMD.py
6
+ # Environment: Python 3.12
7
+
8
+ import sys
9
+ from typing import Iterable, Dict, Any, List, Literal, Optional, Callable, Sequence, Tuple # noqa: F401
10
+ import time
11
+ import math
12
+ import logging
13
+ import warnings
14
+ import os
15
+ import threading, queue
16
+
17
+ import torch as th
18
+ from torch import nn
19
+
20
+ import numpy as np
21
+
22
+ from BUCToolkit.utils._Element_info import MASS, N_MASS, ATOMIC_NUMBER, ATOMIC_SYMBOL
23
+ from BUCToolkit.utils._print_formatter import FLOAT_ARRAY_FORMAT, SCIENTIFIC_ARRAY_FORMAT
24
+ from BUCToolkit.utils.index_ops import index_reduce
25
+ from BUCToolkit.utils.function_utils import preload_func
26
+ from BUCToolkit.BatchStructures.StructuresIO import structures_io_dumper
27
+ from BUCToolkit.utils.setup_loggers import has_any_handler, clear_all_handlers, BaseIO
28
+
29
+
30
+ class _BaseMD(BaseIO):
31
+ """ Base BatchMD """
32
+
33
+ __slots__ = [
34
+ 'time_step', 'time_now',
35
+ 'verbose', 'logger',
36
+ 'batch_tensor', 'batch_scatter',
37
+ 'free_degree',
38
+ 'require_grad',
39
+ 'EK_TARGET',
40
+ 'Ekt_vir',
41
+ 'Ek',
42
+ 'p_iota',
43
+ #'__dict__'
44
+ ]
45
+
46
+ def __init__(
47
+ self,
48
+ time_step: float,
49
+ max_step: int,
50
+ T_init: float = 298.15,
51
+ output_file: str | None = None,
52
+ output_structures_per_step: int = 1,
53
+ device: str | th.device = 'cpu',
54
+ verbose: int = 0,
55
+ is_compile: bool = False,
56
+ compile_kwargs: dict | None = None,
57
+ ):
58
+ """
59
+ Parameters:
60
+ time_step: float, time per step (fs).
61
+ max_step: maximum steps.
62
+ T_init: initial temperature, only to generate initial velocities of atoms by Maxwell-Boltzmann distribution.
63
+ If V_init is given, T_init will be ignored.
64
+ output_file: the path to the binary file that stores trajectories.
65
+ output_structures_per_step: int, output structures per output_structures_per_step steps.
66
+ device: device that program run on.
67
+ verbose: control the detailed degree of output text information.
68
+ 0 for silence, 1 for output Energy and Forces per step, 2 for output all structures.
69
+ Note: verbose > 0 will be very slow, especially for computation on GPU.
70
+ is_compile: whether to use jit to compile integrator or not.
71
+ compile_kwargs: keyword arguments passed to compile. Only work when is_compile is True.
72
+ """
73
+ self.time_step = time_step
74
+ self.time_now = th.scalar_tensor(0., device=device) # the accumulated time
75
+ assert (max_step > 0) and isinstance(max_step, int), f'max_step must be a positive integer, but occurred {max_step}.'
76
+ self.max_step = int(max_step)
77
+ self.T_init = float(T_init)
78
+ self.output_structures_per_step = int(output_structures_per_step)
79
+ self.device = device
80
+ self.verbose = int(verbose)
81
+ self.is_compile = bool(is_compile)
82
+ self.compile_kwargs = compile_kwargs if compile_kwargs is not None else dict()
83
+
84
+ self.EK_TARGET = None # target kinetic energy under set temperature.
85
+ self.Ekt_vir = None # virtual kinetic energy for _CSVR_ thermostat.
86
+ self.Ek = None # kinetic energy at each timestep.
87
+ self.p_iota = None # thermostat var. for Nose-Hoover.
88
+
89
+ self.batch_tensor = None # tensor form of `batch_indices` if it was given.
90
+ self.batch_scatter = None # tensor indices form of `batch_indices` if it was given
91
+ # e.g., batch_indices = (3, 2, 1), thus self.scatter = tensor([0, 0, 0, 1, 1, 2])
92
+ self.free_degree = None # (n_batch, ), freedom degree tensor
93
+ self.require_grad = None
94
+
95
+ # An inner attr that lets the dumper do not close after `self.run`.
96
+ # It is used to contiguously run within a loop.
97
+ # Adv. API `MolecularDynamics` turns on it.
98
+ self._HOLD_DUMPER = False
99
+
100
+ # logging & dumper
101
+ super().__init__(output_file)
102
+ self.init_logger('Main.MD')
103
+
104
+ def _reduce_Ek_T(self, batch_indices, masses, V):
105
+ if batch_indices is not None:
106
+ Ek = th.sum(
107
+ 0.5 * index_reduce(
108
+ masses * V * V,
109
+ self.batch_scatter,
110
+ 1,
111
+ out_size=self.scatter_dim_out_size
112
+ ) * 103.642696562621738,
113
+ dim=-1
114
+ ).squeeze_(0) # (n_batch, ), eV/atom. Faraday constant F = 96485.3321233100184.
115
+ else:
116
+ Ek = 0.5 * th.sum(
117
+ masses * V * V,
118
+ dim=(-2, -1)
119
+ ) * 103.642696562621738 # (n_batch, ), eV/atom. Faraday constant F = 96485.3321233100184.
120
+ temperature = (2 * Ek) / ((self.free_degree + 1e-20) * 8.617333262145e-5) # Boltzmann constant kB = 8.617333262145e-5 eV/K
121
+
122
+ return Ek, temperature
123
+
124
+ def calc_mass_center(self, mass, mass_short, Xr, ) -> th.Tensor:
125
+ """
126
+ calculate mass center based on mass_short and Xr.
127
+ Args:
128
+ mass:
129
+ mass_short:
130
+ Xr:
131
+
132
+ Returns:
133
+
134
+ """
135
+ if self.batch_scatter is None:
136
+ # initialize topologie
137
+ mass_sum = th.sum(mass_short, dim=1, keepdim=True).unsqueeze(-2) # (n_batch, 1, 1)
138
+ mass_center = th.sum(Xr * mass, dim=1, keepdim=True) / mass_sum # (n_batch, 1, n_dim)
139
+ else:
140
+ # initialize topologie
141
+ mass_sum = index_reduce(mass_short, self.batch_scatter, dim=1, out_size=self.scatter_dim_out_size).unsqueeze(-1) # (1, n_batch, 1)
142
+ mass_center = index_reduce(Xr * mass, self.batch_scatter, dim=1, out_size=self.scatter_dim_out_size) / mass_sum # (1, n_batch, n_dim)
143
+ mass_center = mass_center.index_select(1, self.batch_scatter) # (1, sumN*A, n_dim)
144
+
145
+ return mass_center
146
+
147
+ def _do_async_dump(self, q: queue.Queue):
148
+ """
149
+ A backend thread to async. dump
150
+ Args:
151
+ q: queue to receive data. contains: tuple of (dumper, event, *data)
152
+
153
+ Returns: None
154
+
155
+ """
156
+ while True:
157
+ try:
158
+ dumper, event, _print_Ep, _print_X, _print_V, _print_F = q.get()
159
+ if dumper is None:
160
+ break
161
+ # event: th.cuda.Event, ensure copy done
162
+ event.synchronize()
163
+ dumper.step(
164
+ _print_Ep.numpy(),
165
+ _print_X.numpy(),
166
+ _print_V.numpy(),
167
+ _print_F.numpy(),
168
+ )
169
+ except Exception as e:
170
+ self.logger.error(f"Error: Failed to dump data due to \"{e}\"")
171
+
172
+ def _do_async_print(self, q: queue.Queue):
173
+ """
174
+
175
+ Args:
176
+ q:
177
+
178
+ Returns:
179
+
180
+ """
181
+ formatter1 = {'float': '{:> .2f}'.format}
182
+ formatter2 = {'float': '{:> 5.10f}'.format}
183
+ i = 0 # reinsurance for Exception part
184
+ # Note: PRINTING IS VERY EXPENSIVE !!!
185
+ while True:
186
+ try:
187
+ i, batch_indices, _print_Ep, _print_Ek, _print_temperature, _print_X, _print_V, _print_F = q.get()
188
+ if i is None:
189
+ break
190
+ if self.verbose > 0:
191
+ # print format
192
+ np.set_printoptions(
193
+ precision=8,
194
+ linewidth=1024,
195
+ floatmode='fixed',
196
+ suppress=True,
197
+ formatter=formatter1,
198
+ threshold=2000
199
+ )
200
+ self.logger.info(
201
+ f'Step: {i:>12d}\n\t'
202
+ f'T = {_print_temperature.numpy(force=True)}\n\t'
203
+ f'E_tol = {np.array2string((_print_Ek + _print_Ep).numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n\t'
204
+ f'Ek = {np.array2string(_print_Ek.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n\t'
205
+ f'Ep = {np.array2string(_print_Ep.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n\t'
206
+ # f'Time: {time.perf_counter() - t_step:>5.4f}'
207
+ )
208
+ # t_step = time.perf_counter()
209
+ if self.verbose > 1:
210
+ # split to print
211
+ if batch_indices is not None:
212
+ X_tup = th.split(_print_X, batch_indices, dim=1)
213
+ V_tup = th.split(_print_V, batch_indices, dim=1)
214
+ else:
215
+ X_tup = (_print_X,)
216
+ V_tup = (_print_V,)
217
+ np.set_printoptions(
218
+ precision=8,
219
+ floatmode='fixed',
220
+ suppress=True,
221
+ formatter=formatter2,
222
+ threshold=3000000
223
+ )
224
+ self.logger.info('_' * 100)
225
+ self.logger.info(f'Configuration {i}:')
226
+ for __x in X_tup:
227
+ X_str = np.array2string(
228
+ __x.numpy(force=True), **FLOAT_ARRAY_FORMAT
229
+ ).replace("[", " ").replace("]", " ")
230
+ self.logger.info(f'{X_str}\n')
231
+ del X_str, X_tup
232
+ if self.verbose > 2:
233
+ self.logger.info(f'Velocities {i}:')
234
+ for __x in V_tup:
235
+ V_str = np.array2string(
236
+ __x.numpy(force=True), **FLOAT_ARRAY_FORMAT
237
+ ).replace("[", " ").replace("]", " ")
238
+ self.logger.info(f'{V_str}\n')
239
+ del V_str
240
+ self.logger.info('_' * 100)
241
+ except Exception as e:
242
+ self.logger.error(f"Error: Failed to logout at {i}-th iteration due to \"{e}\".")
243
+
244
+ def _print_elem_info(self, Element_list, batch_indices):
245
+ # elem info
246
+ elem_list = list()
247
+ _element_list = list()
248
+ if batch_indices is not None:
249
+ indx_old = 0
250
+ for indx in batch_indices:
251
+ _element_list.append(Element_list[0][indx_old: indx_old + indx])
252
+ indx_old += indx
253
+ else:
254
+ _element_list = Element_list
255
+ for elements in _element_list:
256
+ __element_now = ''
257
+ __elem = ''
258
+ elem_info = ''
259
+ __elem_count = ''
260
+ for i, elem in enumerate(elements, 1):
261
+ # get element symbol
262
+ if isinstance(elem, int):
263
+ __elem = ATOMIC_NUMBER[elem]
264
+ else:
265
+ __elem = elem
266
+ # count element number
267
+ if __elem == __element_now:
268
+ __elem_count += 1
269
+ else:
270
+ elem_info = elem_info + str(__elem_count) + ' '
271
+ elem_info = elem_info + __elem + ': '
272
+ __elem_count = 1
273
+ __element_now = __elem
274
+ elem_info = elem_info + str(__elem_count)
275
+ elem_list.append(elem_info)
276
+ # log out
277
+ for i, ee in enumerate(elem_list):
278
+ self.logger.info(f'Structure {i:>5d}: {ee}')
279
+
280
+ @th.compiler.disable
281
+ def _calc_EF(
282
+ self,
283
+ X,
284
+ func,
285
+ func_args,
286
+ func_kwargs,
287
+ grad_func_,
288
+ grad_func_args,
289
+ grad_func_kwargs,
290
+ is_grad_func_contain_y,
291
+ ) -> Tuple[th.Tensor, th.Tensor]:
292
+ with th.set_grad_enabled(self.require_grad):
293
+ X.requires_grad_(self.require_grad)
294
+ Energy = func(X, *func_args, **func_kwargs)
295
+ if is_grad_func_contain_y:
296
+ Force = - grad_func_(X, Energy, *grad_func_args, **grad_func_kwargs)
297
+ else:
298
+ Force = - grad_func_(X, *grad_func_args, **grad_func_kwargs)
299
+ return Energy, Force
300
+
301
+ def initialize(
302
+ self,
303
+ func: Any | nn.Module,
304
+ X: th.Tensor,
305
+ Element_list: List[List[str]] | List[List[int]],
306
+ masses: th.Tensor,
307
+ V_init: th.Tensor | None = None,
308
+ grad_func: Any | nn.Module = None,
309
+ func_args: Sequence = tuple(),
310
+ func_kwargs: Dict | None = None,
311
+ grad_func_args: Sequence = tuple(),
312
+ grad_func_kwargs: Dict | None = None,
313
+ is_grad_func_contain_y: bool = True,
314
+ require_grad: bool = False,
315
+ batch_indices: List[int] | Tuple[int, ...] | th.Tensor | np.ndarray | None = None,
316
+ fixed_atom_tensor: Optional[th.Tensor] = None,
317
+ is_fix_mass_center: bool = False
318
+ ) -> None:
319
+ """
320
+ Do some possible initialization before entering main loop.
321
+ Default is doing nothing.
322
+ """
323
+ pass
324
+
325
+ def run(
326
+ self,
327
+ func: Any | nn.Module,
328
+ X: th.Tensor,
329
+ Element_list: List[List[str]] | List[List[int]],
330
+ Cell_vector: th.Tensor | None = None,
331
+ V_init: th.Tensor | None = None,
332
+ grad_func: Any | nn.Module = None,
333
+ func_args: Sequence = tuple(),
334
+ func_kwargs: Dict | None = None,
335
+ grad_func_args: Sequence = tuple(),
336
+ grad_func_kwargs: Dict | None = None,
337
+ is_grad_func_contain_y: bool = True,
338
+ require_grad: bool = False,
339
+ batch_indices: List[int] | Tuple[int, ...] | th.Tensor | np.ndarray | None = None,
340
+ fixed_atom_tensor: Optional[th.Tensor] = None,
341
+ move_to_center_freq: int = -1
342
+ ) -> None:
343
+ """
344
+ Parameters:
345
+ func: the main function of instantiated torch.nn.Module class.
346
+ X: Tensor[n_batch, n_atom, 3], the atom coordinates that input to func. If a 2D X was given, the first dimension would be set to 1.
347
+ Element_list: List[List[str | int]], the atomic type (element) corresponding to each row of each batch in X.
348
+ Cell_vector: Tensor[n_batch, 3, 3], the cell vectors. Only for logging out information, no really calculate. If not given, set all zeros.
349
+ V_init: the initial velocities of each atom. If None, a random velocity generated by Boltzmann distribution would be set.
350
+ grad_func: user-defined function that grad_func(X, ...) returns the func's gradient at X. if None, grad_func(X, ...) = th.autograd.grad(func(X, ...), X).
351
+ func_args: optional, other input of func.
352
+ func_kwargs: optional, other input of func.
353
+ grad_func_args: optional, other input of grad_func.
354
+ grad_func_kwargs: optional, other input of grad_func.
355
+ is_grad_func_contain_y: bool, if True, grad_func contains output of func followed by X i.e., grad = grad_func(X, y, ...), else grad = grad_func(X, ...)
356
+ require_grad: bool, if True, autograd will be turned on for func(X, *func_args, **func_kwargs) calculation.
357
+ batch_indices: the split points for given X, Element_list & V_init, must be 1D integer array_like.
358
+ the format of batch_indices is the same as `split_size_or_sections` in torch.split:
359
+ batch_indices = (n1, n2, ..., nN) will split X, Element_list & V_init into N parts, and ith parts has ni atoms. sum(n1, ..., nN) = X.shape[1]
360
+ fixed_atom_tensor: the indices of X that fixed.
361
+ move_to_center_freq: the period of translating coordinates and velocities of atoms into the mass center & 0.
362
+ if `move_to_center_freq` <= 0, the translation would not apply.
363
+
364
+ Returns: None
365
+
366
+ """
367
+ try:
368
+ if th.device(self.device).type == "cuda":
369
+ self.__run_on_cuda(
370
+ func,
371
+ X,
372
+ Element_list,
373
+ Cell_vector,
374
+ V_init,
375
+ grad_func,
376
+ func_args,
377
+ func_kwargs,
378
+ grad_func_args,
379
+ grad_func_kwargs,
380
+ is_grad_func_contain_y,
381
+ require_grad,
382
+ batch_indices,
383
+ fixed_atom_tensor,
384
+ move_to_center_freq
385
+ )
386
+ elif th.device(self.device).type == "cpu":
387
+ self.__run_on_cpu(
388
+ func,
389
+ X,
390
+ Element_list,
391
+ Cell_vector,
392
+ V_init,
393
+ grad_func,
394
+ func_args,
395
+ func_kwargs,
396
+ grad_func_args,
397
+ grad_func_kwargs,
398
+ is_grad_func_contain_y,
399
+ require_grad,
400
+ batch_indices,
401
+ fixed_atom_tensor,
402
+ move_to_center_freq
403
+ )
404
+ else:
405
+ raise NotImplementedError(F"device {self.device} not supported.")
406
+ finally:
407
+ pass
408
+
409
+ def __run_on_cuda(
410
+ self,
411
+ func: Any | nn.Module,
412
+ X: th.Tensor,
413
+ Element_list: List[List[str]] | List[List[int]],
414
+ Cell_vector: th.Tensor | None = None,
415
+ V_init: th.Tensor | None = None,
416
+ grad_func: Any | nn.Module = None,
417
+ func_args: Sequence = tuple(),
418
+ func_kwargs: Dict | None = None,
419
+ grad_func_args: Sequence = tuple(),
420
+ grad_func_kwargs: Dict | None = None,
421
+ is_grad_func_contain_y: bool = True,
422
+ require_grad: bool = False,
423
+ batch_indices: List[int] | Tuple[int, ...] | th.Tensor | np.ndarray | None = None,
424
+ fixed_atom_tensor: Optional[th.Tensor] = None,
425
+ move_to_center_freq: int = -1
426
+ ) -> None:
427
+ """
428
+ GPU version of BatchMD.run(...), including buffer, sync. D2H copy, etc.
429
+ """
430
+ if len(X.shape) == 2:
431
+ X = X.unsqueeze(0)
432
+ elif len(X.shape) != 3:
433
+ raise ValueError(f'`X` must be 2D or 3D, but got shape [{X.shape}]')
434
+ if not isinstance(move_to_center_freq, int):
435
+ raise TypeError(f'`move_to_center_freq` must be an integer, but got {type(move_to_center_freq)}.')
436
+ elif move_to_center_freq <= 0:
437
+ is_fix_mass_center = False
438
+ else:
439
+ is_fix_mass_center = True
440
+ n_batch, n_atom, n_dim = X.shape
441
+ if func_kwargs is None: func_kwargs = dict()
442
+ if grad_func_kwargs is None: grad_func_kwargs = dict()
443
+ # Check batch indices
444
+ if batch_indices is not None:
445
+ if n_batch != 1:
446
+ raise RuntimeError(f'If batch_indices was specified, the 1st dimension of X must be 1 instead of {n_batch}.')
447
+ if isinstance(batch_indices, (th.Tensor, np.ndarray)):
448
+ self.batch_tensor = batch_indices
449
+ batch_indices = batch_indices.tolist()
450
+ elif not isinstance(batch_indices, (List, Tuple)):
451
+ raise TypeError(f'Invalid type of batch_indices {type(batch_indices)}. '
452
+ f'It must be List[int] | Tuple[int, ...] | th.Tensor | np.ndarray | None')
453
+ for i in batch_indices: assert isinstance(i, int), f'All elements in batch_indices must be int, but occurred {type(i)}'
454
+ self.batch_tensor = th.as_tensor(batch_indices, device=self.device) # the tensor version of batch_indices which is a List.
455
+ self.batch_scatter = th.repeat_interleave(
456
+ th.arange(0, len(batch_indices), dtype=th.int64, device=self.device),
457
+ self.batch_tensor,
458
+ dim=0
459
+ ) # scatter mask of the int tensor with the same shape as X.shape[1], which the data in one batch have one index.
460
+ self.scatter_dim_out_size = self.batch_scatter.max().item() + 1
461
+ n_true_batch = len(batch_indices)
462
+ else:
463
+ n_true_batch = n_batch
464
+
465
+ # Manage Atomic Type & Masses
466
+ masses = list()
467
+ atomic_numbers = list()
468
+ for _Elem in Element_list:
469
+ if not isinstance(_Elem, list): raise TypeError(f'Expected `Element_list` of List[List[int|str]], but got List[{type(_Elem)}].')
470
+ atomic_numbers.append([ATOMIC_SYMBOL[__elem] if isinstance(__elem, str) else int(__elem) for __elem in _Elem])
471
+ masses.append([MASS[__elem] if isinstance(__elem, str) else N_MASS[__elem] for __elem in _Elem])
472
+ masses_short = th.tensor(masses, dtype=th.float32, device=self.device) # (n_batch, n_atom)
473
+ masses = masses_short.unsqueeze(-1).expand_as(X).contiguous() # (n_batch, n_atom, n_dim)
474
+ # grad_func
475
+ if grad_func is None:
476
+ is_grad_func_contain_y = True
477
+ def grad_func_(x, y, grad_shape=None):
478
+ if grad_shape is None:
479
+ grad_shape = th.ones_like(y)
480
+ g = th.autograd.grad(y, x, grad_shape)
481
+ return g[0]
482
+ else:
483
+ grad_func_ = grad_func
484
+ # Selective dynamics
485
+ if fixed_atom_tensor is None:
486
+ atom_masks = th.ones_like(X, device=self.device)
487
+ elif fixed_atom_tensor.shape == X.shape:
488
+ atom_masks = fixed_atom_tensor.to(self.device)
489
+ else:
490
+ raise RuntimeError(f'fixed_atom_tensor (shape: {fixed_atom_tensor.shape}) does not match X (shape: {X.shape}).')
491
+ # other check
492
+ if (not isinstance(self.max_step, int)) or (self.max_step <= 0):
493
+ raise ValueError(f'Invalid value of maxiter: {self.max_step}. It would be an integer greater than 0.')
494
+
495
+ # set variables device
496
+ func = preload_func(func, self.device)
497
+
498
+ if isinstance(grad_func_, nn.Module):
499
+ grad_func_ = grad_func_.to(self.device)
500
+ X = X.to(self.device)
501
+ # calc. freedom degree
502
+ if batch_indices is None:
503
+ _free_degree = X.shape[1] * n_dim
504
+ if is_fix_mass_center:
505
+ _free_degree -= 3
506
+ # initialize topologie
507
+ MASS_SUM = th.sum(masses_short, dim=1, keepdim=True).unsqueeze(-2) # (n_batch, 1, 1)
508
+ MASS_CENTER = th.sum(X * masses, dim=1, keepdim=True)/MASS_SUM # (n_batch, 1, n_dim)
509
+ self.free_degree = th.full((n_batch, ), _free_degree, dtype=th.int64, device=self.device)
510
+ n_reduce = th.where(th.abs(atom_masks) < 1e-6, 1, 0).sum(dim=(-2, -1)) # (n_batch, )
511
+ self.free_degree -= n_reduce
512
+ else:
513
+ self.free_degree = self.batch_tensor * n_dim # (n_batch, )
514
+ if is_fix_mass_center:
515
+ self.free_degree -= 3
516
+ # initialize topologie
517
+ MASS_SUM = index_reduce(masses_short, self.batch_scatter, dim=1, out_size=self.scatter_dim_out_size).unsqueeze(-1) # (1, n_batch, 1)
518
+ MASS_CENTER = index_reduce(X * masses, self.batch_scatter, dim=1, out_size=self.scatter_dim_out_size)/MASS_SUM # (1, n_batch, n_dim)
519
+ MASS_CENTER = MASS_CENTER.index_select(1, self.batch_scatter) # (1, sumN*A, n_dim)
520
+ n_reduce_tensor = th.where(th.abs(atom_masks) < 1e-6, 1, 0).sum(dim=-1)
521
+ n_reduce = index_reduce(n_reduce_tensor, self.batch_scatter, dim=1, out_size=self.scatter_dim_out_size).squeeze(0) # (n_batch, )
522
+ self.free_degree -= n_reduce
523
+ # target kinetic energy for NVT|NPT ensembles
524
+ self.EK_TARGET = (self.free_degree / 2.) * 8.617333262145e-5 * self.T_init
525
+ # Generate initial Velocities
526
+ if V_init is not None:
527
+ if V_init.shape != X.shape:
528
+ raise ValueError(f'V_init and X must have the same shape, but got {V_init.shape} and {X.shape}')
529
+ if self.verbose > 0: self.logger.info('Initial velocities was given, T_init will be ignored.')
530
+ V_init = V_init.to(self.device)
531
+ V = V_init.detach() * atom_masks
532
+ V = V.to(self.device)
533
+ else:
534
+ V = th.normal(
535
+ 0.,
536
+ th.sqrt(self.T_init * 8314.46261815324 / masses) * 1.e-5, # Unit: Ang/fs, R = kB * L = 8.31446261815324 J/(mol * K)
537
+ ) * atom_masks
538
+ # remove translation veloc.
539
+ if is_fix_mass_center:
540
+ V.sub_(self.calc_mass_center(masses, masses_short, V))
541
+ # split by batch_indices
542
+ if batch_indices is not None:
543
+ masses_tup = th.split(masses, batch_indices, dim=1)
544
+ V_tup = th.split(V, batch_indices, dim=1)
545
+ else:
546
+ masses_tup = (masses,)
547
+ V_tup = (V,)
548
+
549
+ # initialize thermostat parameters
550
+ # (n_batch, ), eV/atom. The initial virtual Ek_t for CSVR.
551
+ self.Ekt_vir = th.cat(
552
+ [0.5 * th.sum(_m * V_tup[_] ** 2, dim=(-2, -1)) * 103.642696562621738 for _, _m in enumerate(masses_tup)]
553
+ )
554
+ # The initial iota for Nose-Hoover
555
+ if batch_indices is not None:
556
+ self.p_iota = th.zeros(1, len(batch_indices), 1, device=self.device, dtype=th.float32)
557
+ else:
558
+ self.p_iota = th.zeros(n_batch, 1, 1, device=self.device, dtype=th.float32)
559
+ # whether grad needs autograd
560
+ self.require_grad = require_grad
561
+
562
+ # pre-allocate the tensors
563
+ # _buf_* is the vars on GPU that apply copy.
564
+ # _print_* is the vars on CPU that async. do D2H for _buf_*.
565
+ if batch_indices is not None:
566
+ _print_temperature = th.empty(len(batch_indices), device='cpu', dtype=th.float32, pin_memory=True)
567
+ _print_Ek = th.empty(len(batch_indices), device='cpu', dtype=th.float32, pin_memory=True)
568
+ _print_Ep = th.empty(len(batch_indices), device='cpu', dtype=th.float32, pin_memory=True)
569
+ _buf_Tp = th.empty(len(batch_indices), device=self.device, dtype=th.float32)
570
+ _buf_Ek = th.empty(len(batch_indices), device=self.device, dtype=th.float32)
571
+ _buf_Ep = th.empty(len(batch_indices), device=self.device, dtype=th.float32)
572
+ else:
573
+ _print_temperature = th.empty(n_batch, device='cpu', dtype=th.float32, pin_memory=True)
574
+ _print_Ek = th.empty(n_batch, device='cpu', dtype=th.float32, pin_memory=True)
575
+ _print_Ep = th.empty(n_batch, device='cpu', dtype=th.float32, pin_memory=True)
576
+ _buf_Tp = th.empty(n_batch, device=self.device, dtype=th.float32)
577
+ _buf_Ek = th.empty(n_batch, device=self.device, dtype=th.float32)
578
+ _buf_Ep = th.empty(n_batch, device=self.device, dtype=th.float32)
579
+ _print_X = th.empty_like(X, device='cpu', dtype=th.float32, pin_memory=True)
580
+ _print_V = th.empty_like(V, device='cpu', dtype=th.float32, pin_memory=True)
581
+ _print_F = th.empty_like(X, device='cpu', dtype=th.float32, pin_memory=True)
582
+ _buf_X = th.empty_like(X, device=self.device, dtype=th.float32)
583
+ _buf_V = th.empty_like(V, device=self.device, dtype=th.float32)
584
+ _buf_F = th.empty_like(X, device=self.device, dtype=th.float32)
585
+ # initialize the dumper
586
+ X_arr = X.numpy(force=True)
587
+ _x_dtype = X_arr.dtype.str
588
+ atom_masks_arr = atom_masks.numpy(force=True).astype(_x_dtype)
589
+ _num_dump = math.ceil(self.max_step/self.output_structures_per_step)
590
+ dumper = self.dumper
591
+ # write head information.
592
+ if Cell_vector is None:
593
+ Cell_vector = np.zeros((n_true_batch, 3, 3), dtype=np.float32)
594
+ elif isinstance(Cell_vector, th.Tensor):
595
+ Cell_vector = Cell_vector.numpy(force=True)
596
+ elif not isinstance(Cell_vector, np.ndarray):
597
+ Cell_vector = np.asarray(Cell_vector)
598
+ if self.batch_tensor is not None:
599
+ dumper.start_from_arrays(
600
+ 1,
601
+ self.batch_tensor.numpy(force=True), # batch indices
602
+ Cell_vector, # cell
603
+ np.asarray(atomic_numbers), # element type / atomic number
604
+ atom_masks_arr, # fixation mask
605
+ )
606
+ dumper.step(
607
+ self.batch_tensor.numpy(force=True),
608
+ Cell_vector,
609
+ np.asarray(atomic_numbers),
610
+ atom_masks_arr
611
+ )
612
+ else:
613
+ dumper.start_from_arrays(
614
+ 1,
615
+ Cell_vector,
616
+ np.asarray(atomic_numbers), # element type / atomic number
617
+ atom_masks_arr, # fixation mask
618
+ )
619
+ dumper.step(
620
+ Cell_vector,
621
+ np.asarray(atomic_numbers),
622
+ atom_masks_arr
623
+ )
624
+ # continue to write main data
625
+ dumper.start_from_arrays(
626
+ _num_dump,
627
+ _print_Ep.numpy(),
628
+ _print_X.numpy(),
629
+ _print_V.numpy(),
630
+ _print_F.numpy(),
631
+ )
632
+ # custom initialization
633
+ self.initialize(
634
+ func,
635
+ X,
636
+ Element_list,
637
+ masses,
638
+ V,
639
+ grad_func,
640
+ func_args,
641
+ func_kwargs,
642
+ grad_func_args,
643
+ grad_func_kwargs,
644
+ is_grad_func_contain_y,
645
+ require_grad,
646
+ batch_indices,
647
+ fixed_atom_tensor,
648
+ is_fix_mass_center
649
+ )
650
+
651
+ # print Atoms Information
652
+ # if has no handler, means the handler is upper level 'Main', thus not print repeatedly
653
+ if (self.verbose > 0) and len(self.logger.handlers) > 0:
654
+ self._print_elem_info(Element_list, batch_indices)
655
+
656
+ # MAIN Loop
657
+ with th.no_grad():
658
+ X = X.contiguous()
659
+ V = V.contiguous()
660
+ masses = masses.contiguous()
661
+ atom_masks = atom_masks.contiguous()
662
+ t_step = time.perf_counter()
663
+ t_main_loop = time.perf_counter()
664
+ with th.set_grad_enabled(require_grad):
665
+ X.requires_grad_(require_grad)
666
+ Energy = func(X, *func_args, **func_kwargs) # Note: func must return th.Tensor(n_batch, )
667
+ if batch_indices is not None:
668
+ if len(Energy) != len(batch_indices): # check batch number of output
669
+ raise RuntimeError(
670
+ f'batch number of `func` output ({len(Energy)}) does not match the input `batch_indices` ({len(batch_indices)}'
671
+ )
672
+
673
+ if is_grad_func_contain_y:
674
+ Forces = - grad_func_(X, Energy, *grad_func_args, **grad_func_kwargs) * atom_masks
675
+ else:
676
+ Forces = - grad_func_(X, *grad_func_args, **grad_func_kwargs) * atom_masks
677
+ Forces = Forces.contiguous()
678
+
679
+ Ek = th.zeros_like(Energy)
680
+ temperature = th.zeros_like(Energy)
681
+ # preload a graph of Ek, T
682
+ Ek_T_graph = th.cuda.CUDAGraph()
683
+ with th.cuda.graph(Ek_T_graph):
684
+ _e_tmp, _t_tmp = self._reduce_Ek_T(batch_indices, masses, V)
685
+ Ek.copy_(_e_tmp)
686
+ temperature.copy_(_t_tmp)
687
+ # preload a graph of mass center
688
+ if is_fix_mass_center:
689
+ mass_center_graph = th.cuda.CUDAGraph()
690
+ with th.cuda.graph(mass_center_graph):
691
+ _dX = MASS_CENTER - self.calc_mass_center(masses, masses_short, X)
692
+ _dV = - self.calc_mass_center(masses, masses_short, V)
693
+ X.add_(_dX) # (n_batch, n_atom, n_dim) - (n_batch, 1, n_dim)
694
+ V.add_(_dV)
695
+ else:
696
+ mass_center_graph = None
697
+ # preload Verlet update
698
+ #self.Verlet1_graph = th.cuda.CUDAGraph()
699
+ #with th.cuda.graph(self.Verlet1_graph):
700
+ # V.addcdiv_(Forces, masses, value=0.5 * self.time_step * 9.64853329045427e-3)
701
+ # X.add_(V, alpha=self.time_step)
702
+ #self.Verlet2_graph = th.cuda.CUDAGraph()
703
+ #with th.cuda.graph(self.Verlet2_graph):
704
+ # Forces.mul_(atom_masks)
705
+ # # V = V + (Force / (2. * masses)) * self.time_step * 9.64853329045427e-3
706
+ # V.addcdiv_(Forces, masses, value=0.5 * self.time_step * 9.64853329045427e-3)
707
+
708
+ copy_stream = th.cuda.Stream()
709
+ copy_event = th.cuda.Event()
710
+ compute_event = th.cuda.Event()
711
+ compute_event.record(th.cuda.default_stream(self.device)) # the default stream is the compute (main) stream.
712
+ # launch the dumping thread
713
+ dump_queue = queue.Queue()
714
+ dump_thread = threading.Thread(target=self._do_async_dump, args=(dump_queue, ), daemon=True)
715
+ logout_queue = queue.Queue()
716
+ logout_thread = threading.Thread(target=self._do_async_print, args=(logout_queue, ), daemon=True)
717
+ try:
718
+ dump_thread.start()
719
+ logout_thread.start()
720
+ #ptlist = list() # test <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
721
+ fl = th.compile(self._main_for_loop_cuda, **self.compile_kwargs, disable=(not self.is_compile))
722
+ fl(
723
+ Ek_T_graph,
724
+ Ek,
725
+ Energy,
726
+ X,
727
+ V,
728
+ Forces,
729
+ compute_event,
730
+ temperature,
731
+ copy_stream,
732
+ _buf_Tp,
733
+ _buf_Ek,
734
+ _buf_Ep,
735
+ _buf_X,
736
+ _buf_V,
737
+ _buf_F,
738
+ _print_temperature,
739
+ _print_Ek,
740
+ _print_Ep,
741
+ _print_X,
742
+ _print_V,
743
+ _print_F,
744
+ copy_event,
745
+ dump_queue,
746
+ dumper,
747
+ logout_queue,
748
+ func, grad_func_, func_args, func_kwargs, grad_func_args, grad_func_kwargs,
749
+ masses, atom_masks, is_grad_func_contain_y, batch_indices,
750
+ is_fix_mass_center,
751
+ move_to_center_freq,
752
+ mass_center_graph
753
+ )
754
+ if not self._HOLD_DUMPER:
755
+ dumper.close()
756
+ th.cuda.synchronize()
757
+ if self.verbose > 0:
758
+ self.logger.info(f'MAIN LOOP DONE. Elapsed time: {time.perf_counter() - t_main_loop:>5.4f} s')
759
+ finally:
760
+ dump_queue.put([None]*6)
761
+ dump_thread.join()
762
+ logout_queue.put([None]*8)
763
+ logout_thread.join()
764
+
765
+ del self.Ekt_vir
766
+ #return ptlist # test <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
767
+
768
+ def _main_for_loop_cuda(
769
+ self,
770
+ Ek_T_graph,
771
+ Ek,
772
+ Energy,
773
+ X,
774
+ V,
775
+ Forces,
776
+ compute_event,
777
+ temperature,
778
+ copy_stream,
779
+ _buf_Tp,
780
+ _buf_Ek,
781
+ _buf_Ep,
782
+ _buf_X,
783
+ _buf_V,
784
+ _buf_F,
785
+ _print_temperature,
786
+ _print_Ek,
787
+ _print_Ep,
788
+ _print_X,
789
+ _print_V,
790
+ _print_F,
791
+ copy_event,
792
+ dump_queue,
793
+ dumper,
794
+ logout_queue,
795
+ func, grad_func_, func_args, func_kwargs, grad_func_args, grad_func_kwargs,
796
+ masses, atom_masks, is_grad_func_contain_y, batch_indices,
797
+ is_fix_mass_center,
798
+ move_to_center_freq,
799
+ mass_center_graph
800
+ ):
801
+ #t_step = time.perf_counter()
802
+ _do_print = False
803
+ for i in range(self.max_step):
804
+ #with th.profiler.record_function('Calc. E, T <<<<<'):
805
+ Ek_T_graph.replay()
806
+ # Ek, temperature = self._reduce_Ek_T(batch_indices, masses, V)
807
+ self.Ek = Ek # th.sum(Ek, dim=0) # saving the real kinetic energy for VR & CSVR to avoid double counting.
808
+ # if self.verbose > 0:
809
+ #with th.profiler.record_function('D2H COPY <<<<<'):
810
+ if i % self.output_structures_per_step == 0:
811
+ _do_print = True
812
+ compute_event.wait(th.cuda.default_stream(self.device))
813
+ # D2D, fast copy purely on GPU
814
+ _buf_Tp.copy_(temperature.squeeze().contiguous())
815
+ _buf_Ek.copy_(Ek.squeeze().contiguous())
816
+ _buf_Ep.copy_(Energy.squeeze().contiguous())
817
+ _buf_X.copy_(X)
818
+ _buf_V.copy_(V)
819
+ _buf_F.copy_(Forces)
820
+ # D2H, async.
821
+ with th.cuda.stream(copy_stream):
822
+ copy_stream.wait_stream(th.cuda.default_stream(self.device))
823
+ _print_temperature.copy_(_buf_Tp, non_blocking=True)
824
+ _print_Ek.copy_(_buf_Ek, non_blocking=True)
825
+ _print_Ep.copy_(_buf_Ep, non_blocking=True)
826
+ _print_X.copy_(_buf_X, non_blocking=True) # D2H
827
+ _print_V.copy_(_buf_V, non_blocking=True)
828
+ _print_F.copy_(_buf_F, non_blocking=True)
829
+ # ptlist.append(X.numpy(force=True)) # test <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
830
+ copy_event.record(copy_stream)
831
+ # use backend thread to dump
832
+ dump_queue.put((dumper, copy_event, _print_Ep, _print_X, _print_V, _print_F))
833
+
834
+ # Update X, V
835
+ #with th.profiler.record_function('MAIN UPDATE <<<<<'):
836
+ X, V, Energy, Forces = self._updateXV(
837
+ X, V, Forces,
838
+ func, grad_func_, func_args, func_kwargs, grad_func_args, grad_func_kwargs,
839
+ masses, atom_masks, is_grad_func_contain_y, batch_indices
840
+ )
841
+ compute_event.record(th.cuda.default_stream(self.device)) # the default stream is the compute (main) stream.
842
+ # print
843
+ #with th.profiler.record_function('PRINT <<<<<'):
844
+ if _do_print:
845
+ logout_queue.put((i, batch_indices, _print_Ep, _print_Ek, _print_temperature, _print_X, _print_V, _print_F))
846
+ _do_print = False
847
+
848
+ # Correct barycentric transition
849
+ #with th.profiler.record_function('MOVE TO CENTER <<<<<'):
850
+ if is_fix_mass_center and (i % move_to_center_freq == 0):
851
+ mass_center_graph.replay()
852
+ self.time_now += self.time_step
853
+
854
+ def __run_on_cpu(
855
+ self,
856
+ func: Any | nn.Module,
857
+ X: th.Tensor,
858
+ Element_list: List[List[str]] | List[List[int]],
859
+ Cell_vector: th.Tensor | None = None,
860
+ V_init: th.Tensor | None = None,
861
+ grad_func: Any | nn.Module = None,
862
+ func_args: Sequence = tuple(),
863
+ func_kwargs: Dict | None = None,
864
+ grad_func_args: Sequence = tuple(),
865
+ grad_func_kwargs: Dict | None = None,
866
+ is_grad_func_contain_y: bool = True,
867
+ require_grad: bool = False,
868
+ batch_indices: List[int] | Tuple[int, ...] | th.Tensor | np.ndarray | None = None,
869
+ fixed_atom_tensor: Optional[th.Tensor] = None,
870
+ move_to_center_freq: int = -1
871
+ ) -> None:
872
+ """
873
+ Pure CPU version of BaseMD.run(...).
874
+ """
875
+ if len(X.shape) == 2:
876
+ X = X.unsqueeze(0)
877
+ elif len(X.shape) != 3:
878
+ raise ValueError(f'`X` must be 2D or 3D, but got shape [{X.shape}]')
879
+ if not isinstance(move_to_center_freq, int):
880
+ raise TypeError(f'`move_to_center_freq` must be an integer, but got {type(move_to_center_freq)}.')
881
+ elif move_to_center_freq <= 0:
882
+ is_fix_mass_center = False
883
+ else:
884
+ is_fix_mass_center = True
885
+ n_batch, n_atom, n_dim = X.shape
886
+ if func_kwargs is None: func_kwargs = dict()
887
+ if grad_func_kwargs is None: grad_func_kwargs = dict()
888
+ # Check batch indices
889
+ if batch_indices is not None:
890
+ if n_batch != 1:
891
+ raise RuntimeError(f'If batch_indices was specified, the 1st dimension of X must be 1 instead of {n_batch}.')
892
+ if isinstance(batch_indices, (th.Tensor, np.ndarray)):
893
+ self.batch_tensor = batch_indices
894
+ batch_indices = batch_indices.tolist()
895
+ elif not isinstance(batch_indices, (List, Tuple)):
896
+ raise TypeError(f'Invalid type of batch_indices {type(batch_indices)}. '
897
+ f'It must be List[int] | Tuple[int, ...] | th.Tensor | np.ndarray | None')
898
+ for i in batch_indices: assert isinstance(i, int), f'All elements in batch_indices must be int, but occurred {type(i)}'
899
+ self.batch_tensor = th.as_tensor(batch_indices, device=self.device) # the tensor version of batch_indices which is a List.
900
+ self.batch_scatter = th.repeat_interleave(
901
+ th.arange(0, len(batch_indices), dtype=th.int64, device=self.device),
902
+ self.batch_tensor,
903
+ dim=0
904
+ ) # scatter mask of the int tensor with the same shape as X.shape[1], which the data in one batch have one index.
905
+ self.scatter_dim_out_size = self.batch_scatter.max().item() + 1
906
+ n_true_batch = len(batch_indices)
907
+ else:
908
+ n_true_batch = n_batch
909
+
910
+ # Manage Atomic Type & Masses
911
+ masses = list()
912
+ atomic_numbers = list()
913
+ for _Elem in Element_list:
914
+ if not isinstance(_Elem, list): raise TypeError(f'Expected `Element_list` of List[List[int|str]], but got List[{type(_Elem)}].')
915
+ atomic_numbers.append([ATOMIC_SYMBOL[__elem] if isinstance(__elem, str) else ATOMIC_NUMBER[__elem] for __elem in _Elem])
916
+ masses.append([MASS[__elem] if isinstance(__elem, str) else N_MASS[__elem] for __elem in _Elem])
917
+ masses_short = th.tensor(masses, dtype=th.float32, device=self.device) # (n_batch, n_atom)
918
+ masses = masses_short.unsqueeze(-1).expand_as(X).contiguous() # (n_batch, n_atom, n_dim)
919
+ # grad_func
920
+ if grad_func is None:
921
+ is_grad_func_contain_y = True
922
+ def grad_func_(x, y, grad_shape=None):
923
+ if grad_shape is None:
924
+ grad_shape = th.ones_like(y)
925
+ g = th.autograd.grad(y, x, grad_shape)
926
+ return g[0]
927
+ else:
928
+ grad_func_ = grad_func
929
+ # Selective dynamics
930
+ if fixed_atom_tensor is None:
931
+ atom_masks = th.ones_like(X, device=self.device)
932
+ elif fixed_atom_tensor.shape == X.shape:
933
+ atom_masks = fixed_atom_tensor.to(self.device)
934
+ else:
935
+ raise RuntimeError(f'fixed_atom_tensor (shape: {fixed_atom_tensor.shape}) does not match X (shape: {X.shape}).')
936
+ # other check
937
+ if (not isinstance(self.max_step, int)) or (self.max_step <= 0):
938
+ raise ValueError(f'Invalid value of maxiter: {self.max_step}. It would be an integer greater than 0.')
939
+
940
+ # set variables device
941
+ func = preload_func(func, self.device)
942
+
943
+ if isinstance(grad_func_, nn.Module):
944
+ grad_func_ = grad_func_.to(self.device)
945
+ X = X.to(self.device)
946
+ # calc. freedom degree
947
+ if batch_indices is None:
948
+ _free_degree = X.shape[1] * n_dim
949
+ if is_fix_mass_center:
950
+ _free_degree -= 3
951
+ # initialize topologie
952
+ MASS_SUM = th.sum(masses_short, dim=1, keepdim=True).unsqueeze(-2) # (n_batch, 1, 1)
953
+ MASS_CENTER = th.sum(X * masses, dim=1, keepdim=True)/MASS_SUM # (n_batch, 1, n_dim)
954
+ else:
955
+ MASS_CENTER = None
956
+ self.free_degree = th.full((n_batch, ), _free_degree, dtype=th.int64, device=self.device)
957
+ n_reduce = th.where(th.abs(atom_masks) < 1e-6, 1, 0).sum(dim=(-2, -1)) # (n_batch, )
958
+ self.free_degree -= n_reduce
959
+ else:
960
+ self.free_degree = self.batch_tensor * n_dim # (n_batch, )
961
+ if is_fix_mass_center:
962
+ self.free_degree -= 3
963
+ # initialize topologie
964
+ MASS_SUM = index_reduce(masses_short, self.batch_scatter, dim=1, out_size=self.scatter_dim_out_size).unsqueeze(-1) # (1, n_batch, 1)
965
+ MASS_CENTER = index_reduce(X * masses, self.batch_scatter, dim=1, out_size=self.scatter_dim_out_size)/MASS_SUM # (1, n_batch, n_dim)
966
+ MASS_CENTER = MASS_CENTER.index_select(1, self.batch_scatter) # (1, sumN*A, n_dim)
967
+ else:
968
+ MASS_CENTER = None
969
+ n_reduce_tensor = th.where(th.abs(atom_masks) < 1e-6, 1, 0).sum(dim=-1)
970
+ n_reduce = index_reduce(n_reduce_tensor, self.batch_scatter, dim=1, out_size=self.scatter_dim_out_size).squeeze(0) # (n_batch, )
971
+ self.free_degree -= n_reduce
972
+ # target kinetic energy for NVT|NPT ensembles
973
+ self.EK_TARGET = (self.free_degree / 2.) * 8.617333262145e-5 * self.T_init
974
+ # Generate initial Velocities
975
+ if V_init is not None:
976
+ if V_init.shape != X.shape:
977
+ raise ValueError(f'V_init and X must have the same shape, but got {V_init.shape} and {X.shape}')
978
+ if self.verbose > 0: self.logger.info('Initial velocities was given, T_init will be ignored.')
979
+ V_init = V_init.to(self.device)
980
+ V = V_init.detach() * atom_masks
981
+ V = V.to(self.device)
982
+ else:
983
+ V = th.normal(
984
+ 0.,
985
+ th.sqrt(self.T_init * 8314.46261815324 / masses) * 1.e-5, # Unit: Ang/fs, R = kB * L = 8.31446261815324 J/(mol * K)
986
+ ) * atom_masks
987
+ # remove translation veloc.
988
+ if is_fix_mass_center:
989
+ V.sub_(self.calc_mass_center(masses, masses_short, V))
990
+ # split by batch_indices
991
+ if batch_indices is not None:
992
+ masses_tup = th.split(masses, batch_indices, dim=1)
993
+ V_tup = th.split(V, batch_indices, dim=1)
994
+ else:
995
+ masses_tup = (masses,)
996
+ V_tup = (V,)
997
+
998
+ # initialize thermostat parameters
999
+ # (n_batch, ), eV/atom. The initial virtual Ek_t for CSVR.
1000
+ self.Ekt_vir = th.cat(
1001
+ [0.5 * th.sum(_m * V_tup[_] ** 2, dim=(-2, -1)) * 103.642696562621738 for _, _m in enumerate(masses_tup)]
1002
+ )
1003
+ # The initial iota for Nose-Hoover
1004
+ if batch_indices is not None:
1005
+ self.p_iota = th.zeros(1, len(batch_indices), 1, device=self.device, dtype=th.float32)
1006
+ else:
1007
+ self.p_iota = th.zeros(n_batch, 1, 1, device=self.device, dtype=th.float32)
1008
+ # whether grad needs autograd
1009
+ self.require_grad = require_grad
1010
+
1011
+ # initialize the dumper
1012
+ X_arr = X.numpy(force=True)
1013
+ _x_dtype = X_arr.dtype.str
1014
+ atom_masks_arr = atom_masks.numpy(force=True).astype(_x_dtype)
1015
+ # Note: cache_size: NOW it be hard coded as 4 MB
1016
+ _num_dump = math.ceil(self.max_step/self.output_structures_per_step)
1017
+ dumper = self.dumper
1018
+ if Cell_vector is None:
1019
+ Cell_vector = np.zeros((n_true_batch, 3, 3), dtype=np.float32)
1020
+ elif isinstance(Cell_vector, th.Tensor):
1021
+ Cell_vector = Cell_vector.numpy(force=True)
1022
+ elif not isinstance(Cell_vector, np.ndarray):
1023
+ Cell_vector = np.asarray(Cell_vector)
1024
+ # write head information.
1025
+ if self.batch_tensor is not None:
1026
+ dumper.start_from_arrays(
1027
+ 1,
1028
+ self.batch_tensor.numpy(force=True), # batch indices
1029
+ Cell_vector,
1030
+ np.asarray(atomic_numbers), # element type / atomic number
1031
+ atom_masks_arr, # fixation mask
1032
+ )
1033
+ dumper.step(
1034
+ self.batch_tensor.numpy(force=True),
1035
+ Cell_vector,
1036
+ np.asarray(atomic_numbers),
1037
+ atom_masks_arr
1038
+ )
1039
+ else:
1040
+ dumper.start_from_arrays(
1041
+ 1,
1042
+ Cell_vector,
1043
+ np.asarray(atomic_numbers), # element type / atomic number
1044
+ atom_masks_arr, # fixation mask
1045
+ )
1046
+ dumper.step(
1047
+ Cell_vector,
1048
+ np.asarray(atomic_numbers),
1049
+ atom_masks_arr
1050
+ )
1051
+ # continue to write main data
1052
+ dumper.start_from_arrays(
1053
+ _num_dump,
1054
+ self.Ekt_vir.numpy(),
1055
+ X.numpy(),
1056
+ V.numpy(),
1057
+ V.numpy(), # Here F is not calculated yet, but V has the same shape, hence can do this replacement
1058
+ )
1059
+ # custom initialization
1060
+ self.initialize(
1061
+ func,
1062
+ X,
1063
+ Element_list,
1064
+ masses,
1065
+ V,
1066
+ grad_func,
1067
+ func_args,
1068
+ func_kwargs,
1069
+ grad_func_args,
1070
+ grad_func_kwargs,
1071
+ is_grad_func_contain_y,
1072
+ require_grad,
1073
+ batch_indices,
1074
+ fixed_atom_tensor,
1075
+ is_fix_mass_center
1076
+ )
1077
+
1078
+ # print Atoms Information
1079
+ # if has no handler, means the handler is upper level 'Main', thus not print repeatedly
1080
+ if (self.verbose > 0) and len(self.logger.handlers) > 0:
1081
+ self._print_elem_info(Element_list, batch_indices)
1082
+ formatter1 = {'float': '{:> .2f}'.format}
1083
+ formatter2 = {'float': '{:> 5.10f}'.format}
1084
+
1085
+ # MAIN Loop
1086
+ with th.no_grad():
1087
+ X = X.contiguous()
1088
+ V = V.contiguous()
1089
+ masses = masses.contiguous()
1090
+ atom_masks = atom_masks.contiguous()
1091
+
1092
+ t_in = time.perf_counter()
1093
+ with th.set_grad_enabled(require_grad):
1094
+ X.requires_grad_(require_grad)
1095
+ Energy = func(X, *func_args, **func_kwargs) # Note: func must return th.Tensor(n_batch, )
1096
+ if batch_indices is not None:
1097
+ if len(Energy) != len(batch_indices): # check batch number of output
1098
+ raise RuntimeError(
1099
+ f'batch number of `func` output ({len(Energy)}) does not match the input `batch_indices` ({len(batch_indices)}'
1100
+ )
1101
+
1102
+ if is_grad_func_contain_y:
1103
+ Forces = - grad_func_(X, Energy, *grad_func_args, **grad_func_kwargs) * atom_masks
1104
+ else:
1105
+ Forces = - grad_func_(X, *grad_func_args, **grad_func_kwargs) * atom_masks
1106
+ Forces = Forces.contiguous()
1107
+
1108
+ #ptlist = list() # test <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
1109
+ fl = th.compile(self._main_for_loop_cpu, **self.compile_kwargs, disable=(not self.is_compile))
1110
+ fl(
1111
+ Energy,
1112
+ X,
1113
+ V,
1114
+ Forces,
1115
+ dumper,
1116
+ func, grad_func_, func_args, func_kwargs, grad_func_args, grad_func_kwargs,
1117
+ masses, atom_masks, is_grad_func_contain_y, batch_indices,
1118
+ MASS_CENTER,
1119
+ masses_short,
1120
+ formatter1,
1121
+ formatter2,
1122
+ is_fix_mass_center,
1123
+ move_to_center_freq,
1124
+ )
1125
+ if not self._HOLD_DUMPER:
1126
+ dumper.close()
1127
+
1128
+ del self.Ekt_vir
1129
+ #return ptlist # test <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
1130
+
1131
+ def _main_for_loop_cpu(
1132
+ self,
1133
+ Energy,
1134
+ X,
1135
+ V,
1136
+ Forces,
1137
+ dumper,
1138
+ func, grad_func_, func_args, func_kwargs, grad_func_args, grad_func_kwargs,
1139
+ masses, atom_masks, is_grad_func_contain_y, batch_indices,
1140
+ MASS_CENTER,
1141
+ masses_short,
1142
+ formatter1,
1143
+ formatter2,
1144
+ is_fix_mass_center,
1145
+ move_to_center_freq,
1146
+ ):
1147
+ _do_print = False
1148
+ for i in range(self.max_step):
1149
+ #with th.profiler.record_function('Calc. E, T <<<<<'):
1150
+ Ek, temperature = self._reduce_Ek_T(batch_indices, masses, V)
1151
+ self.Ek = Ek # th.sum(Ek, dim=0) # saving the real kinetic energy for VR & CSVR to avoid double counting.
1152
+ # if self.verbose > 0:
1153
+ #with th.profiler.record_function('DUMPING <<<<<'):
1154
+ if i % self.output_structures_per_step == 0:
1155
+ _do_print = True
1156
+ dumper.step(
1157
+ Energy.numpy(),
1158
+ X.numpy(),
1159
+ V.numpy(),
1160
+ Forces.numpy(),
1161
+ )
1162
+ # print, ensure print data correspond to dumping data, and then update. differ from cuda orders.
1163
+ #with th.profiler.record_function('PRINT <<<<<'):
1164
+ if _do_print:
1165
+ # Note: PRINTING IS VERY EXPENSIVE !!!
1166
+ if self.verbose > 0:
1167
+ # print format
1168
+ np.set_printoptions(
1169
+ precision=8,
1170
+ linewidth=1024,
1171
+ floatmode='fixed',
1172
+ suppress=True,
1173
+ formatter=formatter1,
1174
+ threshold=2000
1175
+ )
1176
+ self.logger.info(
1177
+ f'Step: {i:>12d}\n\t'
1178
+ f'T = {temperature.numpy(force=True)}\n\t'
1179
+ f'E_tol = {np.array2string((Ek + Energy).numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n\t'
1180
+ f'Ek = {np.array2string(Ek.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n\t'
1181
+ f'Ep = {np.array2string(Energy.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n\t'
1182
+ #f'Time: {time.perf_counter() - t_in:>5.4f}'
1183
+ )
1184
+ #t_in = time.perf_counter()
1185
+ if self.verbose > 1:
1186
+ # split to print
1187
+ if batch_indices is not None:
1188
+ X_tup = th.split(X, batch_indices, dim=1)
1189
+ V_tup = th.split(V, batch_indices, dim=1)
1190
+ else:
1191
+ X_tup = (X,)
1192
+ V_tup = (V,)
1193
+ np.set_printoptions(
1194
+ precision=8, floatmode='fixed', suppress=True, formatter=formatter2, threshold=3000000
1195
+ )
1196
+ self.logger.info('_' * 100)
1197
+ self.logger.info(f'Configuration {i}:')
1198
+ for __x in X_tup:
1199
+ X_str = np.array2string(
1200
+ __x.numpy(force=True), **FLOAT_ARRAY_FORMAT
1201
+ ).replace("[", " ").replace("]", " ")
1202
+ self.logger.info(f'{X_str}\n')
1203
+ del X_str, X_tup
1204
+ if self.verbose > 2:
1205
+ self.logger.info(f'Velocities {i}:')
1206
+ for __x in V_tup:
1207
+ V_str = np.array2string(
1208
+ __x.numpy(force=True), **FLOAT_ARRAY_FORMAT
1209
+ ).replace("[", " ").replace("]", " ")
1210
+ self.logger.info(f'{V_str}\n')
1211
+ del V_str
1212
+ self.logger.info('_' * 100)
1213
+ _do_print = False
1214
+
1215
+ # Update X, V
1216
+ #with th.profiler.record_function('MAIN UPDATE <<<<<'):
1217
+ X, V, Energy, Forces = self._updateXV(
1218
+ X, V, Forces,
1219
+ func, grad_func_, func_args, func_kwargs, grad_func_args, grad_func_kwargs,
1220
+ masses, atom_masks, is_grad_func_contain_y, batch_indices
1221
+ )
1222
+
1223
+ # Correct barycentric transition
1224
+ #with th.profiler.record_function('MOVE TO CENTER <<<<<'):
1225
+ if is_fix_mass_center and (i % move_to_center_freq == 0):
1226
+ dX = MASS_CENTER - self.calc_mass_center(masses, masses_short, X)
1227
+ dV = - self.calc_mass_center(masses, masses_short, V)
1228
+ X.add_(dX) # (n_batch, n_atom, n_dim) - (n_batch, 1, n_dim)
1229
+ V.add_(dV)
1230
+ self.time_now += self.time_step
1231
+
1232
+ # OVERRIDE THIS METHOD TO IMPLEMENT BatchMD UNDER VARIOUS ENSEMBLES.
1233
+ def _updateXV(
1234
+ self,
1235
+ X,
1236
+ V,
1237
+ Force,
1238
+ func,
1239
+ grad_func_,
1240
+ func_args,
1241
+ func_kwargs,
1242
+ grad_func_args,
1243
+ grad_func_kwargs,
1244
+ masses,
1245
+ atom_masks,
1246
+ is_grad_func_contain_y,
1247
+ batch_indices
1248
+ ) -> Tuple[th.Tensor, th.Tensor, th.Tensor, th.Tensor]:
1249
+ """ Update X, V, Force, and return X, V, Energy, Force. """
1250
+ raise NotImplementedError
1251
+ # return X, V, Energy, Force