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,11 @@
1
+
2
+
3
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
4
+ # Authors: Pu Pengxin, Song Xin
5
+ # Version: 0.9a
6
+ # File: _warnings.py
7
+ # Environment: Python 3.12
8
+
9
+ class NotConvergeWarning(Warning):
10
+ def __init__(self, *args: object) -> None:
11
+ super().__init__(*args)
@@ -0,0 +1,496 @@
1
+ # Copyright (c) 2025.7.4, BUCToolkit.
2
+ # Authors: Pu Pengxin, Song Xin
3
+ # Version: 0.9a
4
+ # File: frequency.py
5
+ # Environment: Python 3.12
6
+ """
7
+ Calculating the harmonic frequencies by finite difference algorithm and automatic differentiation.
8
+ Also support vibration thermodynamic quantities calculations.
9
+ """
10
+ import re
11
+ from typing import Literal, Callable, Tuple, Dict, List
12
+
13
+ import numpy as np
14
+ import torch as th
15
+
16
+
17
+
18
+ class Frequency:
19
+ """
20
+ Calculate normal mode frequency by finite difference method.
21
+
22
+ Args:
23
+ method: 'Coord' for directly calculating 2nd-order deviations, i.e., the Hessian matrix.
24
+ 'Grad' for calculating 1st-order deviations of grad to get Hessian matrix.
25
+ block_size: block size to calculate Hessian. `block_size`*N rows would be input at once.
26
+ `None` for input all (3*N)**2 rows coords at once, which requires enough memory.
27
+ delta: finite difference delta.
28
+ """
29
+ def __init__(self, method: Literal['Coord', 'Grad'] = 'Coord', block_size: None | int = None, delta: float = 1e-2):
30
+ self.hessian = None
31
+ self.method = method
32
+ self.block_size = block_size
33
+ self.delta = delta
34
+
35
+ class _func_wrapper:
36
+ """
37
+ Check the function output format
38
+ """
39
+ def __init__(self, func, args: Tuple = tuple(), kwargs: Dict|None = None):
40
+ if kwargs is None: kwargs = dict()
41
+ self.func = func
42
+ self.args = args
43
+ self.kwargs = kwargs
44
+
45
+ def __call__(self, x):
46
+ y: th.Tensor = self.func(x, *self.args, **self.kwargs)
47
+ if y.dim() > 1:
48
+ y.squeeze_()
49
+ if y.dim() > 1: raise ValueError(f'Expected a 1-dimensional output, but got {y.dim()} dim.')
50
+ return y
51
+
52
+ def _create_finite_diff_tensor(self, coords: th.Tensor, fixed_atom_tensor: th.Tensor | None):
53
+ """
54
+ Generating finite difference tensor with shape (6N+1, N, 3)
55
+
56
+ Args:
57
+ coords: (N, 3).
58
+ fixed_atom_tensor: indices of free atoms. generate from `fixed_atom_tensor` in method `normal_mode`.
59
+
60
+ Returns:
61
+ positive_difference, negative_difference, origin_point: (3 N, N, 3), (3 N, N, 3), (1, N, 3)
62
+ """
63
+
64
+ coords_free = coords[fixed_atom_tensor]
65
+ N = coords_free.shape[0]
66
+ coords_out = coords.unsqueeze(0).expand(3*N, -1, -1)
67
+ device = coords_free.device
68
+ dtype = coords_free.dtype
69
+
70
+ # displacement indx
71
+ k_indices = th.arange(6 * N, device=device)
72
+ i = k_indices // 6 # each point corresponding 2 directions, i.e., the center difference algo.
73
+ direction = (k_indices % 6) // 2 # direction:0=x,1=y,2=z
74
+ # odd remainder: negative; even remainder: positive
75
+ sign = th.where((k_indices % 6) % 2 == 0, 1.0, -1.0).to(dtype=dtype)
76
+ # initialize finite difference tensor
77
+ delta_tensor = th.zeros(6 * N, N, 3, device=device, dtype=dtype)
78
+ delta_tensor[k_indices, i, direction] = self.delta * sign
79
+ displaced = coords_free.unsqueeze(0) + delta_tensor # (6N, N, 3)
80
+ # positive tensor
81
+ pos_indx = th.arange(0, 6 * N, 2)
82
+ # negative tensor
83
+ neg_indx = th.arange(1, 6 * N, 2)
84
+ # split directions
85
+ diff_pos = displaced[pos_indx] # (3N, N, 3)
86
+ diff_neg = displaced[neg_indx]
87
+ # Adding the original point
88
+ origin = coords.unsqueeze(0) # (1, N, 3)
89
+ #diff_tensor = th.cat([origin, displaced], dim=0) # (6N+1, N, 3)
90
+ diff_pos_out = coords_out.clone()
91
+ diff_pos_out[:, fixed_atom_tensor] = diff_pos
92
+ diff_neg_out = coords_out.clone()
93
+ diff_neg_out[:, fixed_atom_tensor] = diff_neg
94
+
95
+ return diff_pos_out, diff_neg_out, origin
96
+
97
+ def create_hessian(
98
+ self,
99
+ func: Callable,
100
+ coords: th.Tensor,
101
+ func_args: Tuple = tuple(),
102
+ func_kwargs: Dict|None = None,
103
+ save_hessian: bool = False,
104
+ fixed_atom_tensor: th.Tensor | None = None,
105
+ ) -> th.Tensor:
106
+ """
107
+ To calculate Hessian matrix in blocks via finite difference
108
+ Args:
109
+ func:
110
+ coords: (N, 3) shape Tensor
111
+ func_args: other input arguments of `func`.
112
+ func_kwargs: other input keyword arguments of `func`.
113
+ save_hessian: whether save calculated hessian as an attribute.
114
+ fixed_atom_tensor: the indices of X that fixed, i.e., not performing vibration calculation.
115
+
116
+ Returns:
117
+
118
+ """
119
+ if func_kwargs is None:
120
+ func_kwargs = dict()
121
+ device = coords.device
122
+ dtype = coords.dtype
123
+ func_ = self._func_wrapper(func, func_args, func_kwargs)
124
+ diff_pos, diff_neg, origin = self._create_finite_diff_tensor(coords, fixed_atom_tensor)
125
+
126
+ pp = diff_pos.unsqueeze(1) + diff_pos.unsqueeze(0) - origin # (3N, 3N, N, 3)
127
+ np = diff_neg.unsqueeze(1) + diff_pos.unsqueeze(0) - origin
128
+ pn = diff_pos.unsqueeze(1) + diff_neg.unsqueeze(0) - origin
129
+ nn = diff_neg.unsqueeze(1) + diff_neg.unsqueeze(0) - origin
130
+
131
+ # reformat as batch dimension
132
+ n_free = len(diff_pos)
133
+ n_atom = n_free // 3
134
+ all_batch = len(pp)**2
135
+ block_size = all_batch if self.block_size is None else self.block_size
136
+ n_int_block = pp.flatten(0, 1).shape[0] // block_size
137
+ n_rest = pp.flatten(0, 1).shape[0] % block_size
138
+ real_block_size = [block_size] * n_int_block
139
+ if n_rest > 0: real_block_size = real_block_size + [n_rest] # manage the problem that all_batch could not be divided by given block_size.
140
+ pp = th.split(pp.flatten(0, 1), real_block_size) # (9N**2, N ,3)
141
+ np = th.split(np.flatten(0, 1), real_block_size)
142
+ pn = th.split(pn.flatten(0, 1), real_block_size)
143
+ nn = th.split(nn.flatten(0, 1), real_block_size)
144
+ num_block = len(pp)
145
+
146
+ # split blocks
147
+ start = 0
148
+ hessian = th.empty(all_batch, device=device, dtype=dtype)
149
+ for _indx in range(num_block):
150
+ end = start + block_size
151
+ hessian[start:end] = (
152
+ func_(pp[_indx]) - func_(np[_indx]) - func_(pn[_indx]) + func_(nn[_indx])
153
+ ) / (4 * self.delta ** 2) # (3N * 3N)
154
+ start = end
155
+ hessian = hessian.reshape(n_free, n_free)
156
+ if save_hessian:
157
+ self.hessian = hessian
158
+
159
+ return hessian.squeeze(0)
160
+
161
+ def hessian_by_autograd(
162
+ self,
163
+ grad_func,
164
+ X: th.Tensor,
165
+ grad_func_args: Tuple = tuple(),
166
+ grad_func_kwargs: Dict | None = None,
167
+ fixed_atom_tensor: th.Tensor | None = None,
168
+ save_hessian: bool = False
169
+ ):
170
+ if grad_func_kwargs is None:
171
+ grad_func_kwargs = dict()
172
+
173
+ if X.dim() == 2:
174
+ X = X.unsqueeze(0)
175
+ elif X.dim() != 3:
176
+ raise ValueError(f'Expected X has 2 or 3 dimensions, but got {X.shape}')
177
+ X_flat = X.flatten(-2, -1)
178
+ if fixed_atom_tensor is None:
179
+ fixed_atom_tensor = th.arange(X_flat.size(1), dtype=X.dtype, device=X.device)
180
+
181
+ n_batch, n_atom, n_dim = X.shape
182
+ n_free_atom = len(fixed_atom_tensor)
183
+ fixed_atom_list = fixed_atom_tensor.tolist()
184
+
185
+ with th.enable_grad():
186
+ X_flat.requires_grad_()
187
+ X_ = X_flat.reshape(n_batch, n_atom, n_dim)
188
+ g: th.Tensor = grad_func(X_, *grad_func_args, **grad_func_kwargs)
189
+ g = g.reshape(n_batch, n_atom * n_dim)
190
+ # Hessian
191
+ hessian = th.zeros((n_batch, n_free_atom, n_free_atom), device=X.device)
192
+ hess_mask = th.zeros(n_batch, n_atom * n_dim, device=X.device)
193
+ for i, indx in enumerate(fixed_atom_list):
194
+ hess_mask[:, indx] = 1.
195
+ H_line = th.autograd.grad(g, X_flat, hess_mask, retain_graph=True)[0] # (n_batch, n_atom*n_dim, 1)
196
+ hessian[:, :, i] = H_line.squeeze(-1)[:, fixed_atom_tensor].detach()
197
+ hess_mask[:, indx] = 0.
198
+ del H_line, g, X_flat
199
+ if save_hessian:
200
+ self.hessian = hessian.squeeze(0)
201
+
202
+ return hessian.squeeze(0)
203
+
204
+ def normal_mode(
205
+ self,
206
+ func: Callable,
207
+ coords: th.Tensor,
208
+ masses: th.Tensor|None = None,
209
+ func_args: Tuple = tuple(),
210
+ func_kwargs: Dict|None = None,
211
+ grad_func: Callable | None = None,
212
+ grad_func_args: Tuple = tuple(),
213
+ grad_func_kwargs: Dict | None = None,
214
+ fixed_atom_tensor: th.Tensor | None = None,
215
+ save_hessian: bool = False
216
+ ):
217
+ """
218
+ Calculating the normal modes and corresponding frequencies.
219
+ Notice: The negative frequencies are actually imaginary frequencies.
220
+ Args:
221
+ func: potential energy function.
222
+ coords: atomic coordinate tensor with shape (N, 3).
223
+ masses: atomic masses tensor with shape (N, 3) or (N, ). `None` for tensor filled with 1.
224
+ func_args: other input arguments of `func`.
225
+ func_kwargs: other input keyword arguments of `func`.
226
+ grad_func: gradient function, only used for self.method = 'Grad'.
227
+ grad_func_args: other input arguments of `grad_func`.
228
+ grad_func_kwargs: other input keyword arguments of `grad_func`.
229
+ fixed_atom_tensor: (N, ) or (N, 3), the indices of X that fixed, i.e., not performing vibration calculation.
230
+ Only the 1st dimension will be read. Fixing partial degrees of freedom for an atom is not supported.
231
+ save_hessian: whether save calculated hessian as an attribute.
232
+
233
+ Returns:
234
+ normal mode frequencies: (3N, ) shape Tensor
235
+ normal mode coordinates: (3N, N, 3) shape Tensor
236
+ """
237
+ # check
238
+ with th.no_grad():
239
+ if fixed_atom_tensor is None:
240
+ fixed_atom_tensor = th.ones_like(coords[:, 0], dtype=th.int)
241
+ elif fixed_atom_tensor.shape == coords.shape:
242
+ fixed_atom_tensor = fixed_atom_tensor[:, 0]
243
+ elif not fixed_atom_tensor.shape == coords[:, 0].shape:
244
+ raise ValueError(f'Invalid shape of fixed_atom_tensor: {fixed_atom_tensor.shape}')
245
+ if th.all(fixed_atom_tensor != 1):
246
+ raise ValueError(f'All atoms are fixed. No vibration will be calculated.')
247
+ fixed_atom_tensor_indx = th.where(fixed_atom_tensor)[0] # (n_free_atom, )
248
+ fixed_atom_tensor_ex = fixed_atom_tensor.unsqueeze(-1).expand(-1, coords.size(-1)).flatten(0, 1) # (n_free_atom * 3)
249
+ fixed_atom_tensor_ex = th.where(fixed_atom_tensor_ex)[0]
250
+ device = coords.device
251
+ if (coords.shape[-1] != 3) or (coords.dim() != 2):
252
+ raise ValueError(f'Expected `coords` have shape (N, 3), but got {coords.shape}')
253
+ if masses is None:
254
+ masses = th.ones_like(coords)
255
+ else:
256
+ if (masses.shape[0] != coords.shape[0]) or masses.dim() > 2:
257
+ raise ValueError(f'Expected `masses` have shape (N, ) or (N, 3), but got {masses.shape}')
258
+ elif masses.dim() < 2:
259
+ masses = th.broadcast_to(masses.unsqueeze(-1), coords.shape) # transfer to (N, 3)
260
+ masses = masses.to(device)
261
+ masses = masses[fixed_atom_tensor_indx]
262
+ flat_masses = masses.flatten(0, 1)
263
+ hess_weights = th.sqrt(flat_masses.unsqueeze(-1) * flat_masses.unsqueeze(0))
264
+ # calc hessian
265
+ if self.method == 'Coord':
266
+ hessian = self.create_hessian(func, coords, func_args, func_kwargs, fixed_atom_tensor=fixed_atom_tensor_indx)
267
+ else:
268
+ hessian = self.hessian_by_autograd(grad_func, coords, grad_func_args, grad_func_kwargs, fixed_atom_tensor=fixed_atom_tensor_ex)
269
+ weighted_hessian = hessian/hess_weights
270
+ norm_freq_square, weighted_norm_mode = th.linalg.eigh(weighted_hessian)
271
+ neg_indx = th.where(norm_freq_square < 0.)[0]
272
+ norm_freq = th.sqrt(th.abs(norm_freq_square))
273
+ norm_freq[neg_indx] = - norm_freq[neg_indx]
274
+ norm_mode = (weighted_norm_mode/flat_masses).reshape(norm_freq_square.size(-1), -1, 3)
275
+ if save_hessian:
276
+ self.hessian = hessian
277
+
278
+ return norm_freq, norm_mode
279
+
280
+
281
+ def vibrational_thermo(frequencies: th.Tensor, T: float = 298.15):
282
+ r"""
283
+ Thermodynamic quantities calculation for condensed states, i.e., U = H, F = G, and \delta(PV) = 0.
284
+ Args:
285
+ frequencies: (N,), frequencies tensor, unit: cm^-1. Negative values mean imaginary frequencies.
286
+ T: Temperature. unit: K.
287
+ Returns:
288
+ S_vib: Entropy (eV·K)
289
+ H_vib: Enthalpy (eV)
290
+ C_vib: Thermo-capacity (eV·K)
291
+ F_vib: Free-energy (eV)
292
+ """
293
+ # CONSTANTS
294
+ HC = 1.98630e-23 # h*c (J·cm)
295
+ NA = 6.02214076e23 # Avogadro const. (mol^-1)
296
+ KB_J = 1.380649e-23 # Boltzmann const. (J/K)
297
+ CM_TO_eV = 1.23981e-4 # cm^-1 -> eV (1 cm^-1 = 1.23981e-4 eV)
298
+ CM_TO_kJ_PER_MOL = HC * NA * 1e-3 # cm^-1 → kJ/mol
299
+ CM_TO_J_PER_MOL = HC * NA # cm^-1 → J/mol
300
+ J_PER_MOL_TO_eV = 1e3 / (NA * 1.60218e-19) # J/mol → eV (约 1.0364e-2)
301
+ CM_TO_eV_PER_MOL = CM_TO_J_PER_MOL * J_PER_MOL_TO_eV # cm^-1 → eV
302
+ # remove imag. freq.
303
+ positive_mask = frequencies > 0
304
+ if th.all(~positive_mask):
305
+ raise RuntimeError(f'Input frequencies are all imaginary frequencies. I HOPE YOU KNOW WHAT YOU ARE DOING.')
306
+ nu = frequencies[positive_mask]
307
+
308
+ # set freq < 50 cm^-1 to 50 cm^-1 to avoid abnormal entropy contribution.
309
+ nu = th.where(nu < 50., 50., nu)
310
+
311
+ # x = hν/kT = ν/(kB * T)
312
+ kB_cm = KB_J / HC
313
+ x = nu / (kB_cm * T)
314
+
315
+ # if x > 100, considered T as a small value. e.g., for freq. of 3200 cm^-1 and T = 298 K, x = 5.406 << 100. If x = 100, T is about merely 16 K.
316
+ high_x = x > 100
317
+
318
+ # U
319
+ exp_x = th.exp(x)
320
+ zero_point_energy = 0.5 * nu
321
+ thermal_energy = nu / (exp_x - 1 + 1e-16)
322
+ U_vib = th.where(high_x, zero_point_energy, zero_point_energy + thermal_energy)
323
+
324
+ # C
325
+ denom = (exp_x - 1) ** 2
326
+ C_factor = (x ** 2) * exp_x / (denom + 1e-16)
327
+ C_vib_cm = th.where(high_x, th.zeros_like(x), kB_cm * C_factor)
328
+
329
+ # S
330
+ S1 = x / (exp_x - 1 + 1e-16)
331
+ S2 = th.log(1 - th.exp(-x) + 1e-16)
332
+ S_vib_cm = kB_cm * th.where(high_x, th.zeros_like(x), S1 - S2)
333
+
334
+ # G
335
+ F_vib = th.where(high_x, zero_point_energy, zero_point_energy + kB_cm * T * S2)
336
+
337
+ # sum
338
+ U_total = th.sum(U_vib)
339
+ C_total = th.sum(C_vib_cm)
340
+ S_total = th.sum(S_vib_cm)
341
+ F_total = th.sum(F_vib)
342
+
343
+ # unit conversion. convert to eV, eV/K
344
+ S_ev = S_total * CM_TO_eV
345
+ C_ev = C_total * CM_TO_eV
346
+ U_ev = U_total * CM_TO_eV
347
+ F_ev = F_total * CM_TO_eV
348
+
349
+ return S_ev, U_ev, C_ev, F_ev
350
+
351
+ def extract_freq(path: str) -> List[np.ndarray]:
352
+ """
353
+ Extract frequency from Vibration calc. output files.
354
+ Args:
355
+ path: path of Frequency calculation output file
356
+
357
+ Returns:
358
+ List of frequencies.
359
+
360
+ """
361
+ with open(path, 'r') as f:
362
+ data = f.read()
363
+
364
+ pattern = re.compile(r'Eigen Vibration Frequency \(cm\^-1\):\n([+.\-0-9\s\n]+)', flags=re.M)
365
+ freqn = re.findall(pattern, data)
366
+ freq_list = [np.asarray(frl.split(), dtype=np.float32) for frl in freqn]
367
+
368
+ return freq_list
369
+
370
+ __all__ = [
371
+ 'Frequency',
372
+ 'vibrational_thermo',
373
+ 'extract_freq'
374
+ ]
375
+
376
+ # 示例用法
377
+ if __name__ == "__main__":
378
+ # 创建示例频率张量 (包含正负频率和低频)
379
+ frequencies = th.tensor([
380
+ 3083.953574,
381
+ 3047.925383,
382
+ 3027.645431,
383
+ 3018.475192,
384
+ 3012.926299,
385
+ 2927.218652,
386
+ 2799.844693,
387
+ 1456.985281,
388
+ 1446.552789,
389
+ 1417.972562,
390
+ 1406.900854,
391
+ 1357.130821,
392
+ 1294.452227,
393
+ 1268.645944,
394
+ 1210.800793,
395
+ 1154.637385,
396
+ 1082.020997,
397
+ 993.746725,
398
+ 969.939002,
399
+ 913.057972,
400
+ 871.097270,
401
+ 818.914607,
402
+ 756.614656,
403
+ 552.091194,
404
+ 414.467590,
405
+ 250.240054,
406
+ 185.291922,
407
+ 116.075049,
408
+ 77.272448,
409
+ 63.180734,
410
+ 39.135462,
411
+ - 99.207538,
412
+ - 1281.881665,
413
+ ])
414
+ freq2 = th.tensor([
415
+ -2677.8059082000, - 718.62634277000, - 515.33343506000,
416
+ -169.42842102000, 128.61962891000, 196.16116333000,
417
+ 259.79168701000 , 416.30462646000 , 492.31179810000,
418
+ 655.95141602000 , 718.72033691000 , 841.46276855000,
419
+ 940.41229248000 , 1146.7479248000 , 1180.0986328100,
420
+ 1234.0729980500 , 1301.6352539100 , 1432.3217773400,
421
+ 1553.4138183600 , 1575.9421386700 , 1638.2396240200,
422
+ 1678.4561767600 , 1733.8581543000 , 1804.6947021500,
423
+ 1953.1727294900 , 2313.4604492200 , 2356.5280761700,
424
+ 2467.3337402300 , 2573.1855468800 , 2679.1540527300,
425
+ 2702.9921875000 , 2840.1782226600 , 3055.0336914100,
426
+ ])# 单位 cm⁻¹
427
+ T = 298.15 # 温度 (K)
428
+ frequencies = th.sort(frequencies).values
429
+ freq2= th.sort(freq2).values
430
+
431
+ S_vib, H_vib, C_vib, F_vib = vibrational_thermo(frequencies, T)
432
+ S_vib_ml, H_vib_ml, C_vib_ml, F_vib_ml = vibrational_thermo(freq2, T)
433
+
434
+ print("频率对比", frequencies - freq2) # 原始正频率
435
+
436
+ print(f"\n振动熵: {S_vib.item():.6f}, {S_vib_ml.item():.6f} eV/K")
437
+ print(f"振动焓: {H_vib.item():.6f}, {H_vib_ml.item():.6f} eV")
438
+ print(f"振动热容: {C_vib.item():.6f}, {C_vib_ml.item():.6f} eV/K")
439
+ print(f"振动自由能: {F_vib.item():.6f}, {F_vib_ml.item():.6f} eV")
440
+ print(f"振动自由能差: {F_vib.item() - F_vib_ml.item():.6f} eV")
441
+
442
+
443
+ if __name__ == '__main__' and False:
444
+ a = th.rand(6, 6)
445
+ a = a + a.mT
446
+
447
+ '''class fn(th.nn.Module):
448
+ def __init__(self):
449
+ super().__init__()
450
+ seed = th.manual_seed(114514)
451
+ self.layers = th.nn.Sequential(
452
+ th.nn.Linear(6, 100, bias=False),
453
+ th.nn.Tanh(),
454
+ th.nn.Linear(100, 100, bias=False),
455
+ th.nn.Tanh(),
456
+ th.nn.Linear(100, 1, bias=False),
457
+ )
458
+
459
+ def forward(self, x):
460
+ x = x.flatten(-2, -1)
461
+ return self.layers(x)**2'''
462
+
463
+ def f(x):
464
+ """
465
+
466
+ Args:
467
+ x:
468
+
469
+ Returns:
470
+
471
+ """
472
+ x = x.flatten(-2, -1).unsqueeze(-1)
473
+ y = th.exp(x).mT @ a @ th.cos(x)
474
+ return y.squeeze(-1, -2)
475
+
476
+ def grad_f(x:th.Tensor):
477
+ with th.enable_grad():
478
+ x.requires_grad_()
479
+ y = f(x)
480
+ g = th.autograd.grad(y, x, th.ones_like(y), create_graph=True)[0]
481
+ return g
482
+
483
+
484
+ x0 = th.ones(2, 3)
485
+ freq1 = Frequency('Coord') # FIXME
486
+ ei1, ej1 = freq1.normal_mode(f, x0, grad_func=grad_f, save_hessian=True, fixed_atom_tensor=th.tensor([0, 1]))
487
+
488
+ x0.detach_()
489
+ freq0 = Frequency('Grad')
490
+ ei0, ej0 = freq0.normal_mode(f, x0, grad_func=grad_f, save_hessian=True, fixed_atom_tensor=th.tensor([1, 1]))
491
+
492
+ x0.detach_()
493
+ freq2 = Frequency('Grad')
494
+ ei2, ej2 = freq2.normal_mode(f, x0, grad_func=grad_f, save_hessian=True, fixed_atom_tensor=th.tensor([0, 1]))
495
+ pass
496
+