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,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,6 @@
1
+ # Copyright (c) 2024-2025.7.4, BUCToolkit.
2
+ # Authors: Pu Pengxin, Song Xin
3
+ # Version: 0.9a
4
+ # File: __init__.py
5
+ # Environment: Python 3.12
6
+
@@ -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
+