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,286 @@
1
+ # Copyright (c) 2025.7.4, BUCToolkit.
2
+ # Authors: Pu Pengxin, Song Xin
3
+ # Version: 0.9a
4
+ # File: CI_NEB.py
5
+ # Environment: Python 3.12
6
+
7
+ from typing import Iterable, Dict, Any, List, Literal, Optional, Callable, Sequence, Tuple # noqa: F401
8
+ import time
9
+ import warnings
10
+ import logging
11
+ import sys
12
+
13
+ import numpy as np
14
+ import torch as th
15
+ from torch import nn
16
+ from torch.nn import functional as Fn
17
+
18
+ from BUCToolkit.utils._print_formatter import GLOBAL_SCIENTIFIC_ARRAY_FORMAT, SCIENTIFIC_ARRAY_FORMAT
19
+ from BUCToolkit.BatchGenerate.coords_interp import linear_interpolation_tens
20
+ from ..minimize import CG, QN, FIRE
21
+
22
+ np.set_printoptions(**GLOBAL_SCIENTIFIC_ARRAY_FORMAT)
23
+
24
+
25
+ class CI_NEB:
26
+ """
27
+ Ref. TODO
28
+
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ N_images: int,
34
+ spring_const: float = 1.,
35
+ optimizer: Literal['FIRE'] = 'FIRE',
36
+ optimizer_configs: Optional[Dict[str, Any]] = None,
37
+ steplength: float = 0.05,
38
+ E_threshold: float = 1e-3,
39
+ F_threshold: float = 0.05,
40
+ maxiter: int = 100,
41
+ device: str | th.device = 'cpu',
42
+ verbose: int = 2
43
+ ):
44
+ if optimizer_configs is None:
45
+ optimizer_configs = dict()
46
+ warnings.filterwarnings('always')
47
+ optimizer_dict = {'FIRE': FIRE}
48
+ if optimizer not in optimizer_dict:
49
+ raise NotImplementedError(f"Optimizer {optimizer} is not implemented yet. Now only `FIRE` is available.")
50
+ self.Optimizer = optimizer_dict[optimizer](
51
+ E_threshold = E_threshold,
52
+ F_threshold = F_threshold,
53
+ maxiter = maxiter,
54
+ steplength = steplength,
55
+ device = device,
56
+ verbose = verbose,
57
+ _hold_samples = True, # important! To ensure that no sample will be removed during NEB process.
58
+ **optimizer_configs
59
+ )
60
+ self.N_images = N_images
61
+ self.spring_const = spring_const
62
+ self.E_threshold = E_threshold
63
+ self.F_threshold = F_threshold
64
+ self.maxiter = maxiter
65
+ self.device = device
66
+ self.verbose = verbose
67
+ # check
68
+ if (not isinstance(self.maxiter, int)) or (self.maxiter <= 0):
69
+ raise ValueError(f'Invalid value of maxiter: {self.maxiter}. It would be an integer greater than 0.')
70
+ if self.N_images < 1:
71
+ raise ValueError(f'`N_images` must be >= 1 for CI-NEB, but got {self.N_images}.')
72
+
73
+ # logger
74
+ self.logger = logging.getLogger('Main.OPT')
75
+ self.logger.setLevel(logging.INFO)
76
+ formatter = logging.Formatter('%(message)s')
77
+ if not self.logger.hasHandlers():
78
+ log_handler = logging.StreamHandler(sys.stdout, )
79
+ log_handler.setLevel(logging.INFO)
80
+ log_handler.setFormatter(formatter)
81
+ self.logger.addHandler(log_handler)
82
+ pass
83
+
84
+ class __NebWrapper:
85
+ """ function wrapper for NEB, Adding the elastic potential """
86
+ def __init__(self, f: Callable, gf: Callable, k: float, require_grad: bool, is_grad_contain_y: bool):
87
+ self.ener = None
88
+ self.F_ori = None
89
+ self.f = f
90
+ self.gf = gf
91
+ self.k = k
92
+ self.is_grad_contain_y = is_grad_contain_y
93
+ self.require_grad = require_grad
94
+
95
+ def energy(self, X, *args, **kwargs):
96
+ with th.set_grad_enabled(self.require_grad):
97
+ X.requires_grad_(self.require_grad)
98
+ y = self.f(X, *args, **kwargs)
99
+ self.ener = y
100
+ if self.is_grad_contain_y: # here `X` is actually `y`
101
+ F_ori = - self.gf(y, X, *args, **kwargs) # (n_i, n_atom, 3)
102
+ else:
103
+ F_ori = - self.gf(X, *args, **kwargs) # (n_i, n_atom, 3)
104
+ self.F_ori = F_ori
105
+ return y # the true energy, only for exhibiting system energy, not involving into optimizing.
106
+
107
+ def grad(self, X, *args, **kwargs):
108
+ """
109
+ Returns the gradient of the energy function
110
+ Args:
111
+ X: It is only a placeholder, a dummy variable.
112
+ *args: args[0] is the true coordinates X
113
+ **kwargs:
114
+
115
+ Returns:
116
+
117
+ """
118
+ assert self.ener is not None, 'Energy is None now, please calculate energy first.'
119
+ F_ori = self.F_ori # (n_i, n_atom, 3)
120
+ if self.is_grad_contain_y:
121
+ X = args[0]
122
+ else:
123
+ X = X
124
+
125
+ n_i, n_atom, n_dim = X.shape
126
+
127
+ with th.no_grad():
128
+ # projected direction
129
+ _tau = th.diff(X, dim=0) # (n_i - 1, n_atom, 3)
130
+ ediff = th.diff(self.ener) # (n_i - 1, )
131
+ # *_back = E[i] - E[i - 1]
132
+ ediff_back = ediff[:-1].unsqueeze(-1).unsqueeze(-1) # (n_i - 2, 1, 1), E_(n) - E_(n-1), keep the same dim as `_tau`
133
+ tau_back = _tau[:-1] # set of image[0 : n_i - 1], X[i] - X[i - 1]
134
+ # *_front = E[i + 1] - E[i]
135
+ ediff_front = ediff[1:].unsqueeze(-1).unsqueeze(-1) # (n_i - 2, 1, 1), E_(n+1) - E_(n)
136
+ tau_front = _tau[1:] # set of image[1 : n_i], X[i + 1] - X[i]
137
+ tau_front_u = Fn.normalize(tau_front.flatten(-2, -1), dim=-1).reshape(n_i - 2, n_atom, -1)
138
+ tau_back_u = Fn.normalize(tau_back.flatten(-2, -1), dim=-1).reshape(n_i - 2, n_atom, -1)
139
+ # *_front - *_back: image[i + 1] - image[i] & deltaE[i + 1] - deltaE[i] for each i \in [0, n_i - 1]
140
+ delta_Emax = th.where(th.abs(ediff_front) > th.abs(ediff_back), th.abs(ediff_front), th.abs(ediff_back))
141
+ delta_Emin = th.where(th.abs(ediff_front) < th.abs(ediff_back), th.abs(ediff_front), th.abs(ediff_back))
142
+ tau_extreme = th.where(
143
+ ediff_front + ediff_back > 0, # (E_(n+1) > E_(n-1))
144
+ tau_front_u * delta_Emax + tau_back_u * delta_Emin,
145
+ tau_front_u * delta_Emin + tau_back_u * delta_Emax
146
+ )
147
+ tau = th.where(
148
+ (ediff_front > 0) * (ediff_back > 0),
149
+ tau_front_u,
150
+ th.where(
151
+ (ediff_front <= 0) * (ediff_back <= 0),
152
+ tau_back_u,
153
+ tau_extreme
154
+ )
155
+ )# (n_i - 2, n_atom, n_dim)
156
+ tau = Fn.normalize(tau.flatten(-2, -1), dim=-1) # (n_i - 2, n_atom*n_dim)
157
+
158
+ # forces
159
+ F_ori[0] = 0.
160
+ F_ori[-1] = 0.
161
+ F_image = F_ori[1: -1].flatten(-2, -1) # (n_i - 2, n_atom*n_dim)
162
+ # climbing
163
+ max_indx = th.argmax(self.ener[1:-1])
164
+ maxF = F_image[max_indx].clone() # (n_atom * n_dim, )
165
+ maxF -= 2 * ((maxF.unsqueeze(0) @ tau[max_indx].unsqueeze(-1)).squeeze()) * tau[max_indx]
166
+ # other NEB
167
+ F_vert = F_image - ((F_image.unsqueeze(1) @ tau.unsqueeze(-1)).squeeze(-1)) * tau
168
+ F_hori = self.k * (th.linalg.norm(tau_front, dim=(-2, -1)) - th.linalg.norm(tau_back, dim=(-2, -1))).unsqueeze(-1) * tau
169
+ F_ori[1: -1] = (F_vert + F_hori).reshape(n_i-2, n_atom, n_dim)
170
+ #print(f'NEB forces: {F_ori.tolist()}')
171
+ F_ori[max_indx + 1] = maxF.reshape(n_atom, n_dim)
172
+
173
+ return - F_ori
174
+
175
+ def run(
176
+ self,
177
+ func: Any | nn.Module,
178
+ X_init: th.Tensor,
179
+ X_fin: th.Tensor,
180
+ grad_func: Any | nn.Module = None,
181
+ func_args: Tuple = tuple(),
182
+ func_kwargs=None,
183
+ grad_func_args: Tuple = tuple(),
184
+ grad_func_kwargs=None,
185
+ is_grad_func_contain_y: bool = True,
186
+ require_grad = False,
187
+ output_grad: bool = False,
188
+ fixed_atom_tensor: Optional[th.Tensor] = None,
189
+ ):
190
+ """
191
+ run CI-NEB algo.
192
+ Args:
193
+ func: function
194
+ X_init: (n_atom, n_dim), initial structure coordinates
195
+ X_fin: (n_atom, n_dim), finale structure coordinates
196
+ grad_func: function of func's gradient
197
+ func_args: function args
198
+ func_kwargs: function kwargs
199
+ grad_func_args: gradient function args
200
+ grad_func_kwargs: gradient function kwargs
201
+ is_grad_func_contain_y: whether gradient function contains dependent various y
202
+ require_grad: bool, if True, autograd will be turned on for func(X, *func_args, **func_kwargs) calculation.
203
+ output_grad: whether output gradient
204
+ fixed_atom_tensor: (n_atom, n_dim), mask of fixed atoms
205
+
206
+ Returns:
207
+
208
+ """
209
+ # initialize
210
+ if grad_func_kwargs is None:
211
+ grad_func_kwargs = dict()
212
+ if func_kwargs is None:
213
+ func_kwargs = dict()
214
+ if grad_func is None:
215
+ is_grad_func_contain_y = True
216
+ require_grad = True
217
+ def grad_func_(y, x, grad_shape=None):
218
+ if grad_shape is None:
219
+ grad_shape = th.ones_like(y)
220
+ g = th.autograd.grad(y, x, grad_shape)
221
+ return g[0]
222
+ else:
223
+ grad_func_ = grad_func
224
+
225
+ # check
226
+ if len(X_init.shape) != 2:
227
+ X_init.squeeze_(0)
228
+ X_fin.squeeze_(0)
229
+ if isinstance(func, nn.Module):
230
+ func = func.to(self.device)
231
+ func.zero_grad()
232
+ if isinstance(grad_func_, nn.Module):
233
+ grad_func_ = grad_func_.to(self.device)
234
+ if X_init.shape != X_fin.shape:
235
+ raise ValueError(f'Shape of X_init {X_init.shape} and X_fin{X_fin.shape} does not match.')
236
+ elif len(X_init.shape) != 2:
237
+ raise ValueError(f'Invalid shape of X_init: {X_init.shape}, it mast be (n_atom, 3).')
238
+ X_init = X_init.to(self.device)
239
+ X_fin = X_fin.to(self.device)
240
+ # Selective dynamics for interpolation
241
+ if fixed_atom_tensor is None:
242
+ atom_masks = th.ones_like(X_init, device=self.device)
243
+ elif fixed_atom_tensor.shape == X_init.shape:
244
+ atom_masks = fixed_atom_tensor.to(self.device)
245
+ else:
246
+ raise RuntimeError(f'fixed_atom_tensor (shape: {fixed_atom_tensor.shape}) does not have the same shape of X (shape: {X_init.shape}).')
247
+
248
+ # interpolation images
249
+ X_pnts: th.Tensor = linear_interpolation_tens(X_init, X_fin, self.N_images + 2, atom_masks) # (N_img, n_atom, n_dim)
250
+ #plist = list() # TEST <<<<
251
+
252
+ # Main Loop
253
+ #X_pnts = X_pnts.flatten(0, 1) # (N_img, n_atom, n_dim). NOTE: '_' means the flatten variables.
254
+ f = self.__NebWrapper(func, grad_func_, self.spring_const, require_grad, is_grad_func_contain_y)
255
+ self.Optimizer: FIRE
256
+ with th.no_grad():
257
+ oup = self.Optimizer.run(
258
+ f.energy,
259
+ X_pnts,
260
+ f.grad,
261
+ func_args,
262
+ func_kwargs,
263
+ grad_func_args,
264
+ grad_func_kwargs,
265
+ is_grad_func_contain_y,
266
+ require_grad,
267
+ output_grad,
268
+ atom_masks.broadcast_to(X_pnts.shape)
269
+ )
270
+ if output_grad:
271
+ _energy, _X, out_grad = oup
272
+ else:
273
+ _energy, _X = oup
274
+ # final print
275
+ if self.verbose:
276
+ max_ener, max_indx = th.max(_energy, dim=0)
277
+ self.logger.info(
278
+ f'-----------------------------------------------------\n'
279
+ f'Final NEB path energy: {np.array2string(_energy.numpy(force=True), **SCIENTIFIC_ARRAY_FORMAT)}\n'
280
+ f'Final CI-NEB max energy: {max_ener.item():< .8e}\n'
281
+ f'Image index: {max_indx.item():< 3d}\n'
282
+ f'-----------------------------------------------------\n'
283
+ )
284
+
285
+ return oup#, plist # TEST <<<<<<
286
+