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,863 @@
1
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
2
+ # Authors: Pu Pengxin, Song Xin
3
+ # Version: 0.9a
4
+ # File: _BaseMC.py
5
+ # Environment: Python 3.12
6
+
7
+ import logging
8
+ import math
9
+ import sys
10
+ from itertools import accumulate
11
+ from typing import Dict, Any, Optional, Sequence, Tuple, List
12
+ import time
13
+ import threading, queue
14
+ import warnings
15
+ import os
16
+
17
+ import numpy as np
18
+ import torch as th
19
+ from torch import nn
20
+
21
+ from BUCToolkit.BatchOptim._utils._warnings import NotConvergeWarning
22
+ from BUCToolkit.utils._Element_info import ATOMIC_SYMBOL
23
+ from BUCToolkit.utils._print_formatter import FLOAT_ARRAY_FORMAT, SCIENTIFIC_ARRAY_FORMAT
24
+ from BUCToolkit.utils.setup_loggers import has_any_handler, clear_all_handlers, BaseIO
25
+ from BUCToolkit.utils.index_ops import index_reduce
26
+ from BUCToolkit.utils.function_utils import preload_func
27
+ from BUCToolkit.BatchStructures.StructuresIO import structures_io_dumper
28
+
29
+
30
+ class _BaseMC(BaseIO):
31
+ def __init__(
32
+ self,
33
+ iter_scheme: str,
34
+ maxiter: int = 100,
35
+ output_file: str|None = None,
36
+ output_structures_per_step: int = 1,
37
+ device: str | th.device = 'cpu',
38
+ verbose: int = 0,
39
+ is_compile: bool = False,
40
+ compile_kwargs: Dict|None = None,
41
+ ) -> None:
42
+ r"""
43
+ A Base Framework of Algorithm for optimization.
44
+
45
+ Args:
46
+ maxiter: int, max iterations.
47
+ output_file: the path to the binary file that stores trajectories.
48
+ output_structures_per_step: int, output structures per output_structures_per_step steps.
49
+ device: The device that program runs on.
50
+ verbose: amount of print information.
51
+ is_compile: whether to use jit to compile the update function or not.
52
+ compile_kwargs: keyword arguments passed to compile. Only work when is_compile is True.
53
+
54
+ Method:
55
+ run: running the main optimization program.
56
+
57
+ """
58
+ self.batch_scatter = None
59
+ self.batch_tensor = None
60
+ self.atom_masks = None
61
+ self.T_now = None
62
+ self.is_compile = is_compile
63
+ self.compile_kwargs = compile_kwargs if compile_kwargs is not None else dict()
64
+
65
+ self.iterform = str(iter_scheme)
66
+ self.verbose = int(verbose)
67
+ self.device = device
68
+ self.maxiter = int(maxiter)
69
+ self.output_structures_per_step = int(output_structures_per_step)
70
+
71
+ self.n_batch, self.n_atom, self.n_dim = None, None, None
72
+ self.is_concat_X = False # whether the output of `func` was concatenated.
73
+ self.scatter_dim_out_size = None
74
+
75
+ # An inner attr that lets the dumper do not close after `self.run`.
76
+ # It is used to contiguously run within a loop.
77
+ # Adv. API `MonteCarlo` turns on it.
78
+ self._HOLD_DUMPER = False
79
+
80
+ # logger
81
+ super().__init__(output_file)
82
+ self.init_logger('Main.MC')
83
+
84
+ def _do_async_dump(self, q: queue.Queue):
85
+ """
86
+ A backend thread to async. dump
87
+ Args:
88
+ q: queue to receive data. contains: tuple of (dumper, event, *data)
89
+
90
+ Returns: None
91
+
92
+ """
93
+ while True:
94
+ try:
95
+ dumper, event, _print_E, _print_X = q.get()
96
+ if dumper is None:
97
+ break
98
+ # event: th.cuda.Event, ensure copy done
99
+ event.synchronize()
100
+ dumper.step(
101
+ _print_E.numpy(),
102
+ _print_X.numpy(),
103
+ )
104
+ except Exception as e:
105
+ self.logger.error(f"Error: Failed to dump data due to \"{e}\"")
106
+
107
+ def _do_async_print(self, q: queue.Queue):
108
+ """
109
+
110
+ Returns:
111
+
112
+ """
113
+ # Print information / Verbose
114
+ while True:
115
+ numit, _print_E, _print_dE, batch_indices, X, batch_slice_indx = q.get()
116
+ if numit is None:
117
+ break
118
+ if self.verbose > 0:
119
+ self.logger.info(
120
+ f"ITERATION {numit:>5d}\n "
121
+ f"delta E: {np.array2string(_print_dE.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
122
+ f"Accepted: {np.array2string(self.is_accept.numpy(force=True))}\n "
123
+ f"Energies: {np.array2string(_print_E.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
124
+ f"Temperature: {self.T_now:.3e}\n "
125
+ # f"TIME: {time.perf_counter() - t_step:>6.4f} s"
126
+ )
127
+ # t_step = time.perf_counter()
128
+ if self.verbose > 1:
129
+ # split batches if specified batch
130
+ if batch_indices is not None:
131
+ X_np = X.numpy(force=True)
132
+ X_tup = np.split(X_np, batch_slice_indx[1:-1], axis=1)
133
+ else:
134
+ X_tup = (X.numpy(force=True),)
135
+ self.logger.info(f" Coordinates:\n")
136
+ X_str = [
137
+ np.array2string(xi, **FLOAT_ARRAY_FORMAT).replace("[", " ").replace("]", " ")
138
+ for xi in X_tup
139
+ ]
140
+ [self.logger.info(f'{x_str}\n') for x_str in X_str]
141
+
142
+ def calc_shape_center(self, Xr, ) -> th.Tensor:
143
+ """
144
+ calculate shape center based on Xr.
145
+ Args:
146
+ Xr:
147
+
148
+ Returns: shape_center
149
+
150
+ """
151
+ if self.batch_scatter is None:
152
+ # initialize topologie
153
+ shape_center = th.mean(Xr, dim=1, keepdim=True) # (n_batch, 1, n_dim)
154
+ else:
155
+ # initialize topologie
156
+ shape_center = index_reduce(Xr, self.batch_scatter, dim=1, out_size=self.scatter_dim_out_size) / self.batch_tensor # (1, n_batch, n_dim)
157
+ shape_center = shape_center.index_select(1, self.batch_scatter) # (1, sumN*A, n_dim)
158
+
159
+ return shape_center
160
+
161
+ def run(
162
+ self,
163
+ func: Any | nn.Module,
164
+ X: th.Tensor,
165
+ Element_list: List[List[str]] | List[List[int]] | None = None,
166
+ Cell_vector: th.Tensor | None = None,
167
+ func_args: Sequence = tuple(),
168
+ func_kwargs: Dict | None = None,
169
+ batch_indices: None | List[int] | Tuple[int, ...] | th.Tensor = None,
170
+ fixed_atom_tensor: Optional[th.Tensor] = None,
171
+ move_to_center_freq: int = -1
172
+ ):
173
+ try:
174
+ if th.device(self.device).type == "cuda":
175
+ self.__run_on_cuda(
176
+ func,
177
+ X,
178
+ Element_list,
179
+ Cell_vector,
180
+ func_args,
181
+ func_kwargs,
182
+ batch_indices,
183
+ fixed_atom_tensor,
184
+ move_to_center_freq
185
+ )
186
+ elif th.device(self.device).type == "cpu":
187
+ self.__run_on_cpu(
188
+ func,
189
+ X,
190
+ Element_list,
191
+ Cell_vector,
192
+ func_args,
193
+ func_kwargs,
194
+ batch_indices,
195
+ fixed_atom_tensor,
196
+ move_to_center_freq
197
+ )
198
+ else:
199
+ raise NotImplementedError(F"device {self.device} not supported.")
200
+ finally:
201
+ self.dumper.close()
202
+
203
+ def __run_on_cuda(
204
+ self,
205
+ func: Any | nn.Module,
206
+ X: th.Tensor,
207
+ Element_list: List[List[str]] | List[List[int]] | None = None,
208
+ Cell_vector: th.Tensor | None = None,
209
+ func_args: Sequence = tuple(),
210
+ func_kwargs: Dict | None = None,
211
+ batch_indices: None | List[int] | Tuple[int, ...] | th.Tensor = None,
212
+ fixed_atom_tensor: Optional[th.Tensor] = None,
213
+ move_to_center_freq: int = -1
214
+ ) -> None:
215
+ """
216
+ Run MC Algorithm.
217
+
218
+ Parameters:
219
+ func: the main function of instantiated torch.nn.Module class.
220
+ X: Tensor[n_batch, n_atom, 3], the atom coordinates that input to func.
221
+ Cell_vector: Tensor[n_batch, 3, 3], the cell vectors. Only for logging out information, not involves in calculation.
222
+ Element_list: The list of elements/atom types. Only for logging out information, not involves in calculation.
223
+ if None, all atomic number of points will be labeled to 0.
224
+ func_args: optional, other input of func.
225
+ func_kwargs: optional, other input of func.
226
+ fixed_atom_tensor: Optional[th.Tensor], the indices of X that fixed.
227
+ batch_indices: the split points for given X, Element_list & V_init, must be 1D integer array_like.
228
+ the format of batch_indices is same as `split_size_or_sections` in torch.split:
229
+ 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]
230
+ move_to_center_freq: the period of translating coordinates and velocities of atoms into the shape center & 0.
231
+ Note that is not MASS CENTER but shape center, different from MD.
232
+ if `move_to_center_freq` <= 0, the translation would not apply.
233
+
234
+ Return: None
235
+ """
236
+
237
+ t_main = time.perf_counter()
238
+ if func_kwargs is None:
239
+ func_kwargs = dict()
240
+ # Check batch indices; irregular batch
241
+ if isinstance(X, th.Tensor):
242
+ if len(X.shape) == 2:
243
+ X = X.unsqueeze(0)
244
+ elif len(X.shape) != 3:
245
+ raise ValueError(f'`X` must be 2D or 3D, but got shape [{X.shape}]')
246
+ n_batch, n_atom, n_dim = X.shape
247
+ else:
248
+ raise TypeError(f'`X` must be torch.Tensor, but occurred {type(X)}.')
249
+ if batch_indices is not None:
250
+ if n_batch != 1:
251
+ raise RuntimeError(f'If batch_indices was specified, the 1st dimension of X must be 1 instead of {n_batch}.')
252
+ if isinstance(batch_indices, (th.Tensor, np.ndarray)):
253
+ batch_indices = batch_indices.tolist()
254
+ elif not isinstance(batch_indices, (Tuple, List)):
255
+ raise TypeError(f'Invalid type of batch_indices {type(batch_indices)}. '
256
+ f'It must be Sequence[int] | th.Tensor | np.ndarray | None')
257
+ for i in batch_indices: assert isinstance(i, int), f'All elements in batch_indices must be int, but occurred {type(i)}'
258
+ n_true_batch = len(batch_indices)
259
+ batch_slice_indx = [0] + list(accumulate(batch_indices)) # convert n_atom of each batch into split point of each batch
260
+ self.batch_tensor = th.as_tensor(batch_indices, device=self.device)
261
+ self.batch_scatter = th.repeat_interleave(
262
+ th.arange(0, len(batch_indices), dtype=th.int64, device=self.device),
263
+ self.batch_tensor,
264
+ dim=0
265
+ )
266
+ self.scatter_dim_out_size = self.batch_scatter.max().item() + 1
267
+ else:
268
+ n_true_batch = n_batch
269
+ self.batch_scatter = None
270
+ self.batch_tensor = None
271
+ batch_slice_indx = None
272
+ # initialize vars
273
+ maxiter = self.maxiter
274
+ self.n_batch, self.n_atom, self.n_dim = n_batch, n_atom, n_dim
275
+ if Element_list is not None:
276
+ atomic_numbers = list()
277
+ for _Elem in Element_list:
278
+ atomic_numbers.append([ATOMIC_SYMBOL[__elem] if isinstance(__elem, str) else int(__elem) for __elem in _Elem])
279
+ else:
280
+ atomic_numbers = [[0]*self.n_atom]*self.n_batch
281
+ if not isinstance(move_to_center_freq, int):
282
+ raise TypeError(f'`move_to_center_freq` must be an integer, but got {type(move_to_center_freq)}.')
283
+ is_fix_mass_center = (move_to_center_freq > 0)
284
+
285
+ # Selective dyamics
286
+ if fixed_atom_tensor is None:
287
+ self.atom_masks = th.ones_like(X, device=self.device)
288
+ elif fixed_atom_tensor.shape == X.shape:
289
+ self.atom_masks = fixed_atom_tensor.to(self.device)
290
+ else:
291
+ raise RuntimeError(f'The shape of fixed_atom_tensor (shape: {fixed_atom_tensor.shape}) does not match X (shape: {X.shape}).')
292
+ # other check
293
+ if (not isinstance(maxiter, int)) or (maxiter <= 0):
294
+ raise ValueError(f'Invalid value of maxiter: {maxiter}. It would be an integer greater than 0.')
295
+
296
+ # set variables device
297
+ func = preload_func(func, self.device)
298
+ X = X.to(self.device)
299
+
300
+ # calc. freedom degree
301
+ if batch_indices is None:
302
+ _free_degree = X.shape[1] * n_dim
303
+ if is_fix_mass_center:
304
+ _free_degree -= 3
305
+ self.free_degree = th.full((n_batch,), _free_degree, dtype=th.int64, device=self.device)
306
+ n_reduce = th.where(th.abs(self.atom_masks) < 1e-6, 1, 0).sum(dim=(-2, -1)) # (n_batch, )
307
+ self.free_degree -= n_reduce
308
+ else:
309
+ self.free_degree = self.batch_tensor * n_dim # (n_batch, )
310
+ if is_fix_mass_center:
311
+ self.free_degree -= 3
312
+ n_reduce_tensor = th.where(th.abs(self.atom_masks) < 1e-6, 1, 0).sum(dim=-1)
313
+ n_reduce = index_reduce(n_reduce_tensor, self.batch_scatter, dim=1, out_size=self.scatter_dim_out_size).squeeze(0) # (n_batch, )
314
+ self.free_degree -= n_reduce
315
+
316
+ # initialize the dumper
317
+ X_arr = X.numpy(force=True)
318
+ _x_dtype = X_arr.dtype.str
319
+ atom_masks_arr = self.atom_masks.numpy(force=True).astype(_x_dtype)
320
+ _num_dump = math.ceil(self.maxiter / self.output_structures_per_step)
321
+ # pre-allocate the tensors
322
+ # _buf_* is the vars on GPU that apply copy.
323
+ # _print_* is the vars on CPU that async. do D2H for _buf_*.
324
+ if batch_indices is not None:
325
+ _print_E = th.empty(len(batch_indices), device='cpu', dtype=th.float32, pin_memory=True)
326
+ _buf_E = th.empty(len(batch_indices), device=self.device, dtype=th.float32)
327
+ _print_dE = th.zeros(len(batch_indices), device='cpu', dtype=th.float32, pin_memory=True)
328
+ _buf_dE = th.empty(len(batch_indices), device=self.device, dtype=th.float32)
329
+ else:
330
+ _print_E = th.empty(n_batch, device='cpu', dtype=th.float32, pin_memory=True)
331
+ _buf_E = th.empty(n_batch, device=self.device, dtype=th.float32)
332
+ _print_dE = th.zeros(n_batch, device='cpu', dtype=th.float32, pin_memory=True)
333
+ _buf_dE = th.empty(n_batch, device=self.device, dtype=th.float32)
334
+ _print_X = th.empty_like(X, device='cpu', dtype=th.float32, pin_memory=True)
335
+ _buf_X = th.empty_like(X, device=self.device, dtype=th.float32)
336
+ # write head information.
337
+ if Cell_vector is None:
338
+ Cell_vector = np.zeros((n_true_batch, 3, 3), dtype=np.float32)
339
+ elif isinstance(Cell_vector, th.Tensor):
340
+ Cell_vector = Cell_vector.numpy(force=True)
341
+ elif not isinstance(Cell_vector, np.ndarray):
342
+ Cell_vector = np.asarray(Cell_vector)
343
+ if self.batch_tensor is not None:
344
+ self.dumper.start_from_arrays(
345
+ 1,
346
+ self.batch_tensor.numpy(force=True), # batch indices
347
+ Cell_vector, # cell
348
+ np.asarray(atomic_numbers), # element type / atomic number
349
+ atom_masks_arr, # fixation mask
350
+ )
351
+ self.dumper.step(
352
+ self.batch_tensor.numpy(force=True),
353
+ Cell_vector,
354
+ np.asarray(atomic_numbers),
355
+ atom_masks_arr
356
+ )
357
+ else:
358
+ self.dumper.start_from_arrays(
359
+ 1,
360
+ Cell_vector,
361
+ np.asarray(atomic_numbers), # element type / atomic number
362
+ atom_masks_arr, # fixation mask
363
+ )
364
+ self.dumper.step(
365
+ Cell_vector,
366
+ np.asarray(atomic_numbers),
367
+ atom_masks_arr
368
+ )
369
+ # continue to write main data
370
+ self.dumper.start_from_arrays(
371
+ _num_dump,
372
+ _print_E.numpy(),
373
+ _print_X.numpy(),
374
+ )
375
+
376
+ # initialize
377
+ t_step = time.perf_counter()
378
+ self.initialize_algo_param()
379
+ #ptlist = [X[:, None, :, 0].numpy(force=True)] # for converged samp, stop calc., test <<<
380
+ if self.verbose:
381
+ self.logger.info('-' * 100)
382
+ self.logger.info(f'Iteration Scheme: {self.iterform}')
383
+ self.logger.info('-' * 100)
384
+ # MAIN LOOP
385
+ with th.no_grad():
386
+ energies: th.Tensor = func(X, *func_args, **func_kwargs)
387
+ delta_E: th.Tensor = th.zeros_like(energies)
388
+ # note: irregular tensor regularized by concat. thus n_batch of X shown as 1, but y has shape of the true batch size.
389
+ if energies.shape[0] != self.n_batch:
390
+ if batch_indices is None:
391
+ raise ValueError(
392
+ f"batch indices is None "
393
+ f"while shape of model output ({energies.shape}) does not match batch size ({self.n_batch})."
394
+ )
395
+ if energies.shape[0] != n_true_batch:
396
+ raise ValueError(f"shape of output ({energies.shape}) does not match given batch indices")
397
+ self.is_concat_X = (batch_indices is not None)
398
+ self.is_accept = th.full_like(energies, False, dtype=th.bool)
399
+
400
+ # preload a graph of shape center. A very small overhead that no longer `is_fix_center`, computation does.
401
+ if batch_indices is None:
402
+ SHAPE_CENTER = th.mean(X, dim=1, keepdim=True) # (n_batch, 1, n_dim)
403
+ else:
404
+ SHAPE_CENTER = index_reduce(X, self.batch_scatter, dim=1, out_size=self.scatter_dim_out_size) / self.batch_tensor.unsqueeze(-1)
405
+ SHAPE_CENTER = SHAPE_CENTER.index_select(1, self.batch_scatter) # (1, sumN*A, n_dim)
406
+
407
+ copy_stream = th.cuda.Stream()
408
+ copy_event = th.cuda.Event()
409
+ compute_event = th.cuda.Event()
410
+ compute_event.record(th.cuda.default_stream(self.device)) # the default stream is the compute (main) stream.
411
+ dump_queue = queue.Queue()
412
+ dump_thread = threading.Thread(target=self._do_async_dump, args=(dump_queue,))
413
+ logout_queue = queue.Queue()
414
+ logout_thread = threading.Thread(target=self._do_async_print, args=(logout_queue,))
415
+ try:
416
+ dump_thread.start()
417
+ logout_thread.start()
418
+ fl = th.compile(self._main_for_loop_cuda, **self.compile_kwargs, disable=(not self.is_compile))
419
+ fl(
420
+ compute_event,
421
+ _buf_E,
422
+ _buf_dE,
423
+ _buf_X,
424
+ _print_E,
425
+ _print_dE,
426
+ _print_X,
427
+ energies,
428
+ delta_E,
429
+ X,
430
+ copy_stream,
431
+ copy_event,
432
+ dump_queue,
433
+ logout_queue,
434
+ func, func_args, func_kwargs,
435
+ batch_indices, batch_slice_indx,
436
+ is_fix_mass_center,
437
+ move_to_center_freq,
438
+ SHAPE_CENTER
439
+ )
440
+
441
+ if not self._HOLD_DUMPER:
442
+ self.dumper.close()
443
+ th.cuda.synchronize()
444
+ finally:
445
+ dump_queue.put([None]*4)
446
+ dump_thread.join()
447
+ logout_queue.put([None]*6)
448
+ logout_thread.join()
449
+
450
+ if self.verbose > 0:
451
+ self.logger.info(
452
+ '-' * 100 + f'\nMAIN LOOP Done. Total Time: {time.perf_counter() - t_main:<.4f} s\n'
453
+ )
454
+ if self.verbose < 2: # verbose = 1, brief mode only output last step coords.
455
+ # split batches if specified batch
456
+ if batch_indices is not None:
457
+ X_np = X.numpy(force=True)
458
+ X_tup = np.split(X_np, batch_slice_indx[1:-1], axis=1)
459
+ else:
460
+ X_tup = (X.numpy(force=True),)
461
+ self.logger.info(f" Final Coordinates:\n")
462
+ X_str = [
463
+ np.array2string(xi, **FLOAT_ARRAY_FORMAT).replace("[", " ").replace("]", " ")
464
+ for xi in X_tup
465
+ ]
466
+ [self.logger.info(f'{x_str}\n') for x_str in X_str]
467
+ # output
468
+ #return energies, X #, ptlist # test <<<
469
+
470
+ def _main_for_loop_cuda(
471
+ self,
472
+ compute_event,
473
+ _buf_E,
474
+ _buf_dE,
475
+ _buf_X,
476
+ _print_E,
477
+ _print_dE,
478
+ _print_X,
479
+ energies,
480
+ delta_E,
481
+ X,
482
+ copy_stream,
483
+ copy_event,
484
+ dump_queue,
485
+ logout_queue,
486
+ func, func_args, func_kwargs,
487
+ batch_indices, batch_slice_indx,
488
+ is_fix_mass_center,
489
+ move_to_center_freq,
490
+ SHAPE_CENTER
491
+ ):
492
+ #t_step = time.perf_counter()
493
+ _do_print = False
494
+ for numit in range(self.maxiter):
495
+ # dump
496
+ if numit % self.output_structures_per_step == 0:
497
+ _do_print = True
498
+ compute_event.wait(th.cuda.default_stream(self.device))
499
+ # D2D, fast copy purely on GPU
500
+ _buf_E.copy_(energies.squeeze().contiguous())
501
+ _buf_dE.copy_(delta_E)
502
+ _buf_X.copy_(X)
503
+ # D2H, async.
504
+ with th.cuda.stream(copy_stream):
505
+ copy_stream.wait_stream(th.cuda.default_stream(self.device))
506
+ _print_E.copy_(_buf_E, non_blocking=True)
507
+ _print_dE.copy_(_buf_dE, non_blocking=True)
508
+ _print_X.copy_(_buf_X, non_blocking=True) # D2H
509
+ copy_event.record(copy_stream)
510
+ # use backend thread to dump
511
+ dump_queue.put((self.dumper, copy_event, _print_E, _print_X))
512
+
513
+ # update X & energy. update before printing to cover the dumping cost by update.
514
+ X_old = X # .clone() # note that X is not in-place updated in `self._update_X` due to `where` ops. Hence `clone` is not necessary.
515
+ energies_old = energies.clone()
516
+ energies, delta_E, X = self._update_X(func, func_args, func_kwargs, energies_old, X) # (n_batch, n_atom, n_dim)
517
+ X_diff = X - X_old
518
+ compute_event.record(th.cuda.default_stream(self.device)) # the default stream is the compute (main) stream.
519
+
520
+ if _do_print:
521
+ logout_queue.put((numit, _print_E, _print_dE, batch_indices, X, batch_slice_indx))
522
+ _do_print = False
523
+
524
+ # update algo. parameters.
525
+ self._update_algo_param(numit, X_diff)
526
+
527
+ # move to center
528
+ if is_fix_mass_center and (numit % move_to_center_freq == 0):
529
+ X.add_(SHAPE_CENTER - self.calc_shape_center(X)) # (n_batch, n_atom, n_dim) - (n_batch, 1, n_dim)
530
+
531
+ # Check NaN
532
+ # if th.any(energies != energies): raise RuntimeError(f'NaN Occurred in output: {energies}')
533
+
534
+ # ptlist.append(X[:, None, :, 0].numpy(force=True)) # test <<<
535
+
536
+ def __run_on_cpu(
537
+ self,
538
+ func: Any | nn.Module,
539
+ X: th.Tensor,
540
+ Element_list: List[List[str]] | List[List[int]] | None = None,
541
+ Cell_vector: th.Tensor | None = None,
542
+ func_args: Sequence = tuple(),
543
+ func_kwargs: Dict | None = None,
544
+ batch_indices: None | List[int] | Tuple[int, ...] | th.Tensor = None,
545
+ fixed_atom_tensor: Optional[th.Tensor] = None,
546
+ move_to_center_freq: int = -1
547
+ ) -> None:
548
+ """
549
+ Run MC Algorithm.
550
+
551
+ Parameters:
552
+ func: the main function of instantiated torch.nn.Module class.
553
+ X: Tensor[n_batch, n_atom, 3], the atom coordinates that input to func.
554
+ Cell_vector: Tensor[n_batch, 3, 3], the cell vectors. Only for logging out information, not involves in calculation.
555
+ Element_list: The list of elements/atom types. Only for logging out information, not involves in calculation.
556
+ if None, all atomic number of points will be labeled to 0.
557
+ func_args: optional, other input of func.
558
+ func_kwargs: optional, other input of func.
559
+ fixed_atom_tensor: Optional[th.Tensor], the indices of X that fixed.
560
+ batch_indices: Sequence | th.Tensor | np.ndarray | None, the split points for given X, Element_list & V_init, must be 1D integer array_like.
561
+ the format of batch_indices is same as `split_size_or_sections` in torch.split:
562
+ 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]
563
+
564
+ Return:
565
+ min func: Tensor(n_batch, ), the minimum of func.
566
+ argmin func: Tensor(X.shape), the X corresponds to min func.
567
+ grad of argmin func: Tensor(X.shape), only output when `output_grad` == True. The gradient of X corresponding to minimum.
568
+ """
569
+ t_main = time.perf_counter()
570
+ if func_kwargs is None:
571
+ func_kwargs = dict()
572
+ # Check batch indices; irregular batch
573
+ if isinstance(X, th.Tensor):
574
+ if len(X.shape) == 2:
575
+ X = X.unsqueeze(0)
576
+ elif len(X.shape) != 3:
577
+ raise ValueError(f'`X` must be 2D or 3D, but got shape [{X.shape}]')
578
+ if not isinstance(move_to_center_freq, int):
579
+ raise TypeError(f'`move_to_center_freq` must be an integer, but got {type(move_to_center_freq)}.')
580
+ n_batch, n_atom, n_dim = X.shape
581
+ else:
582
+ raise TypeError(f'`X` must be torch.Tensor, but occurred {type(X)}.')
583
+ if batch_indices is not None:
584
+ if n_batch != 1:
585
+ raise RuntimeError(f'If batch_indices was specified, the 1st dimension of X must be 1 instead of {n_batch}.')
586
+ if isinstance(batch_indices, (th.Tensor, np.ndarray)):
587
+ batch_indices = batch_indices.tolist()
588
+ elif not isinstance(batch_indices, (Tuple, List)):
589
+ raise TypeError(f'Invalid type of batch_indices {type(batch_indices)}. '
590
+ f'It must be Sequence[int] | th.Tensor | np.ndarray | None')
591
+ for i in batch_indices: assert isinstance(i, int), f'All elements in batch_indices must be int, but occurred {type(i)}'
592
+ n_true_batch = len(batch_indices)
593
+ batch_slice_indx = [0] + list(accumulate(batch_indices)) # convert n_atom of each batch into split point of each batch
594
+ self.batch_tensor = th.as_tensor(batch_indices, device=self.device)
595
+ self.batch_scatter = th.repeat_interleave(
596
+ th.arange(0, len(batch_indices), dtype=th.int64, device=self.device),
597
+ self.batch_tensor,
598
+ dim=0
599
+ )
600
+ else:
601
+ n_true_batch = n_batch
602
+ self.batch_scatter = None
603
+ self.batch_tensor = None
604
+ batch_slice_indx = None
605
+ # initialize vars
606
+ maxiter = self.maxiter
607
+ n_batch, n_atom, n_dim = X.shape
608
+ self.n_batch, self.n_atom, self.n_dim = n_batch, n_atom, n_dim
609
+ if Element_list is not None:
610
+ atomic_numbers = list()
611
+ for _Elem in Element_list:
612
+ atomic_numbers.append([ATOMIC_SYMBOL[__elem] if isinstance(__elem, str) else int(__elem) for __elem in _Elem])
613
+ else:
614
+ atomic_numbers = [[0]*self.n_atom]*self.n_batch
615
+ if not isinstance(move_to_center_freq, int):
616
+ raise TypeError(f'`move_to_center_freq` must be an integer, but got {type(move_to_center_freq)}.')
617
+ is_fix_mass_center = (move_to_center_freq > 0)
618
+
619
+ # Selective dynamics
620
+ if fixed_atom_tensor is None:
621
+ self.atom_masks = th.ones_like(X, device=self.device)
622
+ elif fixed_atom_tensor.shape == X.shape:
623
+ self.atom_masks = fixed_atom_tensor.to(self.device)
624
+ else:
625
+ raise RuntimeError(f'The shape of fixed_atom_tensor (shape: {fixed_atom_tensor.shape}) does not match X (shape: {X.shape}).')
626
+ self.fixed_indices = th.where(th.any(self.atom_masks, dim=-1))[1] # COO sumN or n_atom dim
627
+ # other check
628
+ if (not isinstance(maxiter, int)) or (maxiter <= 0):
629
+ raise ValueError(f'Invalid value of maxiter: {maxiter}. It would be an integer greater than 0.')
630
+
631
+ # set variables device
632
+ func = preload_func(func, self.device)
633
+ X = X.to(self.device)
634
+
635
+ # calc. freedom degree
636
+ if batch_indices is None:
637
+ _free_degree = X.shape[1] * n_dim
638
+ if is_fix_mass_center:
639
+ _free_degree -= 3
640
+ self.free_degree = th.full((n_batch,), _free_degree, dtype=th.int64, device=self.device)
641
+ n_reduce = th.where(th.abs(self.atom_masks) < 1e-6, 1, 0).sum(dim=(-2, -1)) # (n_batch, )
642
+ self.free_degree -= n_reduce
643
+ else:
644
+ self.free_degree = self.batch_tensor * n_dim # (n_batch, )
645
+ if is_fix_mass_center:
646
+ self.free_degree -= 3
647
+ n_reduce_tensor = th.where(th.abs(self.atom_masks) < 1e-6, 1, 0).sum(dim=-1)
648
+ n_reduce = index_reduce(n_reduce_tensor, self.batch_scatter, dim=1, out_size=self.scatter_dim_out_size).squeeze(0) # (n_batch, )
649
+ self.free_degree -= n_reduce
650
+
651
+ # initialize the dumper
652
+ X_arr = X.numpy(force=True)
653
+ _x_dtype = X_arr.dtype.str
654
+ atom_masks_arr = self.atom_masks.numpy(force=True).astype(_x_dtype)
655
+ _num_dump = math.ceil(self.maxiter / self.output_structures_per_step)
656
+ # write head information.
657
+ if Cell_vector is None:
658
+ Cell_vector = np.zeros((n_true_batch, 3, 3), dtype=np.float32)
659
+ elif isinstance(Cell_vector, th.Tensor):
660
+ Cell_vector = Cell_vector.numpy(force=True)
661
+ elif not isinstance(Cell_vector, np.ndarray):
662
+ Cell_vector = np.asarray(Cell_vector)
663
+ if self.batch_tensor is not None:
664
+ self.dumper.start_from_arrays(
665
+ 1,
666
+ self.batch_tensor.numpy(force=True), # batch indices
667
+ Cell_vector, # cell
668
+ np.asarray(atomic_numbers), # element type / atomic number
669
+ atom_masks_arr, # fixation mask
670
+ )
671
+ self.dumper.step(
672
+ self.batch_tensor.numpy(force=True),
673
+ Cell_vector,
674
+ np.asarray(atomic_numbers),
675
+ atom_masks_arr
676
+ )
677
+ else:
678
+ self.dumper.start_from_arrays(
679
+ 1,
680
+ Cell_vector,
681
+ np.asarray(atomic_numbers), # element type / atomic number
682
+ atom_masks_arr, # fixation mask
683
+ )
684
+ self.dumper.step(
685
+ Cell_vector,
686
+ np.asarray(atomic_numbers),
687
+ atom_masks_arr
688
+ )
689
+
690
+ # initialize
691
+ t_step = time.perf_counter()
692
+ self.initialize_algo_param()
693
+ #ptlist = [X[:, None, :, 0].numpy(force=True)] # for converged samp, stop calc., test <<<
694
+ if self.verbose:
695
+ self.logger.info('-' * 100)
696
+ self.logger.info(f'Iteration Scheme: {self.iterform}')
697
+ self.logger.info('-' * 100)
698
+ # MAIN LOOP
699
+ with th.no_grad():
700
+ energies: th.Tensor = func(X, *func_args, **func_kwargs)
701
+ delta_E = th.zeros_like(energies)
702
+ # continue to write main data
703
+ self.dumper.start_from_arrays(
704
+ _num_dump,
705
+ energies.numpy(),
706
+ X.numpy(),
707
+ )
708
+ # note: irregular tensor regularized by concat. thus n_batch of X shown as 1, but y has shape of the true batch size.
709
+ if energies.shape[0] != self.n_batch:
710
+ if batch_indices is None:
711
+ raise ValueError(
712
+ f"batch indices is None "
713
+ f"while shape of model output ({energies.shape}) does not match batch size ({self.n_batch})."
714
+ )
715
+ if energies.shape[0] != n_true_batch:
716
+ raise ValueError(f"shape of output ({energies.shape}) does not match given batch indices")
717
+ self.is_concat_X = (batch_indices is not None)
718
+ self.is_accept = th.full_like(energies, False, dtype=th.bool)
719
+
720
+ # preload a graph of shape center. A very small overhead that no longer `is_fix_center`, computation does.
721
+ if batch_indices is None:
722
+ SHAPE_CENTER = th.mean(X, dim=1, keepdim=True) # (n_batch, 1, n_dim)
723
+ else:
724
+ SHAPE_CENTER = index_reduce(X, self.batch_scatter, dim=1, out_size=self.scatter_dim_out_size) / self.batch_tensor.unsqueeze(-1)
725
+ SHAPE_CENTER = SHAPE_CENTER.index_select(1, self.batch_scatter) # (1, sumN*A, n_dim)
726
+
727
+ fl = th.compile(self._main_for_loop_cpu, **self.compile_kwargs, disable=(not self.is_compile))
728
+ fl(
729
+ energies,
730
+ delta_E,
731
+ X,
732
+ func, func_args, func_kwargs,
733
+ batch_indices, batch_slice_indx,
734
+ is_fix_mass_center,
735
+ move_to_center_freq,
736
+ SHAPE_CENTER
737
+ )
738
+
739
+ if not self._HOLD_DUMPER:
740
+ self.dumper.close()
741
+
742
+ if self.verbose > 0:
743
+ self.logger.info(
744
+ '-' * 100 + f'\nMAIN LOOP Done. Total Time: {time.perf_counter() - t_main:<.4f} s\n'
745
+ )
746
+ if self.verbose < 2: # verbose = 1, brief mode only output last step coords.
747
+ # split batches if specified batch
748
+ if batch_indices is not None:
749
+ X_np = X.numpy(force=True)
750
+ X_tup = np.split(X_np, batch_slice_indx[1:-1], axis=1)
751
+ else:
752
+ X_tup = (X.numpy(force=True),)
753
+ self.logger.info(f" Final Coordinates:\n")
754
+ X_str = [
755
+ np.array2string(xi, **FLOAT_ARRAY_FORMAT).replace("[", " ").replace("]", " ")
756
+ for xi in X_tup
757
+ ]
758
+ [self.logger.info(f'{x_str}\n') for x_str in X_str]
759
+
760
+ def _main_for_loop_cpu(
761
+ self,
762
+ energies,
763
+ delta_E,
764
+ X,
765
+ func, func_args, func_kwargs,
766
+ batch_indices, batch_slice_indx,
767
+ is_fix_mass_center,
768
+ move_to_center_freq,
769
+ SHAPE_CENTER
770
+ ):
771
+ _do_print = False
772
+ for numit in range(self.maxiter):
773
+ # dump
774
+ if numit % self.output_structures_per_step == 0:
775
+ _do_print = True
776
+ self.dumper.step(
777
+ energies.numpy(),
778
+ X.numpy(),
779
+ )
780
+ # Print information / Verbose
781
+ if _do_print:
782
+ if self.verbose > 0:
783
+ self.logger.info(
784
+ f"ITERATION {numit:>5d}\n "
785
+ f"delta E: {np.array2string(delta_E.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
786
+ f"Accepted: {np.array2string(self.is_accept.numpy(force=True))}\n "
787
+ f"Energies: {np.array2string(energies.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
788
+ f"Temperature: {self.T_now:.3e}\n "
789
+ #f"TIME: {time.perf_counter() - t_step:>6.4f} s"
790
+ )
791
+ #t_step = time.perf_counter()
792
+ if self.verbose > 1:
793
+ # split batches if specified batch
794
+ if batch_indices is not None:
795
+ X_np = X.numpy(force=True)
796
+ X_tup = np.split(X_np, batch_slice_indx[1:-1], axis=1)
797
+ else:
798
+ X_tup = (X.numpy(force=True),)
799
+ self.logger.info(f" Coordinates:\n")
800
+ X_str = [
801
+ np.array2string(xi, **FLOAT_ARRAY_FORMAT).replace("[", " ").replace("]", " ")
802
+ for xi in X_tup
803
+ ]
804
+ [self.logger.info(f'{x_str}\n') for x_str in X_str]
805
+
806
+ # update X & energy
807
+ X_old = X.clone()
808
+ energies_old = energies.clone()
809
+ energies, delta_E, X = self._update_X(func, func_args, func_kwargs, energies_old, X) # (n_batch, n_atom, n_dim)
810
+ X_diff = X - X_old
811
+
812
+ # update algo. parameters.
813
+ self._update_algo_param(numit, X_diff)
814
+
815
+ # Check NaN
816
+ # if not th.all(energies.isfinite()): raise RuntimeError(f'NaN Occurred in output: {energies}')
817
+
818
+ # ptlist.append(X[:, None, :, 0].numpy(force=True)) # test <<<
819
+
820
+ # move to center
821
+ if is_fix_mass_center and (numit % move_to_center_freq == 0):
822
+ X.add_(SHAPE_CENTER - self.calc_shape_center(X)) # (n_batch, n_atom, n_dim) - (n_batch, 1, n_dim)
823
+
824
+ def initialize_algo_param(self):
825
+ """
826
+ Override this method to initialize attribute variables for self._update_direction.
827
+ Examples:
828
+
829
+ Returns: None
830
+ """
831
+ raise NotImplementedError
832
+
833
+ def _update_X(
834
+ self,
835
+ func,
836
+ func_args,
837
+ func_kwargs,
838
+ energies_old: th.Tensor,
839
+ X: th.Tensor,
840
+ ) -> Tuple[th.Tensor, th.Tensor, th.Tensor]:
841
+ """
842
+ Override this method to implement X update algorithm.
843
+ Args:
844
+ func: function
845
+ func_args:
846
+ func_kwargs:
847
+ X: (n_batch, n_atom, 3), the independent vars X.
848
+
849
+ Returns:
850
+ energies, delta_E, X
851
+ """
852
+ raise NotImplementedError
853
+
854
+ def _update_algo_param(self, i: int, displace: th.Tensor) -> None:
855
+ """
856
+ Override this method to update the parameters of X update algorithm i.e., self.iterform.
857
+ Args:
858
+ i: iteration step now.
859
+ displace: (n_batch, n_atom*3, 1), the displacement of X at this step. displace = step-length * p
860
+
861
+ Returns: None
862
+ """
863
+ pass