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.
- BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
- BUCToolkit/BatchGenerate/__init__.py +6 -0
- BUCToolkit/BatchGenerate/coords_interp.py +127 -0
- BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
- BUCToolkit/BatchGenerate/random_replace.py +88 -0
- BUCToolkit/BatchMC/MetropolisMC.py +210 -0
- BUCToolkit/BatchMC/_BaseMC.py +863 -0
- BUCToolkit/BatchMC/__init__.py +8 -0
- BUCToolkit/BatchMD/BiasedMD.py +276 -0
- BUCToolkit/BatchMD/NVE.py +92 -0
- BUCToolkit/BatchMD/NVT.py +510 -0
- BUCToolkit/BatchMD/_BaseMD.py +1251 -0
- BUCToolkit/BatchMD/__init__.py +18 -0
- BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
- BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
- BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
- BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
- BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
- BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
- BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
- BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
- BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
- BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
- BUCToolkit/BatchOptim/TS/__init__.py +10 -0
- BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
- BUCToolkit/BatchOptim/__init__.py +22 -0
- BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
- BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
- BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
- BUCToolkit/BatchOptim/frequency.py +496 -0
- BUCToolkit/BatchOptim/minimize/CG.py +256 -0
- BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
- BUCToolkit/BatchOptim/minimize/QN.py +205 -0
- BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
- BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
- BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
- BUCToolkit/BatchStructures/__init__.py +15 -0
- BUCToolkit/Preprocessing/__init__.py +7 -0
- BUCToolkit/Preprocessing/load_files.py +1680 -0
- BUCToolkit/Preprocessing/preprocessing.py +923 -0
- BUCToolkit/Preprocessing/sampling.py +299 -0
- BUCToolkit/Preprocessing/supercells.py +140 -0
- BUCToolkit/Preprocessing/write_files.py +524 -0
- BUCToolkit/StandardTemplate.py +417 -0
- BUCToolkit/__init__.py +33 -0
- BUCToolkit/_version.py +7 -0
- BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
- BUCToolkit/api/DataLoaders.py +321 -0
- BUCToolkit/api/Losses.py +170 -0
- BUCToolkit/api/Metrics.py +84 -0
- BUCToolkit/api/ModelOptims.py +296 -0
- BUCToolkit/api/MolecularDynamics.py +282 -0
- BUCToolkit/api/MonteCarlo.py +280 -0
- BUCToolkit/api/NEB.py +325 -0
- BUCToolkit/api/Predictor.py +292 -0
- BUCToolkit/api/StructureOptimization.py +484 -0
- BUCToolkit/api/Trainer.py +635 -0
- BUCToolkit/api/VibrationAnalysis.py +279 -0
- BUCToolkit/api/__init__.py +38 -0
- BUCToolkit/api/_io.py +1026 -0
- BUCToolkit/cli/__init__.py +20 -0
- BUCToolkit/cli/cli.py +934 -0
- BUCToolkit/cli/input_stub.py +140 -0
- BUCToolkit/cli/main.py +368 -0
- BUCToolkit/cli/print_logo.py +149 -0
- BUCToolkit/cli/template_model.py +6 -0
- BUCToolkit/io.py +30 -0
- BUCToolkit/utils/AtomSelector.py +64 -0
- BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
- BUCToolkit/utils/ElemListReduce.py +52 -0
- BUCToolkit/utils/Ewald.py +210 -0
- BUCToolkit/utils/IrregularTensorReformat.py +59 -0
- BUCToolkit/utils/_CheckModules.py +30 -0
- BUCToolkit/utils/_Element_info.py +527 -0
- BUCToolkit/utils/__init__.py +22 -0
- BUCToolkit/utils/_para_flatt_list.py +131 -0
- BUCToolkit/utils/_print_formatter.py +59 -0
- BUCToolkit/utils/check_structures.py +204 -0
- BUCToolkit/utils/function_utils.py +147 -0
- BUCToolkit/utils/grad_functions.py +69 -0
- BUCToolkit/utils/index_ops.py +432 -0
- BUCToolkit/utils/setup_loggers.py +137 -0
- buctoolkit-1.0b0.dist-info/METADATA +28 -0
- buctoolkit-1.0b0.dist-info/RECORD +88 -0
- buctoolkit-1.0b0.dist-info/WHEEL +5 -0
- buctoolkit-1.0b0.dist-info/entry_points.txt +6 -0
- buctoolkit-1.0b0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
|