BUCToolkit 1.0b0__tar.gz
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-1.0b0/BUCToolkit/BatchGenerate/CheckStructures.py +198 -0
- buctoolkit-1.0b0/BUCToolkit/BatchGenerate/__init__.py +6 -0
- buctoolkit-1.0b0/BUCToolkit/BatchGenerate/coords_interp.py +127 -0
- buctoolkit-1.0b0/BUCToolkit/BatchGenerate/initial_ads_structures.py +351 -0
- buctoolkit-1.0b0/BUCToolkit/BatchGenerate/random_replace.py +88 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMC/MetropolisMC.py +210 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMC/_BaseMC.py +863 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMC/__init__.py +8 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMD/BiasedMD.py +276 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMD/NVE.py +92 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMD/NVT.py +510 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMD/_BaseMD.py +1251 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMD/__init__.py +18 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMD/constrained_md/BiasedMD.py +273 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMD/constrained_md/ConstrNVE.py +169 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMD/constrained_md/ConstrNVT.py +462 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMD/constrained_md/NVE.py +78 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMD/constrained_md/NVT.py +346 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMD/constrained_md/_BaseMD.py +440 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMD/constrained_md/_ConstrBaseMD.py +607 -0
- buctoolkit-1.0b0/BUCToolkit/BatchMD/constrained_md/__init__.py +18 -0
- buctoolkit-1.0b0/BUCToolkit/BatchOptim/TS/CI_NEB.py +286 -0
- buctoolkit-1.0b0/BUCToolkit/BatchOptim/TS/Dimer.py +950 -0
- buctoolkit-1.0b0/BUCToolkit/BatchOptim/TS/__init__.py +10 -0
- buctoolkit-1.0b0/BUCToolkit/BatchOptim/_BaseOpt.py +702 -0
- buctoolkit-1.0b0/BUCToolkit/BatchOptim/__init__.py +22 -0
- buctoolkit-1.0b0/BUCToolkit/BatchOptim/_utils/__init__.py +6 -0
- buctoolkit-1.0b0/BUCToolkit/BatchOptim/_utils/_line_search.py +972 -0
- buctoolkit-1.0b0/BUCToolkit/BatchOptim/_utils/_warnings.py +11 -0
- buctoolkit-1.0b0/BUCToolkit/BatchOptim/frequency.py +496 -0
- buctoolkit-1.0b0/BUCToolkit/BatchOptim/minimize/CG.py +256 -0
- buctoolkit-1.0b0/BUCToolkit/BatchOptim/minimize/FIRE.py +310 -0
- buctoolkit-1.0b0/BUCToolkit/BatchOptim/minimize/QN.py +205 -0
- buctoolkit-1.0b0/BUCToolkit/BatchOptim/minimize/__init__.py +17 -0
- buctoolkit-1.0b0/BUCToolkit/BatchStructures/BatchStructuresBase.py +2261 -0
- buctoolkit-1.0b0/BUCToolkit/BatchStructures/StructuresIO.py +1394 -0
- buctoolkit-1.0b0/BUCToolkit/BatchStructures/__init__.py +15 -0
- buctoolkit-1.0b0/BUCToolkit/Preprocessing/__init__.py +7 -0
- buctoolkit-1.0b0/BUCToolkit/Preprocessing/load_files.py +1680 -0
- buctoolkit-1.0b0/BUCToolkit/Preprocessing/preprocessing.py +923 -0
- buctoolkit-1.0b0/BUCToolkit/Preprocessing/sampling.py +299 -0
- buctoolkit-1.0b0/BUCToolkit/Preprocessing/supercells.py +140 -0
- buctoolkit-1.0b0/BUCToolkit/Preprocessing/write_files.py +524 -0
- buctoolkit-1.0b0/BUCToolkit/StandardTemplate.py +417 -0
- buctoolkit-1.0b0/BUCToolkit/__init__.py +33 -0
- buctoolkit-1.0b0/BUCToolkit/_version.py +7 -0
- buctoolkit-1.0b0/BUCToolkit/api/ConstrainedMolecularDynamics.py +347 -0
- buctoolkit-1.0b0/BUCToolkit/api/DataLoaders.py +321 -0
- buctoolkit-1.0b0/BUCToolkit/api/Losses.py +170 -0
- buctoolkit-1.0b0/BUCToolkit/api/Metrics.py +84 -0
- buctoolkit-1.0b0/BUCToolkit/api/ModelOptims.py +296 -0
- buctoolkit-1.0b0/BUCToolkit/api/MolecularDynamics.py +282 -0
- buctoolkit-1.0b0/BUCToolkit/api/MonteCarlo.py +280 -0
- buctoolkit-1.0b0/BUCToolkit/api/NEB.py +325 -0
- buctoolkit-1.0b0/BUCToolkit/api/Predictor.py +292 -0
- buctoolkit-1.0b0/BUCToolkit/api/StructureOptimization.py +484 -0
- buctoolkit-1.0b0/BUCToolkit/api/Trainer.py +635 -0
- buctoolkit-1.0b0/BUCToolkit/api/VibrationAnalysis.py +279 -0
- buctoolkit-1.0b0/BUCToolkit/api/__init__.py +38 -0
- buctoolkit-1.0b0/BUCToolkit/api/_io.py +1026 -0
- buctoolkit-1.0b0/BUCToolkit/cli/__init__.py +20 -0
- buctoolkit-1.0b0/BUCToolkit/cli/cli.py +934 -0
- buctoolkit-1.0b0/BUCToolkit/cli/input_stub.py +140 -0
- buctoolkit-1.0b0/BUCToolkit/cli/main.py +368 -0
- buctoolkit-1.0b0/BUCToolkit/cli/print_logo.py +149 -0
- buctoolkit-1.0b0/BUCToolkit/cli/template_model.py +6 -0
- buctoolkit-1.0b0/BUCToolkit/io.py +30 -0
- buctoolkit-1.0b0/BUCToolkit/utils/AtomSelector.py +64 -0
- buctoolkit-1.0b0/BUCToolkit/utils/AtomicNumber2Properties.py +39 -0
- buctoolkit-1.0b0/BUCToolkit/utils/ElemListReduce.py +52 -0
- buctoolkit-1.0b0/BUCToolkit/utils/Ewald.py +210 -0
- buctoolkit-1.0b0/BUCToolkit/utils/IrregularTensorReformat.py +59 -0
- buctoolkit-1.0b0/BUCToolkit/utils/_CheckModules.py +30 -0
- buctoolkit-1.0b0/BUCToolkit/utils/_Element_info.py +527 -0
- buctoolkit-1.0b0/BUCToolkit/utils/__init__.py +22 -0
- buctoolkit-1.0b0/BUCToolkit/utils/_para_flatt_list.py +131 -0
- buctoolkit-1.0b0/BUCToolkit/utils/_print_formatter.py +59 -0
- buctoolkit-1.0b0/BUCToolkit/utils/check_structures.py +204 -0
- buctoolkit-1.0b0/BUCToolkit/utils/function_utils.py +147 -0
- buctoolkit-1.0b0/BUCToolkit/utils/grad_functions.py +69 -0
- buctoolkit-1.0b0/BUCToolkit/utils/index_ops.py +432 -0
- buctoolkit-1.0b0/BUCToolkit/utils/setup_loggers.py +137 -0
- buctoolkit-1.0b0/BUCToolkit.egg-info/PKG-INFO +28 -0
- buctoolkit-1.0b0/BUCToolkit.egg-info/SOURCES.txt +91 -0
- buctoolkit-1.0b0/BUCToolkit.egg-info/dependency_links.txt +1 -0
- buctoolkit-1.0b0/BUCToolkit.egg-info/entry_points.txt +6 -0
- buctoolkit-1.0b0/BUCToolkit.egg-info/requires.txt +13 -0
- buctoolkit-1.0b0/BUCToolkit.egg-info/top_level.txt +1 -0
- buctoolkit-1.0b0/LICENSE +21 -0
- buctoolkit-1.0b0/PKG-INFO +28 -0
- buctoolkit-1.0b0/README.md +23 -0
- buctoolkit-1.0b0/setup.cfg +4 -0
- buctoolkit-1.0b0/setup.py +43 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Check Structures by atomic distance, surface smoothness, and coordination.
|
|
3
|
+
"""
|
|
4
|
+
# Copyright (c) 2024-2025.7.4, BUCToolkit.
|
|
5
|
+
# Authors: Pu Pengxin, Song Xin
|
|
6
|
+
# Version: 0.9a
|
|
7
|
+
# File: CheckStructures.py
|
|
8
|
+
# Environment: Python 3.12
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
import warnings
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from BUCToolkit.BatchStructures.BatchStructuresBase import BatchStructures
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
class CheckStructures:
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, batch_structures: BatchStructures):
|
|
22
|
+
assert isinstance(batch_structures, BatchStructures), (f'`batch_structures` must be a BatchStructures type, '
|
|
23
|
+
f'but occurred {type(batch_structures)}.')
|
|
24
|
+
self.BS = batch_structures
|
|
25
|
+
|
|
26
|
+
def check_distance(self, radius_threshold: float=1):
|
|
27
|
+
"""
|
|
28
|
+
check atomic pairwise distances that are too close (less than given `radius_threshold`)
|
|
29
|
+
Args:
|
|
30
|
+
radius_threshold: float, the threshold radius.
|
|
31
|
+
Returns:
|
|
32
|
+
mask_array: ndarray[(n_batch, ), bool], where False for failed to the check.
|
|
33
|
+
"""
|
|
34
|
+
self.BS.direct2cartesian()
|
|
35
|
+
coords = self.BS.Coords
|
|
36
|
+
mask = np.full(len(coords), True, )
|
|
37
|
+
|
|
38
|
+
for i, coo in enumerate(coords):
|
|
39
|
+
if self.BS.Dist_mat is None:
|
|
40
|
+
dist_mat = np.linalg.norm(coo[:, None, :] - coo[None, :, :], axis=-1)
|
|
41
|
+
else:
|
|
42
|
+
dist_mat = self.BS.Dist_mat[i]
|
|
43
|
+
# exclude the diag. part
|
|
44
|
+
diag_indx = np.arange(len(coo))
|
|
45
|
+
dist_mat[diag_indx, diag_indx] = 20.
|
|
46
|
+
|
|
47
|
+
if np.min(dist_mat) < radius_threshold:
|
|
48
|
+
mask[i] = False
|
|
49
|
+
|
|
50
|
+
return mask
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
#@jit(nopython=True)
|
|
54
|
+
def _k_means(x:np.ndarray, c:np.ndarray, maxiter:int=100, thres:float=1e-4):
|
|
55
|
+
"""
|
|
56
|
+
k-mean clustering algorithm
|
|
57
|
+
Args:
|
|
58
|
+
x: (n_atom, ), sample points
|
|
59
|
+
c: (nc, ), cluster center points
|
|
60
|
+
maxiter: max iteration numbers
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
variance
|
|
64
|
+
"""
|
|
65
|
+
n_c = len(c)
|
|
66
|
+
nc_indx = np.arange(n_c)
|
|
67
|
+
c_old = c.copy()
|
|
68
|
+
assert maxiter >= 1, f'Invalid `maxiter` value {maxiter}.'
|
|
69
|
+
for i in range(maxiter):
|
|
70
|
+
# calc. dist.
|
|
71
|
+
dist = np.abs(x[:, None] - c[None, :]) # (n_atom, nc)
|
|
72
|
+
min_dist_indx = np.argmin(dist, -1) # (n_atom, )
|
|
73
|
+
# update center
|
|
74
|
+
_mask = (min_dist_indx[:, None] == nc_indx) # (n_atom, nc)
|
|
75
|
+
n_samp_in_cluster = np.sum(_mask, axis=0) # (n_c, )
|
|
76
|
+
drop_mask = n_samp_in_cluster == 0 # drop the non-sample cluster
|
|
77
|
+
c = np.where(drop_mask, 0., np.sum(x[:, None] * _mask, axis=0)/(n_samp_in_cluster + 1e-20)) # (n_atom, 1) * (n_atom, nc) -mean-> (nc, )
|
|
78
|
+
# delete the empty cluster
|
|
79
|
+
if np.any(drop_mask):
|
|
80
|
+
select_indx = np.where(~drop_mask)[0]
|
|
81
|
+
c = c[select_indx]
|
|
82
|
+
c_old = c_old[select_indx]
|
|
83
|
+
_mask = _mask[:, select_indx]
|
|
84
|
+
n_c = len(c)
|
|
85
|
+
nc_indx = np.arange(n_c)
|
|
86
|
+
# judge threshold
|
|
87
|
+
if np.linalg.norm(c - c_old) < thres:
|
|
88
|
+
return c, np.max(np.sum((x[:, None] - c[None, :])**2 * _mask, axis=0)/np.sum(_mask, axis=0)) # return the cluster points and metrics for smoothness
|
|
89
|
+
else:
|
|
90
|
+
c_old = c.copy()
|
|
91
|
+
|
|
92
|
+
#warnings.warn(f'K-means does not converge in {maxiter} steps.', RuntimeWarning)
|
|
93
|
+
return c, np.max(np.sum((x[:, None] - c[None, :])**2 * _mask, axis=0)/np.sum(_mask, axis=0))
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _spectrum_clustering(x: np.ndarray, r_cutoff: float=3.):
|
|
97
|
+
"""
|
|
98
|
+
Diagonalize laplace matrix of surface and bottom of slab to determined subgraph number.
|
|
99
|
+
Args:
|
|
100
|
+
x: (n_atom, 3), coordinates of surf. or bott.
|
|
101
|
+
r_cutoff: threshold of radius.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
n: number of connected subgraphs.
|
|
105
|
+
"""
|
|
106
|
+
dist = np.linalg.norm(x[:, None] - x[None, :], axis=-1) + (r_cutoff + 10.) * np.eye(len(x)) # exclude the diag.
|
|
107
|
+
adj = np.where(dist < r_cutoff, 1., 0.)
|
|
108
|
+
degree = np.diag(np.sum(adj, axis=-1))
|
|
109
|
+
laplace = degree - adj
|
|
110
|
+
eigs = np.linalg.eigvalsh(laplace)
|
|
111
|
+
n_sub = np.sum(np.abs(eigs - 0.) < 1e-7)
|
|
112
|
+
return n_sub
|
|
113
|
+
|
|
114
|
+
def _periodic_reformat(self, cell, coords):
|
|
115
|
+
"""
|
|
116
|
+
Transition the atom moved out of a cell into the periodic position in the cell.
|
|
117
|
+
Args:
|
|
118
|
+
cell:
|
|
119
|
+
coords:
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
# TODO
|
|
125
|
+
raise NotImplementedError
|
|
126
|
+
|
|
127
|
+
def check_surf_smoothness(
|
|
128
|
+
self,
|
|
129
|
+
smoothness_threshold: float = 0.5,
|
|
130
|
+
r_cutoff: float = 3.,
|
|
131
|
+
surf_check_method: Literal['ConnectedGraph', 'C', 'MinDistance', 'M']|None = 'MinDistance'
|
|
132
|
+
):
|
|
133
|
+
"""
|
|
134
|
+
check whether the surface smooth enough (whether the max difference among surface atoms along z axis < `smoothness_threshold`)
|
|
135
|
+
Args:
|
|
136
|
+
smoothness_threshold: smoothness threshold.
|
|
137
|
+
r_cutoff: the cut-off distance to form the graph or inter-atomic distance checking.
|
|
138
|
+
surf_check_method: method of surface checking.
|
|
139
|
+
None: no check will applied;
|
|
140
|
+
'ConnectedGraph' or 'C': it will check whether the surface atoms form only one connected graph. If not, mask returns False.
|
|
141
|
+
'MinDistance' or 'M': it will check for atoms whose shortest distance from other atoms exceeds the cut-off distance.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
mask_array: ndarray[(n_batch, ), bool], where False for failed to the check.
|
|
145
|
+
"""
|
|
146
|
+
self.BS.direct2cartesian()
|
|
147
|
+
coords = self.BS.Coords
|
|
148
|
+
mask = np.full(len(coords), True, )
|
|
149
|
+
std_val = 1e8
|
|
150
|
+
for i, coo in enumerate(coords):
|
|
151
|
+
# check empty coordinates
|
|
152
|
+
if len(coo) == 0:
|
|
153
|
+
warnings.warn(f'Occurred empty coordinates in {i}th structure, skipped.', RuntimeWarning)
|
|
154
|
+
continue
|
|
155
|
+
# convert to float64
|
|
156
|
+
coo = coo.astype(np.float64)
|
|
157
|
+
sorted_coo = np.sort(coo[:, -1])
|
|
158
|
+
std_val_old = np.inf
|
|
159
|
+
tol_cont = 0 # tolerance step of std_val not decrease.
|
|
160
|
+
|
|
161
|
+
for j in range(len(coo)):
|
|
162
|
+
_centers = np.array([np.mean(_) for _ in np.array_split(sorted_coo, j+1)])
|
|
163
|
+
_centers, std_val = self._k_means(coo[:, -1], _centers)
|
|
164
|
+
# raise for 3 previous steps to break the loop
|
|
165
|
+
if (std_val > std_val_old) or (len(_centers) < j+1):
|
|
166
|
+
tol_cont += 1
|
|
167
|
+
else:
|
|
168
|
+
tol_cont = 0
|
|
169
|
+
if (tol_cont > 2) or (np.min(np.abs(_centers[:, None] - _centers[None, :]) + 20.*np.eye(len(_centers))) < smoothness_threshold):
|
|
170
|
+
break
|
|
171
|
+
elif std_val < 1e-5: # completely converged
|
|
172
|
+
std_val_old = std_val
|
|
173
|
+
centers = _centers
|
|
174
|
+
break
|
|
175
|
+
elif tol_cont == 0:
|
|
176
|
+
std_val_old = std_val
|
|
177
|
+
centers = _centers
|
|
178
|
+
if std_val > smoothness_threshold:
|
|
179
|
+
mask[i] = False
|
|
180
|
+
else:
|
|
181
|
+
# check surface sparseness
|
|
182
|
+
surf_mask = np.abs(coo[:, -1] - np.max(centers)) < smoothness_threshold
|
|
183
|
+
bott_mask = np.abs(coo[:, -1] - np.min(centers)) < smoothness_threshold
|
|
184
|
+
surf_coo = coo[surf_mask]
|
|
185
|
+
bott_coo = coo[bott_mask]
|
|
186
|
+
if surf_check_method == ('ConnectedGraph' or 'C'):
|
|
187
|
+
n_sub_surf = self._spectrum_clustering(surf_coo, r_cutoff=r_cutoff)
|
|
188
|
+
n_sub_bott = self._spectrum_clustering(bott_coo, r_cutoff=r_cutoff)
|
|
189
|
+
if (n_sub_surf > 1) or (n_sub_bott > 1):
|
|
190
|
+
mask[i] = False
|
|
191
|
+
elif surf_check_method == ('MinDistance'or 'M'):
|
|
192
|
+
surf_dist = np.linalg.norm(surf_coo[:, None] - surf_coo[None, :], axis=-1) + (r_cutoff + 10.) * np.eye(len(surf_coo)) # exclude the diag.
|
|
193
|
+
bott_dist = np.linalg.norm(bott_coo[:, None] - bott_coo[None, :], axis=-1) + (r_cutoff + 10.) * np.eye(len(bott_coo))
|
|
194
|
+
if (not np.all(np.sum(surf_dist < r_cutoff, axis=-1))) or (not np.all(np.sum(bott_dist < r_cutoff, axis=-1))):
|
|
195
|
+
mask[i] = False
|
|
196
|
+
return mask
|
|
197
|
+
|
|
198
|
+
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interpolation from 2 structures
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Copyright (c) 2024-2025.10.10, BUCToolkit.
|
|
6
|
+
# Authors: Pu Pengxin, Song Xin
|
|
7
|
+
# Version: 0.9a
|
|
8
|
+
# File: coords_interp.py
|
|
9
|
+
# Environment: Python 3.12
|
|
10
|
+
|
|
11
|
+
from typing import List
|
|
12
|
+
import numpy as np
|
|
13
|
+
import torch as th
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def linear_interpolation(
|
|
17
|
+
coords1: np.ndarray | List,
|
|
18
|
+
coords2: np.ndarray | List,
|
|
19
|
+
n_points: int,
|
|
20
|
+
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
Linear interpolation between `coords1` and `coords2` to generate a series of coordinates, usually used for NEB-like methods.
|
|
24
|
+
Args:
|
|
25
|
+
coords1: the coordinate of 1st structure.
|
|
26
|
+
coords2: the coordinate of 2nd structure.
|
|
27
|
+
n_points: the number of interpolation points.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
np.ndarray[(n_points, n_atoms, 3)], an array of interpolated structures' coordinates.
|
|
31
|
+
"""
|
|
32
|
+
# check vars
|
|
33
|
+
if isinstance(coords1, List):
|
|
34
|
+
coords1 = np.asarray(coords1, dtype=np.float32)
|
|
35
|
+
elif not isinstance(coords1, np.ndarray):
|
|
36
|
+
raise TypeError(f'Invalid type of `coord1`: {type(coords1)}')
|
|
37
|
+
if isinstance(coords2, List):
|
|
38
|
+
coords2 = np.asarray(coords2, dtype=np.float32)
|
|
39
|
+
elif not isinstance(coords2, np.ndarray):
|
|
40
|
+
raise TypeError(f'Invalid type of `coord2`: {type(coords2)}')
|
|
41
|
+
|
|
42
|
+
# main
|
|
43
|
+
points_indx = np.linspace(0, 1, n_points) # n_pt
|
|
44
|
+
coord_diff = coords2 - coords1 # n_atom, 3
|
|
45
|
+
interp_struc = coords1[None, :, :] + points_indx[:, None, None] * coord_diff[None, :, :] # (1, n_atom, 3) + (n_pt, 1, 1) * (1, n_atom, 3)
|
|
46
|
+
|
|
47
|
+
return interp_struc
|
|
48
|
+
|
|
49
|
+
def linear_interpolation_tens(
|
|
50
|
+
coords1: th.Tensor,
|
|
51
|
+
coords2: th.Tensor,
|
|
52
|
+
n_points: int,
|
|
53
|
+
atom_mask: th.Tensor|None = None,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
The linear interpolation between `coords1` and `coords2` for torch.Tensor inputs. See `linear_interpolation`.
|
|
57
|
+
Args:
|
|
58
|
+
coords1: the coordinate of 1st structure.
|
|
59
|
+
coords2: the coordinate of 2nd structure.
|
|
60
|
+
n_points: the number of interpolation points.
|
|
61
|
+
atom_mask: the mask of `coords1` and `coords2` that fixes atoms not to move. wherein, 1 is for free and 0 is for fixation.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
th.Tensor[(n_points, n_atoms, n_dim)], an array of interpolated structures' coordinates.
|
|
65
|
+
"""
|
|
66
|
+
# check
|
|
67
|
+
if atom_mask is None:
|
|
68
|
+
atom_mask = th.ones_like(coords1)[None, :, :]
|
|
69
|
+
elif (isinstance(atom_mask, th.Tensor)) or isinstance(atom_mask, np.ndarray):
|
|
70
|
+
if atom_mask.shape != coords1.shape:
|
|
71
|
+
raise TypeError(f'Invalid shape of `atom_mask`: {atom_mask.shape}. It should be the same as `coord1`: {coords1.shape}.')
|
|
72
|
+
atom_mask = atom_mask[None, :, :]
|
|
73
|
+
else:
|
|
74
|
+
raise TypeError(f'Invalid type of `atom_mask`: {type(atom_mask)}')
|
|
75
|
+
# main
|
|
76
|
+
points_indx = th.linspace(0, 1, n_points, device=coords1.device, dtype=coords1.dtype) # n_pt
|
|
77
|
+
coord_diff = coords2 - coords1 # n_atom, 3
|
|
78
|
+
interp_struc = coords1[None, :, :] + points_indx[:, None, None] * coord_diff[None, :, :] * atom_mask # (1, n_atom, 3) + (n_pt, 1, 1) * (1, n_atom, 3)
|
|
79
|
+
|
|
80
|
+
return interp_struc
|
|
81
|
+
|
|
82
|
+
def direction_for_finite_diff(
|
|
83
|
+
coords1: th.Tensor | np.ndarray,
|
|
84
|
+
coords2: th.Tensor | np.ndarray,
|
|
85
|
+
atom_mask: th.Tensor | np.ndarray | None = None,
|
|
86
|
+
interp_position: float = 0.5,
|
|
87
|
+
):
|
|
88
|
+
"""
|
|
89
|
+
Generate a direction for finite difference. Usually used for Dimer/Lanczos algo.
|
|
90
|
+
Args:
|
|
91
|
+
coords1: the initial coordinate.
|
|
92
|
+
coords2: the final coordinate.
|
|
93
|
+
atom_mask: the mask of `coords1` and `coords2` that fixes atoms not to move. wherein, 1 is for free and 0 is for fixation.
|
|
94
|
+
interp_position: the interpolation position of returned interp. coordinate. It must be between 0 and 1,
|
|
95
|
+
and gives the coordinate of `coords1 + interp_position * (coords2 - coords1)`.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
interp_coo: th.Tensor[(n_points, n_atoms, n_dim)], an array of interpolated structures' coordinates.
|
|
99
|
+
diff_coo : the normalized difference between `coords1` and `coords2`, i.e., a unit vector.
|
|
100
|
+
"""
|
|
101
|
+
# check
|
|
102
|
+
if not isinstance(coords1, th.Tensor):
|
|
103
|
+
dev = 'cpu'
|
|
104
|
+
else:
|
|
105
|
+
dev = coords1.device
|
|
106
|
+
if atom_mask is None:
|
|
107
|
+
atom_mask = th.ones_like(coords1)[None, :, :]
|
|
108
|
+
elif isinstance(atom_mask, th.Tensor) or isinstance(atom_mask, np.ndarray):
|
|
109
|
+
if atom_mask.shape != coords1.shape:
|
|
110
|
+
raise TypeError(f'Invalid shape of `atom_mask`: {atom_mask.shape}. It should be the same as `coord1`: {coords1.shape}.')
|
|
111
|
+
atom_mask = atom_mask[None, :, :]
|
|
112
|
+
atom_mask = th.as_tensor(atom_mask, dtype=atom_mask.dtype, device=dev)
|
|
113
|
+
else:
|
|
114
|
+
raise TypeError(f'Invalid type of `atom_mask`: {type(atom_mask)}')
|
|
115
|
+
|
|
116
|
+
coords1 = th.as_tensor(coords1, dtype=coords1.dtype, device=dev)
|
|
117
|
+
coords2 = th.as_tensor(coords2, dtype=coords2.dtype, device=dev)
|
|
118
|
+
|
|
119
|
+
interp_position = float(interp_position)
|
|
120
|
+
if interp_position > 1. or interp_position < 0.:
|
|
121
|
+
raise ValueError(f"`interp_position` should be between 0 and 1.")
|
|
122
|
+
diff_coo = (coords2 - coords1) * atom_mask
|
|
123
|
+
interp_coo = th.add(coords1, diff_coo, alpha=interp_position)
|
|
124
|
+
th.div(diff_coo, th.linalg.norm(diff_coo, dim=(-2, -1), keepdim=True), out=diff_coo)
|
|
125
|
+
|
|
126
|
+
return interp_coo, diff_coo
|
|
127
|
+
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# Copyright (c) 2024-2025.7.4, BUCToolkit.
|
|
2
|
+
# Authors: Pu Pengxin, Song Xin
|
|
3
|
+
# Version: 0.9a
|
|
4
|
+
# File: initial_ads_structures.py
|
|
5
|
+
# Environment: Python 3.12
|
|
6
|
+
|
|
7
|
+
import copy
|
|
8
|
+
from typing import List, Literal, Dict
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
import torch as th
|
|
12
|
+
from torch import nn
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
from BUCToolkit.BatchStructures.BatchStructuresBase import BatchStructures
|
|
16
|
+
from BUCToolkit.utils.IrregularTensorReformat import IrregularTensorReformat
|
|
17
|
+
from BUCToolkit.BatchOptim.minimize import CG, QN, FIRE
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class InitAdsStructure:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
slabs: BatchStructures,
|
|
24
|
+
ads: th.Tensor,
|
|
25
|
+
ads_elements: Dict,
|
|
26
|
+
vaccuum_direction: Literal['x', 'y', 'z'] = 'z',
|
|
27
|
+
device: str | th.device='cpu',
|
|
28
|
+
verbose:int = 0):
|
|
29
|
+
"""
|
|
30
|
+
Generate initial guesses of surface adsorption structures.
|
|
31
|
+
Args:
|
|
32
|
+
slabs: batch structures of slabs.
|
|
33
|
+
ads: Tensor(n_atom, 3), coordinates of the adsorbate.
|
|
34
|
+
ads_elements: Dict(element symbol: atom number).
|
|
35
|
+
vaccuum_direction: direction of vacuum layers.
|
|
36
|
+
device: the device that program run on.
|
|
37
|
+
verbose: to control the verboseness of output information.
|
|
38
|
+
"""
|
|
39
|
+
self.slabs = slabs
|
|
40
|
+
self.ads = ads.to(device)
|
|
41
|
+
self.ads_elem = ads_elements
|
|
42
|
+
self.verbose = verbose
|
|
43
|
+
self.device = device
|
|
44
|
+
n_batch = len(slabs)
|
|
45
|
+
if not isinstance(self.slabs, BatchStructures):
|
|
46
|
+
raise TypeError(f'`slabs` must be BatchStructures, but occurred {type(slabs)}')
|
|
47
|
+
if not isinstance(self.ads, th.Tensor):
|
|
48
|
+
raise TypeError(f'`ads` must be torch.Tensor, but occurred {type(ads)}')
|
|
49
|
+
if sum(ads_elements.values()) != len(self.ads):
|
|
50
|
+
raise ValueError(f'Number of atoms in `ads_elements` does not match it in `ads`.')
|
|
51
|
+
|
|
52
|
+
# vacuum dirct
|
|
53
|
+
vac_d: dict = {'x': 0, 'y': 1, 'z': 2}
|
|
54
|
+
vac_d: int = vac_d[vaccuum_direction]
|
|
55
|
+
|
|
56
|
+
# dist. mat. of ads. mol.
|
|
57
|
+
self.dist_mol = th.linalg.norm(self.ads.unsqueeze(0) - self.ads.unsqueeze(1), dim=-1) # (n_atom, n_atom)
|
|
58
|
+
max_atom_len = th.max(self.dist_mol)
|
|
59
|
+
# coord of slabs
|
|
60
|
+
# TODO: Convert to Cartesian coordinates.
|
|
61
|
+
self.slab_coords = [th.from_numpy(_).to(self.device) for _ in self.slabs.Coords]
|
|
62
|
+
self.slab_cells = [th.from_numpy(_).to(self.device) for _ in self.slabs.Cells]
|
|
63
|
+
# check vacuum length
|
|
64
|
+
vacuum_length_pak = [(self.slab_cells[i][vac_d, vac_d] - th.max(self.slab_coords[i][:, vac_d]), th.max(self.slab_coords[i][:, vac_d])) for i in range(len(slabs))]
|
|
65
|
+
vacuum_length, highest_slab = zip(*vacuum_length_pak)
|
|
66
|
+
highest_slab= th.tensor(highest_slab) # (n_batch, )
|
|
67
|
+
for i, _vac_len in enumerate(vacuum_length):
|
|
68
|
+
if _vac_len < 3.:
|
|
69
|
+
raise RuntimeError(f'Occurred unacceptable vacuum length {_vac_len} in {i}th structure. I HOPE YOU KNOWN WHAT YOU ARE DOING.')
|
|
70
|
+
elif _vac_len < 9.:
|
|
71
|
+
warnings.warn(f'The vacuum length of {i}th structure was too short: {vacuum_length}.')
|
|
72
|
+
elif (_vac_len < (max_atom_len + 5.)) or th.any(th.linalg.norm(self.slab_cells[i], dim=1) - 5. < max_atom_len):
|
|
73
|
+
warnings.warn(f'The {i}th cell are too small that distance between 2 adsorbates in neighbor lattice might be less than 5 Angstrom.')
|
|
74
|
+
# set the lowest atom in ads to 3 Angstrom higher than the highest atom in slab
|
|
75
|
+
lowest_mol = th.min(self.ads[:, vac_d], dim=-1).values # (n_batch, )
|
|
76
|
+
self.ads = (self.ads.unsqueeze(0)).expand(n_batch, -1, -1) # (n_batch, n_mol_atom, n_dim)
|
|
77
|
+
self.ads = self.ads + (highest_slab + 3. - lowest_mol).unsqueeze(-1).unsqueeze(-1)
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
def run(
|
|
81
|
+
self,
|
|
82
|
+
slab_site: List[List[int]],
|
|
83
|
+
ads_site: List[int],
|
|
84
|
+
bond_length: float,
|
|
85
|
+
bond_coeff: float = 5.,
|
|
86
|
+
non_bond_coeff: float = 3.,
|
|
87
|
+
keep_ads_conf_coeff: float = 2.
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
slab_site: The indices of adsorption site in slabs.
|
|
93
|
+
ads_site: The indices of adsorption site in adsorbate.
|
|
94
|
+
bond_length: The target bond length of adsorbate and slabs.
|
|
95
|
+
bond_coeff: Strength coefficient of ads-slab bond.
|
|
96
|
+
non_bond_coeff: Strength coefficient of repulsiveness of non-bond atom between ads. and slab.
|
|
97
|
+
keep_ads_conf_coeff: Strength coefficient of keeping original adsorbate configuration.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
BatchStructures: the batch structures of slabs with ads.
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
regularizor = IrregularTensorReformat()
|
|
104
|
+
slab_pad_coords, pad_mask = regularizor.regularize(self.slab_coords, 0.)
|
|
105
|
+
n_batch, n_atom, n_dim = slab_pad_coords.shape
|
|
106
|
+
# slabs
|
|
107
|
+
if isinstance(slab_site, List):
|
|
108
|
+
assert len(slab_site) == n_batch, f'number of specified adsorption sites {len(slab_site)} does not match the number of structures {n_batch}.'
|
|
109
|
+
#assert len(bond_length) == n_batch, f'number of specified adsorption sites {len(slab_site)} does not match the number of structures {n_batch}.'
|
|
110
|
+
slab_mask = th.full_like(slab_pad_coords, False, device=self.device, dtype=th.bool) # (n_batch, n_atom, n_dim)
|
|
111
|
+
#target_bond_length = th.full((n_batch, n_atom), 0., device=self.device) # (n_batch, n_atom) # TODO auto-adaptive bond length
|
|
112
|
+
target_bond_length = bond_length
|
|
113
|
+
for i, _sites in enumerate(slab_site):
|
|
114
|
+
slab_mask[i, _sites] = True
|
|
115
|
+
#target_bond_length[i, _sites] = th.tensor(bond_length[i], device=self.device) # TODO auto-adaptive bond length
|
|
116
|
+
slab_mask = slab_mask * pad_mask
|
|
117
|
+
else:
|
|
118
|
+
raise TypeError('`slab_site` must be a List[List[int]].')
|
|
119
|
+
# ads molecules
|
|
120
|
+
if isinstance(ads_site, List):
|
|
121
|
+
ads_mask = th.full_like(self.ads[0], False, device=self.device, dtype=th.bool) # (n_mol_atom, n_dim)
|
|
122
|
+
ads_mask[ads_site] = True
|
|
123
|
+
else:
|
|
124
|
+
raise TypeError('`ads_site` must be a List[int].')
|
|
125
|
+
# calc. force
|
|
126
|
+
loss_fn = _Loss()
|
|
127
|
+
#self._random_perturbate()
|
|
128
|
+
|
|
129
|
+
# optimize
|
|
130
|
+
optimizer = FIRE(
|
|
131
|
+
1e-3,
|
|
132
|
+
0.1,
|
|
133
|
+
200,
|
|
134
|
+
0.01,
|
|
135
|
+
device=self.device,
|
|
136
|
+
verbose=self.verbose
|
|
137
|
+
)
|
|
138
|
+
min_loss, X_ads = optimizer.run(
|
|
139
|
+
loss_fn,
|
|
140
|
+
self.ads,
|
|
141
|
+
None,
|
|
142
|
+
(slab_pad_coords, slab_mask, ads_mask, self.dist_mol, target_bond_length, bond_coeff, non_bond_coeff, keep_ads_conf_coeff),
|
|
143
|
+
None,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
ads_structures = self._integrate_coords(X_ads)
|
|
147
|
+
return ads_structures
|
|
148
|
+
|
|
149
|
+
def _integrate_coords(self, X_ads: th.Tensor) -> BatchStructures:
|
|
150
|
+
"""
|
|
151
|
+
Integrate coordinates of adsorption and slabs.
|
|
152
|
+
Args:
|
|
153
|
+
X_ads: coordinates of ads.
|
|
154
|
+
|
|
155
|
+
Returns: BatchStructure of adsorbed slabs.
|
|
156
|
+
|
|
157
|
+
"""
|
|
158
|
+
ads_elem = list(self.ads_elem.keys())
|
|
159
|
+
ads_number = [self.ads_elem[_] for _ in ads_elem]
|
|
160
|
+
f = copy.deepcopy(self.slabs)
|
|
161
|
+
X_ads = X_ads.numpy(force=True)
|
|
162
|
+
for i in range(len(f)):
|
|
163
|
+
f.Coords[i] = np.concatenate([f.Coords[i], X_ads[i]])
|
|
164
|
+
f.Fixed[i] = np.concatenate([f.Fixed[i], np.ones_like(X_ads[i])])
|
|
165
|
+
f.Elements[i].extend(ads_elem)
|
|
166
|
+
f.Numbers[i].extend(ads_number)
|
|
167
|
+
if f.Atom_list is not None:
|
|
168
|
+
f.generate_atom_list(True)
|
|
169
|
+
if f.Atomic_number_list is not None:
|
|
170
|
+
f.generate_atomic_number_list(True)
|
|
171
|
+
return f
|
|
172
|
+
|
|
173
|
+
def _random_perturbate(self, ):
|
|
174
|
+
"""
|
|
175
|
+
Ramdom perturbate adsorptions by orthogonal and translational transform to search more configuration.
|
|
176
|
+
Returns:
|
|
177
|
+
|
|
178
|
+
"""
|
|
179
|
+
ln = len(self.ads)
|
|
180
|
+
rx = th.rand(ln) * 2 * th.pi
|
|
181
|
+
ry = th.rand(ln) * 2 * th.pi
|
|
182
|
+
rz = th.rand(ln) * 2 * th.pi
|
|
183
|
+
tx = th.randn(ln) * 0.05
|
|
184
|
+
ty = th.randn(ln) * 0.05
|
|
185
|
+
tz = th.randn(ln) * 0.05
|
|
186
|
+
|
|
187
|
+
self.ads = self._distance_preserv_transform(rx, ry ,rz, tx, ty, tz, self.ads)
|
|
188
|
+
|
|
189
|
+
def _distance_preserv_transform(
|
|
190
|
+
self,
|
|
191
|
+
rx: th.Tensor,
|
|
192
|
+
ry: th.Tensor,
|
|
193
|
+
rz: th.Tensor,
|
|
194
|
+
tx: th.Tensor,
|
|
195
|
+
ty: th.Tensor,
|
|
196
|
+
tz: th.Tensor,
|
|
197
|
+
X: th.Tensor
|
|
198
|
+
) -> th.Tensor:
|
|
199
|
+
"""
|
|
200
|
+
Orthogonal and transitional transform on input X.
|
|
201
|
+
Args:
|
|
202
|
+
rx: rotation degree around X-axis.
|
|
203
|
+
ry: rotation degree around Y-axis.
|
|
204
|
+
rz: rotation degree around Z-axis.
|
|
205
|
+
tx: transition along X-axis.
|
|
206
|
+
ty: transition along Y-axis.
|
|
207
|
+
tz: transition along Z-axis.
|
|
208
|
+
X: the coordinated to transform.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
|
|
212
|
+
"""
|
|
213
|
+
ln = len(X)
|
|
214
|
+
if not (len(rx) == len(ry) == len(rz) == len(tx) == len(ty) == len(tz) == ln):
|
|
215
|
+
raise RuntimeError(f'Incompatible shape of rx~rz & tx~tz & X. They must have same batch number.')
|
|
216
|
+
# mass center
|
|
217
|
+
center = th.sum(X, dim=1, keepdim=True) # (n_batch, 1, 3)
|
|
218
|
+
# rotation matrix
|
|
219
|
+
rot_x = th.tensor(
|
|
220
|
+
[
|
|
221
|
+
[[1.]*ln, [0.]*ln, [0.]*ln],
|
|
222
|
+
[[0.]*ln, th.cos(rx).tolist(), th.sin(-rx).tolist()],
|
|
223
|
+
[[0.]*ln, th.sin(rx).tolist(), th.cos(rx).tolist()]
|
|
224
|
+
],
|
|
225
|
+
device=self.device
|
|
226
|
+
)
|
|
227
|
+
rot_y = th.tensor(
|
|
228
|
+
[
|
|
229
|
+
[th.cos(ry).tolist(), [0.]*ln, th.sin(ry).tolist()],
|
|
230
|
+
[[0.]*ln, [1.]*ln, [0.]*ln],
|
|
231
|
+
[th.sin(-ry).tolist(), [0.]*ln, th.cos(ry).tolist()]
|
|
232
|
+
],
|
|
233
|
+
device=self.device
|
|
234
|
+
)
|
|
235
|
+
rot_z = th.tensor(
|
|
236
|
+
[
|
|
237
|
+
[th.cos(rz).tolist(), th.sin(-rz).tolist(), [0.]*ln],
|
|
238
|
+
[th.sin(rz).tolist(), th.cos(rz).tolist(), [0.]*ln],
|
|
239
|
+
[ [0.]*ln, [0.]*ln, [1.]*ln]
|
|
240
|
+
],
|
|
241
|
+
device=self.device
|
|
242
|
+
)
|
|
243
|
+
rot_x = rot_x.transpose(0, -1)
|
|
244
|
+
rot_y = rot_y.transpose(0, -1)
|
|
245
|
+
rot_z = rot_z.transpose(0, -1)
|
|
246
|
+
trans = th.cat((tx.unsqueeze(-1).unsqueeze(-1), ty.unsqueeze(-1).unsqueeze(-1), tz.unsqueeze(-1).unsqueeze(-1)), dim=-1) # (n_batch, 1, 3)
|
|
247
|
+
X_trans = rot_z @ rot_y @ rot_x @ (X - center) + center + trans # (n_batch, n_atom, 3)
|
|
248
|
+
|
|
249
|
+
return X_trans
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def get_highest_atom_indices(structure: BatchStructures, vaccuum_direction: Literal['x', 'y', 'z'] = 'z',):
|
|
253
|
+
"""
|
|
254
|
+
Get the highest atom index of given batch structures.
|
|
255
|
+
Returns:
|
|
256
|
+
List[List[int]], the indices of highest atom in each structure
|
|
257
|
+
"""
|
|
258
|
+
# vacuum dirct
|
|
259
|
+
vac_d: dict = {'x': 0, 'y': 1, 'z': 2}
|
|
260
|
+
vac_d: int = vac_d[vaccuum_direction]
|
|
261
|
+
# coords
|
|
262
|
+
converter = IrregularTensorReformat()
|
|
263
|
+
padded_coords = [th.from_numpy(_) for _ in structure.Coords]
|
|
264
|
+
padded_coords, pad_mask = converter.regularize(padded_coords, -th.inf) # (n_batch, n_atom, 3)
|
|
265
|
+
indices = th.argmax(padded_coords[:, :, vac_d], dim=1)
|
|
266
|
+
|
|
267
|
+
return indices.unsqueeze(-1).tolist()
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class _Loss(nn.Module):
|
|
271
|
+
def __init__(self):
|
|
272
|
+
super().__init__()
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
def forward(self, X_ads, X_slab, slab_mask, ads_mask, mol_origin_dist_mat, target_bond_len, c_b, c_nb, c_k):
|
|
276
|
+
"""
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
X_ads: (n_batch, n_mol_atom, n_dim)
|
|
280
|
+
X_slab: (n_batch, n_atom, n_dim)
|
|
281
|
+
slab_mask: (n_batch, n_atom, n_dim)
|
|
282
|
+
ads_mask: (n_mol_atom, n_dim)
|
|
283
|
+
mol_origin_dist_mat: (n_mol_atom, n_mol_atom)
|
|
284
|
+
target_bond_len: float
|
|
285
|
+
c_b:
|
|
286
|
+
c_nb:
|
|
287
|
+
c_k:
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
|
|
291
|
+
"""
|
|
292
|
+
loss = (c_b * self._bond_force_field(X_ads, X_slab, slab_mask, ads_mask, target_bond_len) +
|
|
293
|
+
c_nb * self._non_bond_loss(X_ads, X_slab, slab_mask, ads_mask) +
|
|
294
|
+
c_k * self._keep_origin_loss(X_ads, mol_origin_dist_mat))
|
|
295
|
+
return loss
|
|
296
|
+
|
|
297
|
+
@staticmethod
|
|
298
|
+
def _bond_force_field(X_ads, X_slab, slab_mask: th.Tensor, ads_mask: th.Tensor, target_bond_len: float):
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
X_ads:
|
|
303
|
+
X_slab:
|
|
304
|
+
slab_mask:
|
|
305
|
+
ads_mask:
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
|
|
309
|
+
"""
|
|
310
|
+
# (n_batch, n_atom, 1, n_dim) - (n_batch, 1, n_mol_atom, n_dim) -> (n_batch, n_atom, n_mol_atom, n_dim)
|
|
311
|
+
dist_nb = th.linalg.norm(X_slab.unsqueeze(2) - X_ads.unsqueeze(1), dim=-1)
|
|
312
|
+
# quadratic potential
|
|
313
|
+
b_loss = (dist_nb - target_bond_len)**2 # (n_batch, n_atom, n_mol_atom)
|
|
314
|
+
# tol mask
|
|
315
|
+
tol_mask = (slab_mask.unsqueeze(-2) * ads_mask.unsqueeze(0).unsqueeze(0))[..., 0] # (n_batch, n_atom, 1, n_dim) * (1, 1, n_mol_atom, n_dim) -> (n_batch, n_atom, n_mol_atom, n_dim)
|
|
316
|
+
b_loss = th.sum(b_loss * tol_mask, dim=(-1, -2))
|
|
317
|
+
return b_loss
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def _non_bond_loss(X_ads, X_slab, slab_mask: th.Tensor, ads_mask: th.Tensor):
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
X_ads:
|
|
325
|
+
X_slab:
|
|
326
|
+
slab_mask: (n_batch, n_atom, n_dim)
|
|
327
|
+
ads_mask: (n_mol_atom, n_dim)
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
|
|
331
|
+
"""
|
|
332
|
+
# (n_batch, n_atom, 1, n_dim) - (n_batch, 1, n_mol_atom, n_dim) -> (n_batch, n_atom, n_mol_atom, n_dim) -> (n_batch, n_atom, n_mol_atom)
|
|
333
|
+
dist_nb = th.linalg.norm(X_slab.unsqueeze(2) - X_ads.unsqueeze(1), dim=-1)
|
|
334
|
+
# exponential repulsive
|
|
335
|
+
n_b_loss = th.where(dist_nb < 3., th.exp(6./dist_nb) - 1, 0.) # (n_batch, n_atom, n_mol_atom)
|
|
336
|
+
# polynomial repulsive
|
|
337
|
+
#n_b_loss = (th.linalg.norm(dist_nb, dim=-1) - 3.5)**3
|
|
338
|
+
#n_b_loss = th.where(n_b_loss < 0., 0., n_b_loss) # (n_batch, n_atom, n_mol_atom)
|
|
339
|
+
# half L-J repulsive
|
|
340
|
+
#n_b_loss = 5./(th.linalg.norm(dist_nb, dim=-1))**12 # (n_batch, n_atom, n_mol_atom)
|
|
341
|
+
# tol mask
|
|
342
|
+
tol_mask = (~slab_mask.unsqueeze(-2) * ~ads_mask.unsqueeze(0).unsqueeze(0))[..., 0] # (n_batch, n_atom, 1, n_dim) * (1, 1, n_mol_atom, n_dim) -> (n_batch, n_atom, n_mol_atom, n_dim)
|
|
343
|
+
n_b_loss = th.sum(n_b_loss * tol_mask, dim=(-1, -2))
|
|
344
|
+
return n_b_loss
|
|
345
|
+
|
|
346
|
+
@staticmethod
|
|
347
|
+
def _keep_origin_loss(X_ads, origin_dist_mat):
|
|
348
|
+
dist_mat = th.linalg.norm(X_ads.unsqueeze(1) - X_ads.unsqueeze(2), dim=-1) # (n_batch, n_mol_atom, n_mol_atom)
|
|
349
|
+
k_o_loss = th.sum((dist_mat - origin_dist_mat)**2)
|
|
350
|
+
return k_o_loss
|
|
351
|
+
|