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,702 @@
1
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
2
+ # Authors: Pu Pengxin, Song Xin
3
+ # Version: 0.9a
4
+ # File: _BaseOpt.py
5
+ # Environment: Python 3.12
6
+ import logging
7
+ import sys
8
+ from itertools import accumulate
9
+ from typing import Dict, Any, Literal, Optional, Sequence, Tuple, List, Callable
10
+ import time
11
+ import warnings
12
+ from abc import ABC, abstractmethod
13
+ import os
14
+
15
+ import numpy as np
16
+ import torch as th
17
+ from torch import nn
18
+ from BUCToolkit.BatchOptim._utils._line_search import LineSearch
19
+ from BUCToolkit.BatchOptim._utils._warnings import NotConvergeWarning
20
+ from BUCToolkit.utils.function_utils import preload_func
21
+ from BUCToolkit.utils.setup_loggers import BaseIO
22
+ from BUCToolkit.utils._print_formatter import FLOAT_ARRAY_FORMAT, SCIENTIFIC_ARRAY_FORMAT, STRING_ARRAY_FORMAT
23
+ from BUCToolkit.utils.index_ops import index_reduce, index_inner_product
24
+
25
+
26
+ class _BaseOpt(BaseIO, ABC):
27
+ def __init__(
28
+ self,
29
+ iter_scheme: str,
30
+ E_threshold: float = 1e-3,
31
+ F_threshold: float = 0.05,
32
+ maxiter: int = 100,
33
+ linesearch: Literal['Backtrack', 'B', 'Wolfe', 'W', 'MT', 'EXACT', 'None', 'N'] = 'Backtrack',
34
+ linesearch_maxiter: int = 10,
35
+ linesearch_thres: float = 0.02,
36
+ linesearch_factor: float = 0.6,
37
+ steplength: float = 0.5,
38
+ use_bb: bool = True,
39
+ output_file: str | None = None,
40
+ device: str | th.device = 'cpu',
41
+ verbose: int = 2,
42
+ _hold_samples: bool = False,
43
+ ) -> None:
44
+ r"""
45
+ A Base Framework of Algorithm for optimization.
46
+
47
+ Args:
48
+ E_threshold: float, threshold of difference of func between 2 iterations.
49
+ F_threshold: float, threshold of gradient of func.
50
+ maxiter: int, max iterations.
51
+ linesearch: Scheme of linesearch.
52
+ 'Backtrack': Backtrack line search to satisfy Armijo's condition.
53
+ 'B': Alias for 'Backtrack'.
54
+ 'Wolfe': More-Thuente algorithm line search to satisfy Wolfe-Powell (weak Wolfe) condition.
55
+ 'W': Alias for 'Wolfe'.
56
+ 'MT': Alias for 'Wolfe'.
57
+ 'EXACT': Exact line search by Brent algorithm.
58
+ 'BRENT': Alias for 'BRENT'.
59
+ 'None': No line search. Directly return input steplength.
60
+ linesearch_maxiter: Max iterations for linesearch.
61
+ linesearch_thres: Threshold for linesearch. Only for "Golden" and "Newton".
62
+ linesearch_factor: A factor in linesearch. Shrinkage factor for "Backtrack", scaling factor in interval search for "Golden" and line steplength for "Newton".
63
+ steplength: The initial step length.
64
+ use_bb: whether to use Barzilai-Borwein steplength (BB1 or long BB) as initial steplength instead of fixed one.
65
+ output_file: the file to dump trajectory. if None, nothing will be dumped.
66
+ device: The device that program runs on.
67
+ verbose: amount of print information.
68
+ _hold_samples: ONLY FOR SPECIAL USES (e.g., CI-NEB or DEBUG).
69
+ If True, optimizer will not remove any sample in a batch even if the sample has converged.
70
+
71
+ Method:
72
+ run: running the main optimization program.
73
+
74
+ """
75
+ warnings.filterwarnings('always', category=NotConvergeWarning)
76
+ warnings.filterwarnings('always', )
77
+
78
+ self.iterform = iter_scheme
79
+ self.n_true_batch = None
80
+
81
+ self.linesearch: str = linesearch
82
+ self.steplength: float = steplength
83
+ self.linesearch_maxiter = linesearch_maxiter
84
+ self.linesearch_thres = linesearch_thres
85
+ self.linesearch_factor = linesearch_factor
86
+ self.use_bb = use_bb
87
+ self._line_search = LineSearch(
88
+ linesearch,
89
+ maxiter=linesearch_maxiter,
90
+ thres=linesearch_thres,
91
+ factor=linesearch_factor,
92
+ verbose=verbose,
93
+ )
94
+
95
+ self.E_threshold = E_threshold
96
+ self.F_threshold = F_threshold
97
+ self.maxiter = maxiter
98
+ self.n_batch, self.n_atom, self.n_dim = None, None, None
99
+ self.converge_mask = None # To record the batch which has converged and not update.
100
+ self.is_concat_X = False # whether the output of `func` was concatenated.
101
+
102
+ self._hold_samples = _hold_samples
103
+ self.device = device
104
+ self.verbose = verbose
105
+
106
+ # logger
107
+ super().__init__(output_file)
108
+ self.init_logger('Main.OPT')
109
+
110
+ def _update_batch(self, mask: th.Tensor, func_args: Tuple, func_kwargs: Dict, grad_func_args: Tuple, grad_func_kwargs: Dict):
111
+ """
112
+ Default update method for the input of func if the func has non-opt variables, i.e., the identical transform.
113
+ Args:
114
+ mask:
115
+
116
+ Returns:
117
+
118
+ """
119
+ return func_args, func_kwargs, grad_func_args, grad_func_kwargs
120
+
121
+ def set_batch_updater(
122
+ self,
123
+ method: Callable[[th.Tensor, Tuple|None, Dict|None, Tuple|None, Dict|None], Tuple[Tuple, Dict, Tuple, Dict]] | None,
124
+ line_search_method: Callable[[th.Tensor, Tuple|None, Dict|None, Tuple|None, Dict|None], Tuple[Tuple, Dict, Tuple, Dict]] | None = None,
125
+ ) -> None:
126
+ """
127
+ Set a method to update the taget function when variables change.
128
+ If input Callables, these Callables receive a mask tensor of shape (n_batch, )
129
+ that only selects the `True` part to input to the function, the old
130
+ `func_args`, `func_kwargs`, `grad_func_args`, and `grad_func_kwargs`,
131
+ returns the corresponding masked new `func_args`, `func_kwargs`, `grad_func_args`, and `grad_func_kwargs`.
132
+ If input None, self._hold_samples will be set to True that toggles off the dynamic removal.
133
+
134
+ This method is used to dynamically 'remove' the samples which have been converged in a batch to avoid
135
+ redundant calculation of converged samples.
136
+
137
+ Default transform is identical transform (i.e., do nothing)
138
+
139
+ `method` is for main loop update; and `line_search_method` is for line search subroutine update;
140
+ Args:
141
+ method: Callable(
142
+ mask: Tensor, func_args: Tuple, func_kwargs: Dict, grad_func_args: Tuple, grad_func_kwargs: Dict
143
+ ) -> Tuple[Tuple, Dict, Tuple, Dict], the method of updating function arguments for a mask.
144
+ line_search_method: as the same use of `method`, but for line search subroutines.
145
+
146
+ Returns: None
147
+ """
148
+ if method is None:
149
+ self._hold_samples = True
150
+ elif callable(method):
151
+ self._update_batch = method
152
+ else:
153
+ raise TypeError(f'`method` must be a callable, but {type(method)} is not.')
154
+ self._line_search.set_batch_updater(line_search_method)
155
+
156
+ def run(
157
+ self,
158
+ func: Any | nn.Module,
159
+ X: th.Tensor,
160
+ grad_func: Any | nn.Module = None,
161
+ func_args: Tuple[Any] = tuple(),
162
+ func_kwargs: Dict | None = None,
163
+ grad_func_args: Tuple[Any] = tuple(),
164
+ grad_func_kwargs: Dict | None = None,
165
+ is_grad_func_contain_y: bool = True,
166
+ require_grad: bool = False,
167
+ output_grad: bool = False,
168
+ fixed_atom_tensor: Optional[th.Tensor] = None,
169
+ batch_indices: None | List[int] | Tuple[int, ...] | th.Tensor = None,
170
+ ) -> Tuple[th.Tensor, th.Tensor] | Tuple[th.Tensor, th.Tensor, th.Tensor]:
171
+ """
172
+ Run the Optimization Algorithm.
173
+
174
+ Args:
175
+ func: the main function of instantiated torch.nn.Module class.
176
+ X: Tensor[n_batch, n_atom, 3], the atom coordinates that input to func.
177
+ 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).
178
+ func_args: optional, other input of func.
179
+ func_kwargs: optional, other input of func.
180
+ grad_func_args: optional, other input of grad_func.
181
+ grad_func_kwargs: optional, other input of grad_func.
182
+ is_grad_func_contain_y: bool, if True, grad_func contains output of func followed by X
183
+ i.e., grad = grad_func(X, y, *grad_func_args, **grad_func_kwargs), else grad = grad_func(X, *grad_func_args, **grad_func_kwargs)
184
+ require_grad: bool, if True, autograd will be turned on for func(X, *func_args, **func_kwargs) calculation.
185
+ output_grad: bool, whether output gradient of last step.
186
+ fixed_atom_tensor: Optional[th.Tensor], the indices of X that fixed.
187
+ batch_indices: Sequence | th.Tensor | np.ndarray | None, the split points for given X, Element_list & V_init, must be 1D integer array_like.
188
+ the format of batch_indices is the same as `split_size_or_sections` in torch.split:
189
+ 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]
190
+
191
+ Returns:
192
+ min func: Tensor(n_batch, ), the minimum of func.
193
+ argmin func: Tensor(X.shape), the X corresponds to min func.
194
+ grad of argmin func: Tensor(X.shape), only output when `output_grad` == True. The gradient of X corresponding to minimum.
195
+ """
196
+ t_main = time.perf_counter()
197
+ if func_kwargs is None:
198
+ func_kwargs = dict()
199
+ if grad_func_kwargs is None:
200
+ grad_func_kwargs = dict()
201
+ # Check batch indices; irregular batch
202
+ if isinstance(X, th.Tensor):
203
+ if len(X.shape) == 2:
204
+ X = X.unsqueeze(0)
205
+ elif len(X.shape) != 3:
206
+ raise ValueError(f'`X` must be 2D or 3D, but got shape [{X.shape}]')
207
+ n_batch, n_atom, n_dim = X.shape
208
+ else:
209
+ raise TypeError(f'`X` must be torch.Tensor, but occurred {type(X)}.')
210
+ if batch_indices is not None:
211
+ if n_batch != 1:
212
+ raise RuntimeError(f'If batch_indices was specified, the 1st dimension of X must be 1 instead of {n_batch}.')
213
+ if isinstance(batch_indices, (th.Tensor, np.ndarray)):
214
+ batch_indices = batch_indices.tolist()
215
+ elif not isinstance(batch_indices, (Tuple, List)):
216
+ raise TypeError(f'Invalid type of batch_indices {type(batch_indices)}. '
217
+ f'It must be Sequence[int] | th.Tensor | np.ndarray | None')
218
+ for i in batch_indices: assert isinstance(i, int), f'All elements in batch_indices must be int, but occurred {type(i)}'
219
+ n_true_batch = len(batch_indices) # the true batch size for irregular batches
220
+ batch_slice_indx = [0] + list(accumulate(batch_indices)) # convert n_atom of each batch into split point of each batch
221
+ self.batch_tensor = th.as_tensor(batch_indices, device=self.device)
222
+ self.batch_scatter = th.repeat_interleave(
223
+ th.arange(0, len(batch_indices), dtype=th.int64, device=self.device),
224
+ self.batch_tensor,
225
+ dim=0
226
+ )
227
+ # steplength
228
+ steplength_tensor = th.full(
229
+ (1, n_true_batch, 1), fill_value=self.steplength, device=self.device, dtype=th.float32
230
+ ) # (n_batch, sumN, 1), initial step length
231
+ else:
232
+ n_true_batch = n_batch
233
+ # steplength
234
+ steplength_tensor = th.full(
235
+ (n_batch, 1, 1), fill_value=self.steplength, device=self.device, dtype=th.float32
236
+ ) # (n_batch, sumN, 1), initial step length
237
+ # initialize vars
238
+ self.n_true_batch = n_true_batch
239
+ maxiter = self.maxiter
240
+ n_batch, n_atom, n_dim = X.shape
241
+ self.n_batch, self.n_atom, self.n_dim = n_batch, n_atom, n_dim
242
+ p = th.zeros_like(X) # like X, the previous direction
243
+ self.converge_mask = None # (n_true_batch, )
244
+ X_grad_old = th.full_like(X, 1e-20, dtype=th.float32, device=self.device) # like X, initial old grad.
245
+ displace = th.full_like(X_grad_old, 0.) # like X, the X displacement
246
+ # handle grad func
247
+ if grad_func is None:
248
+ is_grad_func_contain_y = True
249
+ require_grad = True
250
+ def grad_func_(y, x, grad_shape=None):
251
+ if grad_shape is None:
252
+ grad_shape = th.ones_like(y)
253
+ _g = th.autograd.grad(y, x, grad_shape)
254
+ return _g[0]
255
+ else:
256
+ grad_func_ = grad_func
257
+ self._line_search.require_grad = require_grad # set linear search
258
+ # Selective dyamics
259
+ if fixed_atom_tensor is None:
260
+ atom_masks = th.ones_like(X, device=self.device)
261
+ elif fixed_atom_tensor.shape == X.shape:
262
+ atom_masks = fixed_atom_tensor.to(self.device)
263
+ else:
264
+ raise RuntimeError(f'The shape of fixed_atom_tensor (shape: {fixed_atom_tensor.shape}) does not match X (shape: {X.shape}).')
265
+ #atom_masks = atom_masks.flatten(-2, -1).unsqueeze(-1) # (n_batch, n_atom*n_dim, 1)
266
+ # other check
267
+ if (not isinstance(maxiter, int)) or (maxiter <= 0):
268
+ raise ValueError(f'Invalid value of maxiter: {maxiter}. It would be an integer greater than 0.')
269
+
270
+ # set variables device
271
+ func = preload_func(func, self.device)
272
+
273
+ if isinstance(grad_func_, nn.Module):
274
+ grad_func_ = grad_func_.to(self.device)
275
+ X = X.detach()
276
+ X = X.to(self.device)
277
+
278
+ # initialize
279
+ ############################## BATCHED ALGORITHM ###################################
280
+ # variables with '_' refer to the dynamically changed variables during iteration,
281
+ # and they will in-placed copy into origin variables (i.e., without '_') at the end
282
+ # of each iteration to update data.
283
+ #
284
+ ####################################################################################
285
+ is_main_loop_converge = False
286
+ t_st = time.perf_counter()
287
+ # Section: initialize
288
+ #ptlist = [X[:, None, :, 0].numpy(force=True)] # for converged samp, stop calc., test <<<
289
+ if self.verbose:
290
+ self.logger.info('-' * 100)
291
+ self.logger.info(f'Iteration Scheme: {self.iterform}')
292
+ self.logger.info('-' * 100)
293
+ # MAIN LOOP
294
+ with th.no_grad():
295
+ with th.set_grad_enabled(require_grad):
296
+ X.requires_grad_(require_grad)
297
+ energies: th.Tensor = func(X, *func_args, **func_kwargs)
298
+ # note: irregular tensor regularized by concat. thus n_batch of X shown as 1, but y has shape of the true batch size.
299
+ if energies.shape[0] != self.n_batch:
300
+ if batch_indices is None:
301
+ raise ValueError(
302
+ f"batch indices is None "
303
+ f"while shape of model output ({energies.shape}) does not match batch size ({self.n_batch})."
304
+ )
305
+ if energies.shape[0] != n_true_batch:
306
+ raise ValueError(f"shape of output ({energies.shape}) does not match given batch indices")
307
+ self.is_concat_X = (batch_indices is not None)
308
+ # calc. grad
309
+ if is_grad_func_contain_y:
310
+ X_grad = grad_func_(energies, X, *grad_func_args, **grad_func_kwargs)
311
+ else:
312
+ X_grad = grad_func_(X, *grad_func_args, **grad_func_kwargs)
313
+ if X_grad.shape != X.shape:
314
+ raise RuntimeError(f'X_grad ({X_grad.shape}) and X ({X.shape}) have different shapes.')
315
+ energies = energies.detach()
316
+ energies_old = th.full_like(energies, th.inf)
317
+ X_grad = X_grad.detach()
318
+ X_grad.mul_(atom_masks)
319
+ X = X.detach()
320
+ # Section: initialize custom algorithm state.
321
+ self.initialize_algo_param()
322
+ # cache for dynamically changed batch indices due to convergence, avoiding reallocate mem.
323
+ batch_tensor_indx_cache = th.arange(0, len(self.batch_tensor), dtype=th.int64, device=self.device)
324
+ for numit in range(maxiter):
325
+ # Calc. Criteria
326
+ E_diff = energies - energies_old
327
+ E_eps = th.abs(E_diff) # (n_batch, )
328
+ energies_old.copy_(energies)
329
+ # manage the irregular tensors
330
+ if self.is_concat_X:
331
+ # (1, n_batch*n_atom, 3)
332
+ F_eps = index_reduce(
333
+ th.max(th.abs(X_grad[0]), dim=-1).values, self.batch_scatter, 0, 'amax', -1.
334
+ )
335
+ f_converge = F_eps < self.F_threshold
336
+ converge_mask = (E_eps < self.E_threshold) & f_converge # (n_true_batch, ), to stop the update of converged samples.
337
+ converge_check = converge_mask
338
+ self.converge_mask = converge_check
339
+ converge_str = converge_check.numpy(force=True)
340
+ converge_mask = converge_mask.reshape(1, -1, 1)[:, self.batch_scatter, ...] # (1, n_batch*n_atom, 3)
341
+ else:
342
+ F_eps = th.amax(th.abs(X_grad), dim=(-2, -1)) # (n_batch, n_atom, 3) -> (n_batch)
343
+ f_converge = (F_eps < self.F_threshold).reshape(-1, 1, 1)
344
+ converge_mask = (E_eps < self.E_threshold).reshape(-1, 1, 1) & f_converge # To stop the update of converged samples.
345
+ converge_check = converge_mask[:, 0, 0]
346
+ self.converge_mask = converge_check
347
+ converge_str = (converge_mask[:, 0, 0]).numpy(force=True)
348
+
349
+ # Print information / Verbose
350
+ if self.verbose > 0:
351
+ self.logger.info(f"ITERATION {numit:>5d}\n "
352
+ f"MAD_energies: {np.array2string(E_diff.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
353
+ f"MAX_F: {np.array2string(F_eps.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
354
+ f"Energies: {np.array2string(energies.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
355
+ f"Converged: {np.array2string(converge_str, **STRING_ARRAY_FORMAT)}\n "
356
+ f"TIME: {time.perf_counter() - t_st:>6.4f} s")
357
+ if self.verbose > 1:
358
+ # split batches if specified batch
359
+ if batch_indices is not None:
360
+ X_np = X.numpy(force=True)
361
+ X_tup = np.split(X_np, batch_slice_indx[1:-1], axis=1)
362
+ if self.verbose > 2:
363
+ F_np = (- X_grad).numpy(force=True)
364
+ F_tup = np.split(F_np, batch_slice_indx[1:-1], axis=1)
365
+ else:
366
+ X_tup = (X.numpy(force=True),)
367
+ if self.verbose > 2:
368
+ F_tup = (- X_grad.numpy(force=True),)
369
+ self.logger.info(f" Coordinates:\n")
370
+ X_str = [
371
+ np.array2string(xi, **FLOAT_ARRAY_FORMAT).replace("[", " ").replace("]", " ")
372
+ for xi in X_tup
373
+ ]
374
+ [self.logger.info(f'{x_str}\n') for x_str in X_str]
375
+ if self.verbose > 2:
376
+ self.logger.info(f" Forces:\n")
377
+ X_str = [
378
+ np.array2string(xi, **FLOAT_ARRAY_FORMAT).replace("[", " ").replace("]", " ")
379
+ for xi in F_tup
380
+ ]
381
+ [self.logger.info(f'{x_str}\n') for x_str in X_str]
382
+
383
+ # judge thres
384
+ if th.all(self.converge_mask):
385
+ is_main_loop_converge = True
386
+ break
387
+
388
+ #g: th.Tensor = th.flatten(X_grad, 1, 2).unsqueeze(-1).contiguous() # (n_batch, n_atom*3, 1)
389
+ # Section: update batch
390
+ if not self._hold_samples:
391
+ func_args_, func_kwargs_, grad_func_args_, grad_func_kwargs_ = self._update_batch(
392
+ ~converge_check,
393
+ func_args,
394
+ func_kwargs,
395
+ grad_func_args,
396
+ grad_func_kwargs
397
+ )
398
+ if self.is_concat_X:
399
+ select_mask = ~(converge_mask[0, :, 0])
400
+ select_mask_short = ~converge_check
401
+ energies_ = energies[select_mask_short]
402
+ X_grad_ = X_grad[:, select_mask, :]
403
+ X_grad_old_ = X_grad_old[:, select_mask, :]
404
+ p_ = p[:, select_mask, :]
405
+ X_ = X[:, select_mask, :]
406
+ displace_ = displace[:, select_mask, :]
407
+ atom_masks_ = atom_masks[:, select_mask, :]
408
+ steplength_ = steplength_tensor[:, select_mask_short, :]
409
+ batch_tensor_ = self.batch_tensor[select_mask_short]
410
+ batch_scatter_ = th.repeat_interleave(
411
+ batch_tensor_indx_cache[:len(batch_tensor_)],
412
+ batch_tensor_,
413
+ dim=0
414
+ )
415
+ else:
416
+ select_mask = ~converge_check
417
+ energies_ = energies[select_mask]
418
+ X_grad_ = X_grad[select_mask, ...]
419
+ X_grad_old_ = X_grad_old[select_mask, ...]
420
+ p_ = p[select_mask, ...]
421
+ X_ = X[select_mask, ...]
422
+ atom_masks_ = atom_masks[select_mask, ...]
423
+ displace_ = displace[select_mask, ...]
424
+ steplength_ = steplength_tensor[select_mask, ...]
425
+ batch_tensor_ = None
426
+ batch_scatter_ = None
427
+ else:
428
+ func_args_, func_kwargs_, grad_func_args_, grad_func_kwargs_ = (
429
+ func_args,
430
+ func_kwargs,
431
+ grad_func_args,
432
+ grad_func_kwargs
433
+ )
434
+ select_mask = ~(converge_mask[0, :, 0])
435
+ select_mask_short = ~converge_check
436
+ energies_ = energies
437
+ X_grad_ = X_grad
438
+ X_grad_old_ = X_grad_old
439
+ p_ = p
440
+ X_ = X
441
+ displace_ = displace
442
+ atom_masks_ = atom_masks
443
+ steplength_ = steplength_tensor
444
+ batch_tensor_ = self.batch_tensor
445
+ batch_scatter_ = self.batch_scatter
446
+
447
+ # Section: update algo. parameters.
448
+ self._update_algo_param(
449
+ select_mask,
450
+ select_mask_short,
451
+ batch_scatter_,
452
+ X_grad_,
453
+ X_grad_old_,
454
+ p_,
455
+ displace_
456
+ )
457
+ self.select_mask = select_mask
458
+
459
+ t_st = time.perf_counter()
460
+ # Section: search directions
461
+ p_ = self._update_direction(
462
+ X_grad_,
463
+ X_grad_old_,
464
+ p_,
465
+ X_,
466
+ batch_scatter_,
467
+ ) # (n_batch, n_atom, n_dim)
468
+ # use BB steplength_tensor
469
+ if self.use_bb:
470
+ g_go = X_grad_ - X_grad_old_ # (n_batch, n_atom, n_dim)
471
+ if self.is_concat_X:
472
+ _steplength_ = th.sum(index_inner_product(
473
+ displace_,
474
+ displace_,
475
+ dim=1,
476
+ batch_indices=batch_scatter_
477
+ ), dim=-1, keepdim=True) / th.sum(index_inner_product(
478
+ displace_,
479
+ g_go,
480
+ dim=1,
481
+ batch_indices=batch_scatter_
482
+ ), dim=-1, keepdim=True) # BB1, (1, B, 1)
483
+ _steplength_ = th.where(
484
+ (_steplength_ < 2. * self.steplength) & (_steplength_ > 1e-4),
485
+ _steplength_,
486
+ steplength_
487
+ )
488
+ else:
489
+ # (n_batch, 1, n_atom*n_dim) @ (n_batch, n_atom*n_dim, 1) =
490
+ _steplength_ = th.sum(
491
+ displace_ * displace_, dim=(-2, -1), keepdim=True
492
+ ) / th.sum(
493
+ displace_ * g_go, dim=(-2, -1), keepdim=True
494
+ ) # BB1
495
+ _steplength_ = th.where(
496
+ (_steplength_ < 2. * self.steplength) * (_steplength_ > 1e-4),
497
+ _steplength_,
498
+ steplength_
499
+ )
500
+ else:
501
+ _steplength_ = steplength_
502
+ # Section: search step length -> steplength_tensor: (n_batch, 1, 1)
503
+ steplength_: th.Tensor = self._line_search.run(
504
+ func,
505
+ grad_func_,
506
+ X_,
507
+ energies_,
508
+ X_grad_,
509
+ p_,
510
+ _steplength_,
511
+ is_grad_func_contain_y,
512
+ require_grad,
513
+ func_args=func_args_,
514
+ func_kwargs=func_kwargs_,
515
+ grad_func_args=grad_func_args_,
516
+ grad_func_kwargs=grad_func_kwargs_,
517
+ batch_indices=batch_tensor_
518
+ )
519
+ # update X
520
+ if self.is_concat_X:
521
+ alpha = steplength_.index_select(1, batch_scatter_)
522
+ else:
523
+ alpha = steplength_
524
+ displace_ = alpha * p_ # (n_batch, 1, 1) * (n_batch, n_atom, n_dim) or (1, sumN, 1) * (1, sumN, n_dim)
525
+ X_.add_(displace_) # (n_batch, n_atom, 3) + (n_batch, n_atom, 3)
526
+ # update old grad
527
+ X_grad_old_ = X_grad_ # (n_batch, n_atom, n_dim)
528
+ # calc. new energy & grad.
529
+ with th.set_grad_enabled(require_grad):
530
+ X_.requires_grad_(require_grad)
531
+ energies_: th.Tensor = func(X_, *func_args_, **func_kwargs_)
532
+ if is_grad_func_contain_y:
533
+ X_grad_ = grad_func_(energies_, X_, *grad_func_args_, **grad_func_kwargs_)
534
+ else:
535
+ X_grad_ = grad_func_(X_, *grad_func_args_, **grad_func_kwargs_)
536
+ energies_ = energies_.detach()
537
+ X_grad_ = X_grad_.detach()
538
+ X_grad_.mul_(atom_masks_)
539
+ X_.detach_()
540
+
541
+ # Section: rewrite. update origin variables
542
+ if not self._hold_samples:
543
+ if self.is_concat_X:
544
+ select_indices = th.where(select_mask)[0]
545
+ select_indices_short = th.where(select_mask_short)[0]
546
+ energies.index_copy_(0, select_indices_short, energies_)
547
+ X_grad.index_copy_(1, select_indices, X_grad_)
548
+ X_grad_old.index_copy_(1, select_indices, X_grad_old_)
549
+ p.index_copy_(1, select_indices, p_)
550
+ X.index_copy_(1, select_indices, X_)
551
+ displace.index_copy_(1, select_indices, displace_)
552
+ #atom_masks.index_copy_(1, select_indices, atom_masks_)
553
+ #steplength_tensor.index_copy_(1, select_indices_short, steplength_)
554
+
555
+ else:
556
+ select_indices = th.where(select_mask)[0]
557
+ energies.index_copy_(0, select_indices, energies_)
558
+ X_grad.index_copy_(0, select_indices, X_grad_)
559
+ X_grad_old.index_copy_(0, select_indices, X_grad_old_)
560
+ p.index_copy_(0, select_indices, p_)
561
+ X.index_copy_(0, select_indices, X_)
562
+ displace.index_copy_(0, select_indices, displace_)
563
+ #atom_masks.index_copy_(0, select_indices, atom_masks_)
564
+ #steplength_tensor.index_copy_(0, select_indices, steplength_)
565
+ else:
566
+ #select_indices = th.where(select_mask)[0]
567
+ #select_indices_short = th.where(select_mask_short)[0]
568
+ energies = energies_
569
+ X_grad = X_grad_
570
+ X_grad_old = X_grad_old_
571
+ p = p_
572
+ X = X_
573
+ displace = displace_
574
+ # Section: update batch information of algos if necessary
575
+ self._update_algo_batches(select_indices, select_indices_short)
576
+ # Check NaN
577
+ #if not th.all(energies.isfinite()): raise RuntimeError(f'NaN Occurred in output: {energies}')
578
+
579
+ #ptlist.append(X[:, None, :, 0].numpy(force=True)) # test <<<
580
+
581
+ if self.verbose > 0:
582
+ if is_main_loop_converge:
583
+ self.logger.info(
584
+ '-' * 100 + f'\nAll Structures were Converged.\nMAIN LOOP Done. Total Time: {time.perf_counter() - t_main:<.4f} s\n'
585
+ )
586
+ else:
587
+ self.logger.info(
588
+ '-' * 100 + f'\nSome Structures were NOT Converged yet!\nMAIN LOOP Done. Total Time: {time.perf_counter() - t_main:<.4f} s\n'
589
+ )
590
+
591
+ if self.verbose < 2: # verbose = 1, brief mode only output last step coords.
592
+ # split batches if specified batch
593
+ if batch_indices is not None:
594
+ X_np = X.numpy(force=True)
595
+ X_tup = np.split(X_np, batch_slice_indx[1:-1], axis=1)
596
+ F_np = (- X_grad).numpy(force=True)
597
+ F_tup = np.split(F_np, batch_slice_indx[1:-1], axis=1)
598
+ else:
599
+ X_tup = (X.numpy(force=True),)
600
+ F_tup = (- X_grad.numpy(force=True),)
601
+ self.logger.info(f" Final Coordinates:\n")
602
+ X_str = [
603
+ np.array2string(xi, **FLOAT_ARRAY_FORMAT).replace("[", " ").replace("]", " ")
604
+ for xi in X_tup
605
+ ]
606
+ [self.logger.info(f'{x_str}\n') for x_str in X_str]
607
+ self.logger.info(f" Final Forces:\n")
608
+ X_str = [
609
+ np.array2string(xi, **FLOAT_ARRAY_FORMAT).replace("[", " ").replace("]", " ")
610
+ for xi in F_tup
611
+ ]
612
+ [self.logger.info(f'{x_str}\n') for x_str in X_str]
613
+ else:
614
+ if not is_main_loop_converge: warnings.warn('Some Structures were NOT Converged yet!',
615
+ NotConvergeWarning)
616
+ # release
617
+
618
+ # output
619
+ if output_grad:
620
+ return energies, X, X_grad
621
+ else:
622
+ return energies, X #, ptlist # test <<<
623
+
624
+ @abstractmethod
625
+ def initialize_algo_param(self):
626
+ """
627
+ Override this method to initialize attribute variables for self._update_direction.
628
+ Examples:
629
+ (BFGS algo.)
630
+ # descent direction
631
+ self.p = 0.
632
+ # Initial quasi-inverse Hessian Matrix (n_batch, n_atom*n_dim, n_atom*n_dim)
633
+ self.H_inv = (th.eye(n_atom * n_dim, device=self.device).unsqueeze(0)).expand(n_batch, -1, -1)
634
+ # prepared identity matrix
635
+ self.Ident = (th.eye(n_atom * n_dim, device=self.device).unsqueeze(0)).expand(n_batch, -1, -1)
636
+
637
+ Returns: None
638
+ """
639
+ raise NotImplementedError
640
+
641
+ @abstractmethod
642
+ def _update_direction(
643
+ self,
644
+ g: th.Tensor,
645
+ g_old: th.Tensor,
646
+ p: th.Tensor,
647
+ X: th.Tensor,
648
+ batch_scatter_indices: th.Tensor | None,
649
+ ) -> th.Tensor:
650
+ """
651
+ Override this method to implement X update algorithm.
652
+ Args:
653
+ g: (n_batch, n_atom, n_dim), the gradient of X at this step
654
+ g_old: (n_batch, n_atom, n_dim), the gradient of X at last step
655
+ p: (n_batch, n_atom, n_dim), the update direction of X at last step
656
+ X: (n_batch, n_atom, n_dim), the independent vars X.
657
+ batch_scatter_indices: the batch indices. See `_update_algo_param`.
658
+
659
+ Returns:
660
+ p: th.Tensor, the new update direction of X.
661
+ """
662
+ raise NotImplementedError
663
+
664
+ @abstractmethod
665
+ def _update_algo_param(
666
+ self,
667
+ select_mask: th.Tensor,
668
+ select_mask_short: th.Tensor | None,
669
+ batch_scatter_indices: th.Tensor | None,
670
+ g: th.Tensor,
671
+ g_old: th.Tensor,
672
+ p: th.Tensor,
673
+ displace: th.Tensor
674
+ ) -> None:
675
+ """
676
+ Override this method to update the parameters of X update algorithm i.e., self.iterform.
677
+ Args:
678
+ select_mask: (n_batch, ), the mask of batch that converged. Only the position of `True` would be selected to calculate.
679
+ batch_scatter_indices: (sumN, ), the batch scatter indices of sample that not yet converged.
680
+ format: [0, 0, ..., 0, 1, 1, ..., 1, ..., N-1], where the same number means the line in the same sample.
681
+ If samples are in a regular batch, it would be set to None.
682
+ g: (n_batch, n_atom, n_dim), the gradient of X at this step
683
+ g_old: (n_batch, n_atom, n_dim), the gradient of X at last step
684
+ p: (n_batch, n_atom, n_dim), the update direction of X at last step
685
+ displace: (n_batch, n_atom, n_dim), the displacement of X at this step. displace = step-length * p
686
+
687
+ Returns: None
688
+ """
689
+ raise NotImplementedError
690
+
691
+ def _update_algo_batches(
692
+ self,
693
+ select_indices: th.Tensor,
694
+ select_indices_short: th.Tensor | None,
695
+ ):
696
+ """
697
+ Optional.
698
+ Override this method to update the batch information of algorithm parameters, i.e., self.iterform.
699
+ Returns:
700
+
701
+ """
702
+ pass