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,950 @@
1
+ """ Dimer Algo. for transition state searching. J. Chem. Phys. 1999, 111: 7010. """
2
+ from itertools import accumulate
3
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
4
+ # Authors: Pu Pengxin, Song Xin
5
+ # Version: 0.9a
6
+ # File: Dimer.py
7
+ # Environment: Python 3.12
8
+
9
+ # ruff: noqa: E701, E702, E703
10
+ from typing import Iterable, Dict, Any, List, Literal, Optional, Callable, Sequence, Tuple # noqa: F401
11
+ import time
12
+ import warnings
13
+ import logging
14
+ import sys
15
+ import os
16
+
17
+ import numpy as np
18
+ import torch as th
19
+ from torch import nn
20
+
21
+ from BUCToolkit.utils._print_formatter import GLOBAL_SCIENTIFIC_ARRAY_FORMAT, FLOAT_ARRAY_FORMAT, SCIENTIFIC_ARRAY_FORMAT, STRING_ARRAY_FORMAT
22
+ from BUCToolkit.utils import index_ops
23
+ from BUCToolkit.utils.function_utils import preload_func
24
+ from BUCToolkit.utils.setup_loggers import has_any_handler, clear_all_handlers, BaseIO
25
+
26
+ np.set_printoptions(**GLOBAL_SCIENTIFIC_ARRAY_FORMAT)
27
+
28
+ def fin_diff_hvp(
29
+ f: Callable,
30
+ f_args: Tuple,
31
+ f_kwargs: None | Dict,
32
+ g: Callable,
33
+ g_args: Tuple,
34
+ g_kwargs: None | Dict,
35
+ X: th.Tensor,
36
+ v: th.Tensor,
37
+ batch_indices: th.Tensor|None = None,
38
+ is_g_contain_y: bool = False,
39
+ require_grad: bool = False,
40
+ normalize_v: bool = True,
41
+ delta: float = 1e-2,
42
+ eps: float = 1e-20,
43
+ ) -> Tuple[th.Tensor, th.Tensor, th.Tensor]:
44
+ """
45
+ Solve the Hessian vector product (for f = \nabla func) by finite difference.
46
+ Args:
47
+ f: the function
48
+ f_args: positional arguments of f
49
+ f_kwargs: keyword arguments of f
50
+ g: gradient function
51
+ g_args: positional arguments of g
52
+ g_kwargs: keyword arguments of g
53
+ X: the input tensor of f & g
54
+ v: the output tensor of f & g
55
+ batch_indices: the batched indices of X & v with format [0, 0, 0, ..., 1, 1, ..., n_batch - 1]
56
+ is_g_contain_y: whether the grad function requires function value.
57
+ require_grad: whether to require gradient.
58
+ normalize_v: whether to normalize v to ||v|| = 1.
59
+ delta: the step size for finite difference approximation
60
+ eps: a small number to avoid division by zero
61
+
62
+ Returns: y_mean, g_mean, hvp
63
+ y_mean: th.Tensor, the function value of (y+ + y-) / 2
64
+ g_mean: th.Tensor, the function value of (g+ + g-) / 2
65
+ hvp: th.Tensor, the finite difference solution.
66
+
67
+ """
68
+ if f_kwargs is None:
69
+ f_kwargs = dict()
70
+ #n_batch, n_atom, n_dim = X.shape
71
+ if batch_indices is None:
72
+ v_norm = (th.sqrt(th.sum(v * v, dim=-1, keepdim=True)) + eps)
73
+ v = v / v_norm # (B, A, D)
74
+ dX_f = X + delta * v
75
+ dX_b = X - delta * v
76
+
77
+ else:
78
+ # X, v: (1, B0*A, D)
79
+ v_norm = th.sqrt(th.sum(index_ops.index_inner_product(v, v, 1, batch_indices, None), dim=-1, keepdim=True)) # (1, B0, 1)
80
+ v = v / v_norm.index_select(1, batch_indices)
81
+ dX_f = X + delta * v
82
+ dX_b = X - delta * v
83
+
84
+ with th.set_grad_enabled(require_grad):
85
+ dX_f.requires_grad_(require_grad)
86
+ dX_b.requires_grad_(require_grad)
87
+ if is_g_contain_y:
88
+ y_f = f(dX_f, *f_args, **f_kwargs)
89
+ g_f = g(y_f, dX_f, *g_args, **g_kwargs)
90
+ y_b = f(dX_b, *f_args, **f_kwargs)
91
+ g_b = g(y_b, dX_b, *g_args, **g_kwargs)
92
+ hvp = (g_f - g_b) / (2 * delta)
93
+ else:
94
+ y_f = f(dX_f, *f_args, **f_kwargs)
95
+ g_f = g(dX_f, *g_args, **g_kwargs)
96
+ y_b = f(dX_b, *f_args, **f_kwargs)
97
+ g_b = g(dX_b, *g_args, **g_kwargs)
98
+ hvp = (g_f - g_b) / (2 * delta)
99
+ y_mean = (y_f + y_b).detach() / 2.
100
+ g_mean = (g_f + g_b).detach() / 2.
101
+ if not normalize_v:
102
+ hvp.mul_(v_norm.index_select(1, batch_indices))
103
+
104
+ return y_mean, g_mean, hvp
105
+
106
+
107
+ class FindMinEigen(BaseIO):
108
+ """
109
+ Find the eigenvector with minimum eigenvalue by Riemann gradient descent on S^2 manifold v^T v = 1.
110
+ In fact, dimer only requires the direction within negative cone, i.e., v^T H v < 0.
111
+ """
112
+ def __init__(
113
+ self,
114
+ Torque_thres: float = 1e-2,
115
+ Curve_thres: float = -0.1,
116
+ maxiter_rot: int = 10,
117
+ dx: float = 1.e-2,
118
+ device: str | th.device = 'cpu',
119
+ verbose: int = 2,
120
+ _hold_samples: bool = False,
121
+ ):
122
+ """
123
+
124
+ Args:
125
+ Torque_thres: convergence threshold of torque.
126
+ Curve_thres: convergence threshold of curvature, that v^T H v < `Curve_thres` is viewed as converged.
127
+ maxiter_rot: maximum number of rotation iterations.
128
+ dx: step size for finite difference approximation.
129
+ device: the device on which the computation runs.
130
+ verbose: the verbosity level.
131
+ _hold_samples: whether to hold samples during optimization. if True, samples will not be removed even they have converged.
132
+ """
133
+
134
+ warnings.filterwarnings('always')
135
+ self.Torque_thres = float(Torque_thres)
136
+ self.Curve_thres = float(Curve_thres)
137
+ if self.Curve_thres >= 0.:
138
+ warnings.warn(f'`Curve_thres` should be less than 0 to determind the negative cone direction, but occurred {Curve_thres}.\n'
139
+ f'Now it has been set to its opposite number: {- Curve_thres}.')
140
+ self.Curve_thres = - Curve_thres
141
+ assert (maxiter_rot > 0) and isinstance(maxiter_rot, int), '`maxiter_rot` must be an integer greater than 0.'
142
+ self.maxiter_rot = int(maxiter_rot)
143
+ self.dx = float(dx)
144
+ self.subspace_hessian = None
145
+ self.device = device
146
+ self.verbose = verbose
147
+ self._hold_samples = _hold_samples
148
+
149
+ # logger
150
+ super().__init__()
151
+ self.init_logger('Main.TS')
152
+
153
+ def _update_batch(self, mask: th.Tensor, func_args: Tuple, func_kwargs: Dict, grad_func_args: Tuple, grad_func_kwargs: Dict):
154
+ """
155
+ Default update method for the input of func if the func has non-opt variables, i.e., the identical transform.
156
+ Args:
157
+ mask:
158
+
159
+ Returns:
160
+
161
+ """
162
+ return func_args, func_kwargs, grad_func_args, grad_func_kwargs
163
+
164
+ def set_batch_updater(
165
+ self,
166
+ method: Callable[[th.Tensor, Tuple | None, Dict | None, Tuple | None, Dict | None], Tuple[Tuple, Dict, Tuple, Dict]]
167
+ ) -> None:
168
+ """
169
+ Set a method to update the taget function when variables change.
170
+ It receives a mask tensor of shape (n_batch, ) that only selects the `True` part to input to the function, and receives the old
171
+ `func_args`, `func_kwargs`, `grad_func_args`, and `grad_func_kwargs`,
172
+ returns the corresponding masked new `func_args`, `func_kwargs`, `grad_func_args`, and `grad_func_kwargs`.
173
+
174
+ This method is used to dynamically 'remove' the samples which have been converged in a batch to avoid
175
+ redundant calculation of converged samples.
176
+
177
+ Default transform is identical transform (i.e., do nothing)
178
+ Args:
179
+ method: Callable(mask: Tensor, func_args: Tuple, func_kwargs: Dict, grad_func_args: Tuple, grad_func_kwargs: Dict) -> Tuple[Tuple, Dict, Tuple, Dict],
180
+ the method of updating function arguments for a mask.
181
+
182
+ Returns: None
183
+ """
184
+ self._update_batch = method
185
+ self._hold_samples = False
186
+
187
+ def run(
188
+ self,
189
+ func: Any | nn.Module,
190
+ X: th.Tensor,
191
+ v: th.Tensor,
192
+ grad_func: Any | nn.Module = None,
193
+ func_args: Tuple = tuple(),
194
+ func_kwargs: Dict | None = None,
195
+ grad_func_args: Tuple = tuple(),
196
+ grad_func_kwargs: Dict | None = None,
197
+ is_grad_func_contain_y: bool = True,
198
+ require_grad: bool = False,
199
+ fixed_atom_tensor: Optional[th.Tensor] = None,
200
+ batch_indices: None | List[int] | Tuple[int, ...] | th.Tensor = None,
201
+ ) -> Tuple[th.Tensor, th.Tensor, th.Tensor, th.Tensor, th.Tensor]:
202
+ """
203
+ Find the eigenvector of Hessian at X with the minimum eigenvalue, by Riemannian gradient descent on S^n manifold v^T v = I.
204
+
205
+ Parameters:
206
+ func: the main function of instantiated torch.nn.Module class.
207
+ X: Tensor[n_batch, n_atom, 3], the atom coordinates that input to func.
208
+ v: Tensor[n_batch, n_atom, 3], the atom direction used to finite difference.
209
+ 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).
210
+ func_args: optional, other input of func.
211
+ func_kwargs: optional, other input of func.
212
+ grad_func_args: optional, other input of grad_func.
213
+ grad_func_kwargs: optional, other input of grad_func.
214
+ is_grad_func_contain_y: bool, if True, grad_func contains output of func followed by X
215
+ i.e., grad = grad_func(X, y, *grad_func_args, **grad_func_kwargs), else grad = grad_func(X, *grad_func_args, **grad_func_kwargs)
216
+ require_grad: bool, if True, autograd will be turned on for func(X, *func_args, **func_kwargs) calculation.
217
+ fixed_atom_tensor: Optional[th.Tensor], the indices of X that fixed.
218
+ batch_indices: Sequence | th.Tensor | np.ndarray | None, the split points for given X, Element_list & V_init, must be 1D integer array_like.
219
+ the format of batch_indices is the same as `split_size_or_sections` in torch.split:
220
+ 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]
221
+
222
+ Return:
223
+ v: the eigenvector with min eigenvalue at X (or at least the negative cone direction)
224
+ y: the mean value of function at X, i.e., (f(X + delta * v) + f(X - delta * v))/2
225
+ g: the mean grad of function at X, i.e., (grad(X + delta * v) + grad(X - delta * v))/2
226
+ vHv: the curvature at X given by finite difference.
227
+ """
228
+ t_main = time.perf_counter()
229
+ if func_kwargs is None:
230
+ func_kwargs = dict()
231
+ if grad_func_kwargs is None:
232
+ grad_func_kwargs = dict()
233
+ # Check batch indices; irregular batch
234
+ if isinstance(X, th.Tensor):
235
+ n_batch, n_atom, n_dim = X.shape
236
+ else:
237
+ raise TypeError(f'`X` must be torch.Tensor, but occurred {type(X)}.')
238
+ if batch_indices is not None:
239
+ if n_batch != 1:
240
+ raise RuntimeError(f'If batch_indices was specified, the 1st dimension of X must be 1 instead of {n_batch}.')
241
+ if isinstance(batch_indices, (th.Tensor, np.ndarray)):
242
+ batch_indices = batch_indices.tolist()
243
+ elif not isinstance(batch_indices, (Tuple, List)):
244
+ raise TypeError(f'Invalid type of batch_indices {type(batch_indices)}. '
245
+ f'It must be Sequence[int] | th.Tensor | np.ndarray | None')
246
+ for i in batch_indices: assert isinstance(i, int), f'All elements in batch_indices must be int, but occurred {type(i)}'
247
+ batch_slice_indx = [0] + list(accumulate(batch_indices)) # convert n_atom of each batch into split point of each batch
248
+ n_inner_batch = len(batch_indices)
249
+ self.batch_tensor = th.as_tensor(batch_indices, device=self.device)
250
+ self.batch_scatter = th.repeat_interleave(
251
+ th.arange(0, len(batch_indices), dtype=th.int64, device=self.device),
252
+ self.batch_tensor,
253
+ dim=0
254
+ )
255
+ else:
256
+ raise NotImplementedError
257
+ n_inner_batch = 1
258
+ batch_indx_dict = dict()
259
+ batch_tensor = None
260
+ # initialize vars
261
+ n_batch, n_atom, n_dim = X.shape
262
+ self.n_batch, self.n_atom, self.n_dim = n_batch, n_atom, n_dim
263
+ if grad_func is None:
264
+ is_grad_func_contain_y = True
265
+ require_grad = True
266
+
267
+ def grad_func_(y, x, grad_shape=None):
268
+ if grad_shape is None:
269
+ grad_shape = th.ones_like(y)
270
+ _g = th.autograd.grad(y, x, grad_shape)
271
+ return _g[0]
272
+ else:
273
+ grad_func_ = grad_func
274
+
275
+ if hasattr(self._update_batch, 'initialize'):
276
+ self._update_batch.initialize()
277
+ elif hasattr(self._update_batch, '__init__'):
278
+ self._update_batch.__init__()
279
+ # Selective dynamics
280
+ if fixed_atom_tensor is None:
281
+ atom_masks = th.ones_like(X, device=self.device)
282
+ elif fixed_atom_tensor.shape == X.shape:
283
+ atom_masks = fixed_atom_tensor.to(self.device)
284
+ else:
285
+ raise RuntimeError(f'The shape of fixed_atom_tensor (shape: {fixed_atom_tensor.shape}) does not match X (shape: {X.shape}).')
286
+ # atom_masks = atom_masks.flatten(-2, -1).unsqueeze(-1) # (n_batch, n_atom*n_dim, 1)
287
+ # other check
288
+ if (not isinstance(self.maxiter_rot, int)) or (self.maxiter_rot <= 0):
289
+ raise ValueError(f'Invalid value of maxiter_rot: {self.maxiter_rot}. It would be an integer greater than 0.')
290
+
291
+ # set variables device
292
+ func = preload_func(func, self.device)
293
+
294
+ if isinstance(grad_func_, nn.Module):
295
+ grad_func_ = grad_func_.to(self.device)
296
+ X = X.detach()
297
+ X = X.to(self.device)
298
+ # normalize v
299
+ v.mul_(atom_masks)
300
+ v_norm = th.sqrt(th.sum(index_ops.index_inner_product(v, v, 1, self.batch_scatter), dim=-1, keepdim=True))
301
+ v = v / v_norm.index_select(1, self.batch_scatter)
302
+
303
+ # initialize
304
+ ############################## BATCHED ALGORITHM ###################################
305
+ # variables with '_' refer to the dynamically changed variables during iteration,
306
+ # and they will in-placed copy into origin variables (i.e., without '_') at the end
307
+ # of each iteration to update data.
308
+ #
309
+ ####################################################################################
310
+ is_main_loop_converge = False
311
+ t_st = time.perf_counter()
312
+ #ptlist = [X[:, None, :, 0].numpy(force=True)] # for converged samp, stop calc., test <<<
313
+ if self.verbose:
314
+ self.logger.info('-' * 100)
315
+ # MAIN LOOP
316
+ # X (1, n_batch * n_atom, n_dim)
317
+ theta = th.zeros(n_inner_batch, device=self.device)
318
+ func_args_, func_kwargs_, grad_func_args_, grad_func_kwargs_ = func_args, func_kwargs, grad_func_args, grad_func_kwargs
319
+ y, g, Hv = fin_diff_hvp(
320
+ func,
321
+ func_args_,
322
+ func_kwargs_,
323
+ grad_func_,
324
+ grad_func_args_,
325
+ grad_func_kwargs_,
326
+ X,
327
+ v,
328
+ self.batch_scatter,
329
+ is_g_contain_y=is_grad_func_contain_y,
330
+ require_grad=require_grad,
331
+ delta=self.dx
332
+ )
333
+ Hv.mul_(atom_masks)
334
+ # curvature, vHv (1, B0, 1)
335
+ vHv = th.sum(
336
+ index_ops.index_inner_product(v, Hv, dim=1, batch_indices=self.batch_scatter),
337
+ dim=-1,
338
+ keepdim=True
339
+ )
340
+ # grad in the tangent space
341
+ gT = Hv - vHv.index_select(1, self.batch_scatter) * v
342
+ gT_norm = th.sqrt(th.sum(index_ops.index_inner_product(gT, gT, 1, self.batch_scatter), dim=-1, keepdim=True))
343
+ w = gT / (gT_norm.index_select(1, self.batch_scatter) + 1e-20) # (1, sumB*A, N)
344
+ # cache for dynamically changed batch indices due to convergence, avoiding reallocate mem.
345
+ batch_tensor_indx_cache = th.arange(0, len(self.batch_tensor), dtype=th.int64, device=self.device)
346
+ for i in range(self.maxiter_rot):
347
+ # threshold. Only need v in the negative cone, i.e., vHv < 0.
348
+ converge_mask_curve = (vHv < self.Curve_thres)
349
+ converge_mask_torque = (gT_norm < self.Torque_thres)
350
+ converge_mask = converge_mask_curve | converge_mask_torque # (1, B, 1)
351
+ # print
352
+ if self.verbose > 0:
353
+ self.logger.info(f"Rot {i:>5d}\n "
354
+ f"Torque: {np.array2string(gT_norm.squeeze().numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
355
+ f"Curvature: {np.array2string(vHv.squeeze().numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
356
+ f"Energies: {np.array2string(y.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
357
+ f"Theta (rad): {np.array2string(theta.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
358
+ f"Rot. Conv.: {np.array2string(converge_mask.squeeze().numpy(force=True), **STRING_ARRAY_FORMAT)}\n "
359
+ f"TIME: {time.perf_counter() - t_st:>6.4f} s")
360
+ t_st = time.perf_counter()
361
+ # judge thres
362
+ if th.all(converge_mask):
363
+ is_main_loop_converge = True
364
+ break
365
+ converge_mask_short = converge_mask
366
+ converge_mask = converge_mask[:, self.batch_scatter, ...] # (1, sumB*A, 1)
367
+ # update batch, remove the already converged ones.
368
+ if not self._hold_samples:
369
+ func_args_, func_kwargs_, grad_func_args_, grad_func_kwargs_ = self._update_batch(
370
+ ~converge_mask_short.squeeze(),
371
+ func_args,
372
+ func_kwargs,
373
+ grad_func_args,
374
+ grad_func_kwargs
375
+ )
376
+ select_mask = ~(converge_mask[0, :, 0]) # (sumB*A, )
377
+ select_mask_short = ~converge_mask_short[0, :, 0] # (B, )
378
+ #y_ = y[select_mask_short]
379
+ vHv_ = vHv[:, select_mask_short, :]
380
+ Hv_ = Hv[:, select_mask, :]
381
+ X_ = X[:, select_mask, :]
382
+ v_ = v[:, select_mask, :]
383
+ gT_ = gT[:, select_mask, :]
384
+ gT_norm_ = gT_norm[:, select_mask_short, :]
385
+ atom_masks_ = atom_masks[:, select_mask, :]
386
+ batch_tensor_ = self.batch_tensor[select_mask_short]
387
+ batch_scatter_ = th.repeat_interleave(
388
+ batch_tensor_indx_cache[:len(batch_tensor_)],
389
+ batch_tensor_,
390
+ dim=0
391
+ )
392
+ else:
393
+ select_mask = None
394
+ select_mask_short = None
395
+ func_args_, func_kwargs_, grad_func_args_, grad_func_kwargs_ = func_args, func_kwargs, grad_func_args, grad_func_kwargs
396
+ vHv_ = vHv
397
+ Hv_ = Hv
398
+ X_ = X
399
+ v_ = v
400
+ gT_ = gT
401
+ gT_norm_ = gT_norm
402
+ atom_masks_ = atom_masks
403
+ batch_tensor_ = self.batch_tensor
404
+ batch_scatter_ = self.batch_scatter
405
+
406
+ # construction subspace Hessian [[vHv vHw] [wHv wHw]] with shape (B0, 2, 2) for 2nd order precise linear search
407
+ w_ = gT_ / (gT_norm_.index_select(1, batch_scatter_) + 1e-20) # (1, sumB*A, N)
408
+ y2_t, g2_, Hw_ = fin_diff_hvp(
409
+ func,
410
+ func_args_,
411
+ func_kwargs_,
412
+ grad_func_,
413
+ grad_func_args_,
414
+ grad_func_kwargs_,
415
+ X_,
416
+ w_,
417
+ batch_scatter_,
418
+ is_g_contain_y=is_grad_func_contain_y,
419
+ require_grad=require_grad,
420
+ delta=self.dx
421
+ )
422
+ Hw_.mul_(atom_masks_) # mask
423
+ # subspace Hessian
424
+ vHw_ = th.sum(
425
+ index_ops.index_inner_product(v_, Hw_, dim=1, batch_indices=batch_scatter_),
426
+ dim=-1,
427
+ keepdim=True
428
+ )
429
+ wHv_ = th.sum(
430
+ index_ops.index_inner_product(w_, Hv_, dim=1, batch_indices=batch_scatter_),
431
+ dim=-1,
432
+ keepdim=True
433
+ )
434
+ wHw_ = th.sum(
435
+ index_ops.index_inner_product(w_, Hw_, dim=1, batch_indices=batch_scatter_),
436
+ dim=-1,
437
+ keepdim=True
438
+ )
439
+ nondiag_ = 0.5 * (wHv_ + vHw_) # (1, B, 1)
440
+ H22_ = th.cat((vHv_, nondiag_, nondiag_, wHw_), dim=-1).reshape(-1, 2, 2)
441
+
442
+ sub_eigval_, sub_eigvec_ = th.linalg.eigh(H22_) # (B, 2), (B, 2, 2)
443
+ cos_t_ = sub_eigvec_[None, :, 0:1, 0].index_select(1, batch_scatter_)
444
+ sin_t_ = sub_eigvec_[None, :, 1:2, 0].index_select(1, batch_scatter_) # (1, sumB*A, 1)
445
+ theta_ = th.atan2(sub_eigvec_[:, 0, 0], sub_eigvec_[:, 0, 1]) # (B, )
446
+ # update
447
+ v_.mul_(cos_t_)
448
+ v_.add_(w_ * sin_t_)
449
+ v_.mul_(atom_masks_)
450
+ Hv_.mul_(cos_t_)
451
+ Hv_.add_(Hw_ * sin_t_)
452
+ Hv_.mul_(atom_masks_)
453
+ # curvature, vHv (1, B0, 1)
454
+ vHv_ = th.sum(
455
+ index_ops.index_inner_product(v_, Hv_, dim=1, batch_indices=batch_scatter_),
456
+ dim=-1,
457
+ keepdim=True
458
+ )
459
+ # grad in the tangent space
460
+ gT_ = Hv_ - vHv_.index_select(1, batch_scatter_) * v_
461
+ gT_norm_ = th.sqrt(th.sum(index_ops.index_inner_product(gT_, gT_, 1, batch_scatter_), dim=-1, keepdim=True))
462
+
463
+ # update origin variables
464
+ if not self._hold_samples:
465
+ select_indices = th.where(select_mask)[0]
466
+ select_indices_short = th.where(select_mask_short)[0]
467
+ y.index_copy_(0, select_indices_short, y2_t)
468
+ v.index_copy_(1, select_indices, v_)
469
+ #w.index_copy_(1, select_indices, w_)
470
+ #X.index_copy_(1, select_indices, X_)
471
+ Hv.index_copy_(1, select_indices, Hv_)
472
+ vHv.index_copy_(1, select_indices_short, vHv_)
473
+ gT_norm.index_copy_(1, select_indices_short, gT_norm_)
474
+ g.index_copy_(1, select_indices, g2_)
475
+ theta.index_copy_(0, select_indices_short, theta_) # (B, )
476
+ #atom_masks.index_copy_(1, select_indices, atom_masks_)
477
+ else:
478
+ y = y2_t
479
+ v = v_
480
+ #X = X_
481
+ Hv = Hv_
482
+ vHv = vHv_
483
+ gT_norm = gT_norm_
484
+ g = g2_
485
+ theta = theta_
486
+
487
+ if self.verbose and is_main_loop_converge:
488
+ self.logger.info(
489
+ '-' * 100 + f'\nrotation done. time: {time.perf_counter() - t_main:<.4f} s\n'
490
+ )
491
+ else:
492
+ self.logger.warning(
493
+ '-' * 100 + f'\nWARNING: Some Structures\' Rotation were NOT Converged yet!\nrotation done. time: {time.perf_counter() - t_main:<.4f} s\n'
494
+ )
495
+
496
+ # recalc y, g, Hv (Optional)
497
+ #y, g, Hv = fin_diff_hvp(
498
+ # func,
499
+ # func_args,
500
+ # func_kwargs,
501
+ # grad_func_,
502
+ # grad_func_args,
503
+ # grad_func_kwargs,
504
+ # X,
505
+ # v,
506
+ # self.batch_scatter,
507
+ # is_g_contain_y=is_grad_func_contain_y,
508
+ # require_grad=require_grad,
509
+ #)
510
+ Hv.mul_(atom_masks)
511
+ g.mul_(atom_masks)
512
+ # curvature, vHv (1, B0, 1)
513
+ vHv = th.sum(
514
+ index_ops.index_inner_product(v, Hv, dim=1, batch_indices=self.batch_scatter),
515
+ dim=-1,
516
+ keepdim=True
517
+ )
518
+
519
+ return v, y, g, Hv, vHv
520
+
521
+
522
+ class Dimer(BaseIO):
523
+ """
524
+ Modified Dimer
525
+ Ref. J Chem Phys 2005, 132, 224101.
526
+
527
+ """
528
+
529
+ def __init__(
530
+ self,
531
+ E_threshold: float = 1e-3,
532
+ Torque_thres: float = 1e-2,
533
+ Curvature_thres: float = - 0.1,
534
+ F_threshold: float = 0.05,
535
+ maxiter_trans: int = 100,
536
+ maxiter_rot: int = 10,
537
+ max_steplength: float = 0.5,
538
+ dx: float = 1.e-2,
539
+ device: str | th.device = 'cpu',
540
+ verbose: int = 2
541
+ ):
542
+ warnings.filterwarnings('always')
543
+ self.E_threshold = float(E_threshold)
544
+ self.Torque_thres = float(Torque_thres)
545
+ self.Curvature_thres = float(Curvature_thres)
546
+ self.F_threshold = float(F_threshold)
547
+ self.maxiter_trans = int(maxiter_trans)
548
+ assert (maxiter_rot > 0) and isinstance(maxiter_rot, int), '`maxiter_rot` must be an integer greater than 0.'
549
+ self.maxiter_rot = int(maxiter_rot)
550
+ self.max_steplength = float(max_steplength)
551
+ self.dx = float(dx)
552
+ self.device = device
553
+ self.verbose = verbose
554
+
555
+ self.Rotator = FindMinEigen(
556
+ self.Torque_thres,
557
+ self.Curvature_thres,
558
+ self.maxiter_rot,
559
+ self.dx,
560
+ self.device,
561
+ self.verbose,
562
+ _hold_samples=True
563
+ )
564
+
565
+ # logger
566
+ super().__init__()
567
+ self.init_logger('Main.TS')
568
+
569
+ def _update_batch(self, mask: th.Tensor, func_args: Tuple, func_kwargs: Dict, grad_func_args: Tuple, grad_func_kwargs: Dict):
570
+ """
571
+ Default update method for the input of func if the func has non-opt variables, i.e., the identical transform.
572
+ Args:
573
+ mask:
574
+
575
+ Returns:
576
+
577
+ """
578
+ return func_args, func_kwargs, grad_func_args, grad_func_kwargs
579
+
580
+ def _linear_search(self, dX_, ):
581
+ """
582
+ Linear search for dimer translation
583
+ Returns:
584
+
585
+ """
586
+ # TODO, adding linear search algo. to determine steplength.
587
+ pass
588
+
589
+ def set_batch_updater(
590
+ self,
591
+ method_trans: Callable[[th.Tensor, Tuple | None, Dict | None, Tuple | None, Dict | None], Tuple[Tuple, Dict, Tuple, Dict]],
592
+ method_rot: Callable[[th.Tensor, Tuple | None, Dict | None, Tuple | None, Dict | None], Tuple[Tuple, Dict, Tuple, Dict]] | None = None,
593
+ ) -> None:
594
+ """
595
+ Set a method to update the taget function when variables change.
596
+ It receives a mask tensor of shape (n_batch, ) that only selects the `True` part to input to the function, and receives the old
597
+ `func_args`, `func_kwargs`, `grad_func_args`, and `grad_func_kwargs`,
598
+ returns the corresponding masked new `func_args`, `func_kwargs`, `grad_func_args`, and `grad_func_kwargs`.
599
+
600
+ This method is used to dynamically 'remove' the samples which have been converged in a batch to avoid
601
+ redundant calculation of converged samples.
602
+
603
+ Default transform is identical transform (i.e., do nothing)
604
+ Args:
605
+ method_trans: Callable(mask: Tensor, func_args: Tuple, func_kwargs: Dict, grad_func_args: Tuple, grad_func_kwargs: Dict) -> Tuple[Tuple, Dict, Tuple, Dict],
606
+ method_rot: batch updater for rotations. the method of updating function arguments for a mask.
607
+
608
+ Returns: None
609
+ """
610
+ if method_rot is not None:
611
+ self.Rotator.set_batch_updater(method_rot)
612
+ else:
613
+ self.Rotator._hold_samples = True
614
+ self._update_batch = method_trans
615
+
616
+ def run(
617
+ self,
618
+ func: Any | nn.Module,
619
+ X: th.Tensor,
620
+ X_diff: th.Tensor | None = None,
621
+ grad_func: Any | nn.Module = None,
622
+ func_args: Sequence = tuple(),
623
+ func_kwargs=None,
624
+ grad_func_args: Sequence = tuple(),
625
+ grad_func_kwargs=None,
626
+ is_grad_func_contain_y: bool = True,
627
+ require_grad: bool = True,
628
+ output_grad: bool = False,
629
+ fixed_atom_tensor: Optional[th.Tensor] = None,
630
+ batch_indices: Optional[th.Tensor | List] = None,
631
+ ):
632
+ """
633
+ run Dimer algo.
634
+ Args:
635
+ func:
636
+ X:
637
+ X_diff:
638
+ grad_func:
639
+ func_args:
640
+ func_kwargs:
641
+ grad_func_args:
642
+ grad_func_kwargs:
643
+ is_grad_func_contain_y:
644
+ require_grad:
645
+ output_grad:
646
+ fixed_atom_tensor:
647
+ batch_indices:
648
+
649
+ Returns:
650
+
651
+ """
652
+ if grad_func_kwargs is None:
653
+ grad_func_kwargs = dict()
654
+ if func_kwargs is None:
655
+ func_kwargs = dict()
656
+ func_args = tuple(func_args)
657
+ grad_func_args = tuple(grad_func_args)
658
+ if X_diff is None:
659
+ X_diff = th.randn_like(X)
660
+ n_batch, n_atom, n_dim = X.shape
661
+ if grad_func is None:
662
+ is_grad_func_contain_y = True
663
+
664
+ def grad_func_(y, x, grad_shape=None):
665
+ if grad_shape is None:
666
+ grad_shape = th.ones_like(y)
667
+ g = th.autograd.grad(y, x, grad_shape)
668
+ return g[0]
669
+ else:
670
+ grad_func_ = grad_func
671
+ if isinstance(X, th.Tensor):
672
+ n_batch, n_atom, n_dim = X.shape
673
+ else:
674
+ raise TypeError(f'`X` must be torch.Tensor, but occurred {type(X)}.')
675
+ # batch_check
676
+ if batch_indices is not None:
677
+ if n_batch != 1:
678
+ raise RuntimeError(f'If batch_indices was specified, the 1st dimension of X must be 1 instead of {n_batch}.')
679
+ if isinstance(batch_indices, (th.Tensor, np.ndarray)):
680
+ batch_indices = batch_indices.tolist()
681
+ elif not isinstance(batch_indices, (Tuple, List)):
682
+ raise TypeError(f'Invalid type of batch_indices {type(batch_indices)}. '
683
+ f'It must be Sequence[int] | th.Tensor | np.ndarray | None')
684
+ for i in batch_indices: assert isinstance(i, int), f'All elements in batch_indices must be int, but occurred {type(i)}'
685
+ batch_slice_indx = [0] + list(accumulate(batch_indices)) # convert n_atom of each batch into split point of each batch
686
+ n_inner_batch = len(batch_indices)
687
+ self.batch_tensor = th.as_tensor(batch_indices, device=self.device)
688
+ self.batch_scatter = th.repeat_interleave(
689
+ th.arange(0, len(batch_indices), dtype=th.int64, device=self.device),
690
+ self.batch_tensor,
691
+ dim=0
692
+ )
693
+ else: # temporarily not implemented
694
+ raise NotImplementedError(f'Regular batch algo. is not implemented yet.')
695
+ n_inner_batch = 1
696
+ batch_indx_dict = dict()
697
+ batch_tensor = None
698
+
699
+ # Selective dynamics
700
+ if fixed_atom_tensor is None:
701
+ atom_masks = th.ones_like(X, device=self.device)
702
+ elif fixed_atom_tensor.shape == X.shape:
703
+ atom_masks = fixed_atom_tensor.to(self.device)
704
+ else:
705
+ raise RuntimeError(f'fixed_atom_tensor (shape: {fixed_atom_tensor.shape}) does not have the same shape of X (shape: {X.shape}).')
706
+ # other check
707
+ if (not isinstance(self.maxiter_trans, int)) or (not isinstance(self.maxiter_rot, int)) \
708
+ or (self.maxiter_trans <= 0) or (self.maxiter_rot <= 0):
709
+ raise ValueError(f'Invalid value of maxiter: {self.maxiter_trans}. It would be an integer greater than 0.')
710
+
711
+ # set variables device
712
+ if isinstance(func, nn.Module):
713
+ func = func.to(self.device)
714
+ func.zero_grad()
715
+ if isinstance(grad_func_, nn.Module):
716
+ grad_func_ = grad_func_.to(self.device)
717
+ X = X.to(self.device)
718
+ X_diff = X_diff.to(self.device)
719
+ v = X_diff.mul(atom_masks)
720
+ #plist = list() # TEST <<<<
721
+ is_main_loop_converge = False
722
+ # Main Loop
723
+ # initialize
724
+ y, g, Hv = fin_diff_hvp(
725
+ func,
726
+ func_args,
727
+ func_kwargs,
728
+ grad_func_,
729
+ grad_func_args,
730
+ grad_func_kwargs,
731
+ X,
732
+ v,
733
+ self.batch_scatter,
734
+ is_g_contain_y=is_grad_func_contain_y,
735
+ require_grad=require_grad,
736
+ )
737
+ vHv = th.sum(
738
+ index_ops.index_inner_product(v, Hv, dim=1, batch_indices=self.batch_scatter),
739
+ dim=-1,
740
+ keepdim=True
741
+ )
742
+ y_old = th.full_like(y, th.inf, device=self.device)
743
+ eig_thres_neg = - 0.01
744
+ eig_thres_pos = 0.01
745
+ batch_tensor_indx_cache = th.arange(0, len(self.batch_tensor), dtype=th.int64, device=self.device)
746
+ t_st = time.perf_counter()
747
+ with th.no_grad():
748
+ for i in range(self.maxiter_trans):
749
+ #plist.append(X[:, None, :, 0].numpy(force=True)) # TEST <<<<<<<<<<<<<
750
+ #plist.append(X2[:, None, :, 0].numpy(force=True))
751
+ # Section: check threshold <<<
752
+ # threshold.
753
+ converge_mask_curve = (vHv < 0.)
754
+ F_eps = index_ops.index_reduce(
755
+ th.max(th.abs(g), dim=-1, keepdim=True).values,
756
+ self.batch_scatter, 1, 'amax', -th.inf
757
+ )
758
+ E_eps = th.abs(y - y_old)
759
+ converge_mask_g = (F_eps < self.F_threshold)
760
+ converge_mask_e = th.lt(E_eps, self.E_threshold).reshape(1, -1, 1)
761
+ converge_mask = converge_mask_curve & converge_mask_g & converge_mask_e # (1, B, 1)
762
+ y_old = y.clone()
763
+ # print
764
+ if self.verbose > 0:
765
+ self.logger.info(f"Translation {i:>5d}\n "
766
+ f"MAD_Energies: {np.array2string(E_eps.squeeze().numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
767
+ f"MAX_F: {np.array2string(F_eps.squeeze().numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
768
+ f"Curvature: {np.array2string(vHv.squeeze().numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
769
+ f"Energies: {np.array2string(y.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n "
770
+ f"Converged: {np.array2string(converge_mask.squeeze().numpy(force=True), **STRING_ARRAY_FORMAT)}\n "
771
+ f"TIME: {time.perf_counter() - t_st:>6.4f} s")
772
+ t_st = time.perf_counter()
773
+ # OUTPUT COORD
774
+ if self.verbose > 1:
775
+ # split batches if specified batch
776
+ if batch_indices is not None:
777
+ X_np = X.numpy(force=True)
778
+ X_tup = np.split(X_np, batch_slice_indx[1:-1], axis=1)
779
+ if self.verbose > 2:
780
+ F_np = (- g).numpy(force=True)
781
+ F_tup = np.split(F_np, batch_slice_indx[1:-1], axis=1)
782
+ else:
783
+ X_tup = (X.numpy(force=True),)
784
+ if self.verbose > 2:
785
+ F_tup = (- g.numpy(force=True),)
786
+ self.logger.info(f" Coordinates:\n")
787
+ X_str = [
788
+ np.array2string(xi, **FLOAT_ARRAY_FORMAT).replace("[", " ").replace("]", " ")
789
+ for xi in X_tup
790
+ ]
791
+ for x_str in X_str: self.logger.info(f'{x_str}\n')
792
+ if self.verbose > 2:
793
+ self.logger.info(f" Forces:\n")
794
+ X_str = [
795
+ np.array2string(xi, **FLOAT_ARRAY_FORMAT).replace("[", " ").replace("]", " ")
796
+ for xi in F_tup
797
+ ]
798
+ for x_str in X_str: self.logger.info(f'{x_str}\n')
799
+ # judge thres
800
+ if th.all(converge_mask):
801
+ is_main_loop_converge = True
802
+ break
803
+ converge_mask_short = converge_mask
804
+ converge_mask = converge_mask[:, self.batch_scatter, ...] # (1, sumB*A, 1)
805
+ # update batch, remove the already converged ones.
806
+ func_args_, func_kwargs_, grad_func_args_, grad_func_kwargs_ = self._update_batch(
807
+ ~converge_mask_short.squeeze(),
808
+ func_args,
809
+ func_kwargs,
810
+ grad_func_args,
811
+ grad_func_kwargs
812
+ )
813
+ select_mask = ~(converge_mask[0, :, 0]) # (sumB*A, )
814
+ select_mask_short = ~converge_mask_short[0, :, 0] # (B, )
815
+ X_ = X[:, select_mask, :]
816
+ g_ = g[:, select_mask, :]
817
+ v_ = v[:, select_mask, :]
818
+ Hv_ = Hv[:, select_mask, :]
819
+ atom_masks_ = atom_masks[:, select_mask, :]
820
+ batch_tensor_ = self.batch_tensor[select_mask_short]
821
+ batch_scatter_ = th.repeat_interleave(
822
+ batch_tensor_indx_cache[:len(batch_tensor_)],
823
+ batch_tensor_,
824
+ dim=0
825
+ )
826
+
827
+ # Section: Transition to maximum <<<
828
+ # 2D subspace Newton Search
829
+ vg_ = th.sum(index_ops.index_inner_product(v_, g_, dim=1, batch_indices=batch_scatter_), dim=-1, keepdim=True) # (1, B0, 1)
830
+ normal_grad_ = vg_.index_select(1, batch_scatter_) * v_ # (1, sumB*A, N)
831
+ tangent_grad_ = g_ - normal_grad_
832
+ tangent_norm_ = th.sqrt(
833
+ th.sum(index_ops.index_inner_product(tangent_grad_, tangent_grad_, 1, batch_scatter_), dim=-1, keepdim=True)
834
+ )
835
+ u_ = tangent_grad_ / (tangent_norm_.index_select(1, batch_scatter_) + 1e-20)
836
+ ug_ = th.sum(index_ops.index_inner_product(u_, g_, dim=1, batch_indices=batch_scatter_), dim=-1, keepdim=True) # (1, B0, 1)
837
+ _, _, Hu_ = fin_diff_hvp(
838
+ func,
839
+ func_args_,
840
+ func_kwargs_,
841
+ grad_func_,
842
+ grad_func_args_,
843
+ grad_func_kwargs_,
844
+ X_,
845
+ u_,
846
+ batch_scatter_,
847
+ is_g_contain_y=is_grad_func_contain_y,
848
+ require_grad=require_grad,
849
+ )
850
+ Hv_.mul_(atom_masks_)
851
+ Hu_.mul_(atom_masks_)
852
+ # curvature, vHv (1, B0, 1)
853
+ vHv_ = th.sum(
854
+ index_ops.index_inner_product(v_, Hv_, dim=1, batch_indices=batch_scatter_),
855
+ dim=-1,
856
+ keepdim=True
857
+ )
858
+ vHu_ = th.sum(
859
+ index_ops.index_inner_product(v_, Hu_, dim=1, batch_indices=batch_scatter_),
860
+ dim=-1,
861
+ keepdim=True
862
+ )
863
+ uHv_ = th.sum(
864
+ index_ops.index_inner_product(u_, Hv_, dim=1, batch_indices=batch_scatter_),
865
+ dim=-1,
866
+ keepdim=True
867
+ )
868
+ uHu_ = th.sum(
869
+ index_ops.index_inner_product(u_, Hu_, dim=1, batch_indices=batch_scatter_),
870
+ dim=-1,
871
+ keepdim=True
872
+ )
873
+ nondiag_ = 0.5 * (uHv_ + vHu_) # (1, B, 1)
874
+ H22_ = th.cat((vHv_, nondiag_, nondiag_, uHu_), dim=-1).reshape(-1, 2, 2)
875
+ Lambda, U = th.linalg.eigh(H22_) # (B, 2), (B, 2, 2)
876
+ # modify spectra
877
+ Lambda[:, 0] = th.where(Lambda[:, 0] >= 0., - Lambda[:, 0], Lambda[:, 0])
878
+ Lambda[:, 0] = th.where(th.gt(Lambda[:, 0], eig_thres_neg), eig_thres_neg, Lambda[:, 0])
879
+ Lambda[:, 1] = th.where(th.le(Lambda[:, 1], eig_thres_pos), eig_thres_pos, Lambda[:, 1])
880
+ # subspace Newton step
881
+ sub_g_ = th.cat((vg_, ug_), dim=-1).squeeze(0)
882
+ # (B, 2, 2) @ (B, 2, 2) @ (B, 2, 1) -> (B, 2)
883
+ p_ = th.einsum('bij, bi -> bj', - U @ (th.reciprocal(Lambda.unsqueeze(-1)) * U.mT), sub_g_)
884
+ dX_ = p_[batch_scatter_, 0:1] * v_ + p_[batch_scatter_, 1:] * u_ # (sumB * A, D)
885
+ dX_norm_ = th.sqrt(th.sum(index_ops.index_inner_product(dX_, dX_, 1, batch_scatter_), dim=-1, keepdim=True))
886
+ dX_norm_scatter_ = dX_norm_.index_select(1, batch_scatter_)
887
+ dX_ = th.where(
888
+ dX_norm_scatter_ > self.max_steplength,
889
+ self.max_steplength / dX_norm_scatter_ * dX_,
890
+ dX_
891
+ )
892
+ # update X
893
+ dX_.mul_(atom_masks_)
894
+ X_.add_(dX_)
895
+ # traditional scheme
896
+ # dx = th.where(
897
+ # vHv_ > 0.,
898
+ # - normal_grad,
899
+ # tangent_grad - normal_grad,
900
+ # ) # (1, sumB*A, N)
901
+ _small_dX_ = (dX_norm_ < 1.e-6) # (1, B0, 1)
902
+ if th.any(_small_dX_):
903
+ _too_small_step_indx = th.where(select_mask_short)[0][th.where(_small_dX_)[0]]
904
+ warnings.warn(
905
+ RuntimeWarning(
906
+ f'Convergence is not met while the steplengths of {_too_small_step_indx.tolist()}-th structure(s) are 0. '
907
+ )
908
+ )
909
+ if th.all(_small_dX_):
910
+ self.logger.error('ERROR: All unconverged structures reached 0 steplength. LOOP BREAK.')
911
+ break
912
+
913
+
914
+ # Section: Rotate to minimum <<<
915
+ v_, y_, g_, Hv_, vHv_ = self.Rotator.run(
916
+ func=func,
917
+ X=X_,
918
+ v=v_,
919
+ grad_func=grad_func_,
920
+ func_args=func_args_,
921
+ func_kwargs=func_kwargs_,
922
+ grad_func_args=grad_func_args_,
923
+ grad_func_kwargs=grad_func_kwargs_,
924
+ is_grad_func_contain_y=is_grad_func_contain_y,
925
+ require_grad=require_grad,
926
+ fixed_atom_tensor=atom_masks_,
927
+ batch_indices=batch_tensor_
928
+ )
929
+
930
+ # update origin variables
931
+ select_indices = th.where(select_mask)[0]
932
+ select_indices_short = th.where(select_mask_short)[0]
933
+ y.index_copy_(0, select_indices_short, y_)
934
+ v.index_copy_(1, select_indices, v_)
935
+ X.index_copy_(1, select_indices, X_)
936
+ Hv.index_copy_(1, select_indices, Hv_)
937
+ vHv.index_copy_(1, select_indices_short, vHv_)
938
+ g.index_copy_(1, select_indices, g_)
939
+
940
+ if self.verbose:
941
+ if is_main_loop_converge:
942
+ self.logger.info('-' * 100 + '\nAll Structures Were Converged.\nMAIN LOOP Done.')
943
+ else:
944
+ self.logger.info('-' * 100 + '\nSome Structures were NOT Converged yet!\nMAIN LOOP Done.')
945
+
946
+ if output_grad:
947
+ return y, X, g
948
+ else:
949
+ return y, X#, plist # TEST <<<<<<
950
+