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,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
|
+
|