surface-construct 0.10.3__tar.gz → 0.11__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.
Files changed (39) hide show
  1. {surface_construct-0.10.3/surface_construct.egg-info → surface_construct-0.11}/PKG-INFO +17 -1
  2. {surface_construct-0.10.3 → surface_construct-0.11}/README.md +16 -0
  3. {surface_construct-0.10.3 → surface_construct-0.11}/setup.py +1 -1
  4. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct/__init__.py +4 -1
  5. surface_construct-0.11/surface_construct/structures/adsorbate.py +384 -0
  6. surface_construct-0.11/surface_construct/structures/combiner.py +143 -0
  7. surface_construct-0.11/surface_construct/structures/pymsym_test.py +30 -0
  8. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct/structures/surface_grid.py +27 -4
  9. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct/tasks/sitesampling.py +75 -44
  10. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct/tasks/taskbase.py +3 -2
  11. surface_construct-0.11/surface_construct/tasks/terminations.py +0 -0
  12. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct/utils/__init__.py +8 -0
  13. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct/utils/atoms.py +90 -2
  14. surface_construct-0.11/surface_construct/utils/geometry.py +389 -0
  15. surface_construct-0.11/surface_construct/utils/pymsym_wrapper.py +73 -0
  16. surface_construct-0.11/surface_construct/utils/spglib_wrapper.py +194 -0
  17. {surface_construct-0.10.3 → surface_construct-0.11/surface_construct.egg-info}/PKG-INFO +17 -1
  18. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct.egg-info/SOURCES.txt +7 -0
  19. surface_construct-0.11/tests/test_adsorbate.py +101 -0
  20. surface_construct-0.11/tests/test_combiner.py +61 -0
  21. {surface_construct-0.10.3 → surface_construct-0.11}/tests/test_surface_grid.py +0 -2
  22. {surface_construct-0.10.3 → surface_construct-0.11}/tests/test_task.py +34 -4
  23. surface_construct-0.10.3/surface_construct/structures/__init__.py +0 -50
  24. surface_construct-0.10.3/surface_construct/structures/adsorbate.py +0 -38
  25. {surface_construct-0.10.3 → surface_construct-0.11}/LICENSE +0 -0
  26. {surface_construct-0.10.3 → surface_construct-0.11}/setup.cfg +0 -0
  27. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct/db.py +0 -0
  28. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct/default_parameter.py +0 -0
  29. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct/sg_sampler.py +0 -0
  30. /surface_construct-0.10.3/surface_construct/tasks/terminations.py → /surface_construct-0.11/surface_construct/structures/__init__.py +0 -0
  31. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct/structures/surface.py +0 -0
  32. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct/tasks/__init__.py +0 -0
  33. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct/utils/weight_functions.py +0 -0
  34. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct.egg-info/dependency_links.txt +0 -0
  35. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct.egg-info/requires.txt +0 -0
  36. {surface_construct-0.10.3 → surface_construct-0.11}/surface_construct.egg-info/top_level.txt +0 -0
  37. {surface_construct-0.10.3 → surface_construct-0.11}/tests/test_sampling1.py +0 -0
  38. {surface_construct-0.10.3 → surface_construct-0.11}/tests/test_sampling2.py +0 -0
  39. {surface_construct-0.10.3 → surface_construct-0.11}/tests/test_simple_surface.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: surface_construct
3
- Version: 0.10.3
3
+ Version: 0.11
4
4
  Summary: Surface termination construction especially for complex model, such as oxides or carbides.
5
5
  Home-page: https://gitee.com/pjren/surface_construct/
6
6
  Author: ren
@@ -34,6 +34,21 @@ Dynamic: summary
34
34
 
35
35
  本软件同时进行表面位点的构建、采样、计算、分析,和反应过程的分析采样,即过渡态和分子构象,由此可以实现分子在表面反应的全过程采样。其中的技术关键是采样的效率,我们针对性地使用综合使用了多样化的采样策略,保证表面各类型位点和关键位点的充分采样。
36
36
 
37
+ ### 应用场景
38
+ * 催化剂表面最可能反应位点采样
39
+ * 适用于催化剂活性快速评价
40
+ * VIP 采样:位点(S,site),二面角构象(C,conformation),旋转姿态(R,rotation)
41
+ * 优化 C、R、Z (z方向,距离表面的距离)空间,固定COM
42
+ * 返回所采结构中最低能量和对应的结构和能量
43
+
44
+ * 催化剂表面全势能面采样
45
+ * 适用于催化剂位点全局考察
46
+ * 适用于表面分子全局的构象和姿态
47
+ * 适用于反应全过程采样
48
+ * 不优化,或者仅优化Z空间
49
+ * 返回全部能量和结构,以及全局的fitting结果
50
+ * 使用能量截断(dEmax<2eV)
51
+
37
52
  <img src="docs/birds_view.png" alt="表面位点全局分析" style="zoom:50%;" />
38
53
 
39
54
  ## 程序流程图 Program Workflow
@@ -157,6 +172,7 @@ $$
157
172
  * 计算位点 SALI (Structure-Activity Landscape Index),量化 activity cliffs,SALI = |ΔActivity| / (1 − Similarity)
158
173
  * 新增 PathSampler,用于在相邻的关键格点之间的扩散路径进行采样,使用 ase.neb.idpp 方法进行采样,使用 Delaunay 剖分找到相邻位点的路径。
159
174
  * Low-Energy Region Explorer 方法借鉴[^LoreX]: 划分能量区域, 进行 Delaunay 划分,合并较小的区域为较大的区域。然后针对低能的区域进行额外采样。也可以适用不同的方法,低精度进行低能高能区域识别,然后高精度方法加强。
175
+ * 表面格点使用对称性进行降维。在最初期就引入对称性。参考 adsorbate rotation 思路
160
176
  * 表面位点数据库
161
177
  * 多原子体系(内坐标受限体系)
162
178
  * 完善用户界面、例子、教程
@@ -4,6 +4,21 @@
4
4
 
5
5
  本软件同时进行表面位点的构建、采样、计算、分析,和反应过程的分析采样,即过渡态和分子构象,由此可以实现分子在表面反应的全过程采样。其中的技术关键是采样的效率,我们针对性地使用综合使用了多样化的采样策略,保证表面各类型位点和关键位点的充分采样。
6
6
 
7
+ ### 应用场景
8
+ * 催化剂表面最可能反应位点采样
9
+ * 适用于催化剂活性快速评价
10
+ * VIP 采样:位点(S,site),二面角构象(C,conformation),旋转姿态(R,rotation)
11
+ * 优化 C、R、Z (z方向,距离表面的距离)空间,固定COM
12
+ * 返回所采结构中最低能量和对应的结构和能量
13
+
14
+ * 催化剂表面全势能面采样
15
+ * 适用于催化剂位点全局考察
16
+ * 适用于表面分子全局的构象和姿态
17
+ * 适用于反应全过程采样
18
+ * 不优化,或者仅优化Z空间
19
+ * 返回全部能量和结构,以及全局的fitting结果
20
+ * 使用能量截断(dEmax<2eV)
21
+
7
22
  <img src="docs/birds_view.png" alt="表面位点全局分析" style="zoom:50%;" />
8
23
 
9
24
  ## 程序流程图 Program Workflow
@@ -127,6 +142,7 @@ $$
127
142
  * 计算位点 SALI (Structure-Activity Landscape Index),量化 activity cliffs,SALI = |ΔActivity| / (1 − Similarity)
128
143
  * 新增 PathSampler,用于在相邻的关键格点之间的扩散路径进行采样,使用 ase.neb.idpp 方法进行采样,使用 Delaunay 剖分找到相邻位点的路径。
129
144
  * Low-Energy Region Explorer 方法借鉴[^LoreX]: 划分能量区域, 进行 Delaunay 划分,合并较小的区域为较大的区域。然后针对低能的区域进行额外采样。也可以适用不同的方法,低精度进行低能高能区域识别,然后高精度方法加强。
145
+ * 表面格点使用对称性进行降维。在最初期就引入对称性。参考 adsorbate rotation 思路
130
146
  * 表面位点数据库
131
147
  * 多原子体系(内坐标受限体系)
132
148
  * 完善用户界面、例子、教程
@@ -15,7 +15,7 @@ install_requires = [
15
15
 
16
16
  setup(
17
17
  name='surface_construct',
18
- version='0.10.3',
18
+ version='0.11',
19
19
  packages=find_packages(),
20
20
  url='https://gitee.com/pjren/surface_construct/',
21
21
  license='GPL',
@@ -1,7 +1,8 @@
1
1
  from surface_construct.structures.surface import Crystal, Surface, Slab, Termination
2
2
  from surface_construct.structures.surface import get_terminations_score
3
3
  from surface_construct.structures.surface_grid import SurfaceGrid, GridGenerator
4
-
4
+ from surface_construct.structures.adsorbate import Adsorbate
5
+ from surface_construct.structures.combiner import AdsGridCombiner
5
6
 
6
7
  __all__ = ['SurfaceGrid',
7
8
  'GridGenerator',
@@ -9,4 +10,6 @@ __all__ = ['SurfaceGrid',
9
10
  'Surface',
10
11
  'Slab',
11
12
  'Termination',
13
+ 'Adsorbate',
14
+ 'AdsGridCombiner',
12
15
  ]
@@ -0,0 +1,384 @@
1
+ import ase
2
+ import numpy as np
3
+ from typing import Tuple, Sequence, Dict, Union
4
+ from ase.geometry.analysis import Analysis as ase_Analysis
5
+ from ase.geometry.distance import distance as ase_distance
6
+ from scipy.spatial import cKDTree
7
+
8
+ from surface_construct.utils.atoms import atoms2graph, atoms_regulate, set_dihedrals
9
+ from surface_construct.utils.geometry import dih_grid, sample_rotations_with_symmetry, view_samples, \
10
+ estimate_rotation_samples
11
+ from surface_construct.utils.pymsym_wrapper import get_pure_rotations, get_point_group, get_Cn
12
+
13
+
14
+ def get_dock_point_idx(atoms_pos, x_pos):
15
+ if len(x_pos) == 0:
16
+ return []
17
+ tree = cKDTree(atoms_pos)
18
+ x_tree = cKDTree(x_pos)
19
+ dd = tree.sparse_distance_matrix(x_tree, max_distance=2.0, p=2).toarray()
20
+ idx = dd.argmin(axis=0).reshape(1,-1)
21
+ return idx
22
+
23
+ class Adsorbate:
24
+ """
25
+ 吸附分子的类,类似与表面格点类,它包含了所有可能的姿态和二面角构象的离散化空间。
26
+ 还可以计算分子的质心,内坐标,主轴。
27
+ """
28
+ def __init__(self, atoms:ase.Atoms, **kwargs):
29
+ # TODO:加上半径的参数,计算分子半径
30
+ self.atoms = atoms.copy()
31
+ del self.atoms[atoms.numbers == 0]
32
+ self.dock_point_indices = get_dock_point_idx(self.atoms.positions,
33
+ atoms.positions[atoms.numbers == 0])
34
+ self._atoms_graph = None
35
+ self.nl = kwargs.get('nl', None)
36
+ self.analysis = ase_Analysis(self.atoms, nl=self.nl)
37
+ if self.nl is None:
38
+ self.nl = self.analysis.nl
39
+ self._adj_matrix = self.analysis.adjacency_matrix[0].toarray()
40
+ self.internal_coords = dict()
41
+ self.kwargs = kwargs
42
+ self.is_regulated = False
43
+ self.rtype = kwargs.get('rtype', 'covalent_radii')
44
+ self._rads = None
45
+ self._all_dihedrals = None
46
+ self._dihedral_grid = None
47
+ if len(self.atoms) < 4:
48
+ self._all_dihedrals = []
49
+ self._dihedral_grid = []
50
+ self._dihedral_mask_dct = dict()
51
+ self._dihedral_delta = None
52
+ self._rotation_delta = None
53
+ self._symmetry_rotations = None
54
+ self._symmetry_rotation_operations = None
55
+ self._rotation_grid = None
56
+ self._rotation_grid_points = None # 旋转角的空间点
57
+ if len(self.atoms) == 1:
58
+ self._rotation_grid = []
59
+
60
+ @property
61
+ def info(self):
62
+ px, py, pz = self.principal_axis
63
+ info = [f"Adsorbate molecule info: {self.atoms.get_chemical_formula()}",
64
+ f" Point Group: {self.point_group}",
65
+ f" Radius: {self.rads}",
66
+ f" Principal axis: [{px:8.3f}, {py:8.3f}, {pz:8.3f}] ",
67
+ f" Number of dihedral: {len(self.all_dihedrals)}"]
68
+ if self.dihedral_delta:
69
+ info.append(f" Number of dihedral grid: {len(self.dihedral_grid)}",)
70
+ info += [f" Number of symmetry rotation operations: {len(self.symmetry_rotation_operations)}",
71
+ f" Number of symmetry rotations: {len(self._symmetry_rotations)}",]
72
+ if self.rotation_delta:
73
+ info.append(f" Number of rotation grid: {len(self.rotation_grid)}")
74
+ if len(self.dock_points)>0:
75
+ info.append(f" Number of dock points: {len(self.dock_points)}")
76
+ return '\n'.join(info)
77
+
78
+ @property
79
+ def dock_points(self):
80
+ pos = self.atoms.positions
81
+ dp = [pos[i].mean(axis=0) for i in self.dock_point_indices]
82
+ return np.asarray(dp)
83
+
84
+ @property
85
+ def point_group(self):
86
+ return get_point_group(self.atoms)
87
+
88
+ def regulate(self) -> None:
89
+ """
90
+ 将分子旋转到主轴 = z, 次主轴=x, 第三轴 = y
91
+ """
92
+ # com to 0,0,0
93
+ if len(self.atoms) > 1:
94
+ atoms_regulate(self.atoms)
95
+ else:
96
+ self.atoms.set_center_of_mass([0.,0.,0.])
97
+ self.is_regulated = True
98
+
99
+ @property
100
+ def com(self):
101
+ if self.is_regulated:
102
+ return np.array([0., 0., 0.])
103
+ else:
104
+ return self.atoms.center_of_mass()
105
+
106
+ @property
107
+ def principal_axis(self):
108
+ # 分子主轴向量
109
+ if not self.is_regulated:
110
+ self.regulate()
111
+ evals, evecs = self.atoms.get_moments_of_inertia(vectors=True)
112
+ return evecs[np.argmin(np.abs(evals))]
113
+
114
+ @property
115
+ def rads(self):
116
+ # 分子的半径,sg_obj 构造时作为参考
117
+ # 思考:这是为了生成格点的半径,格点的位置是一种参考的位置,可以尽量接近真实的吸附结构。
118
+ # 因而使用吸附原子的半径作为半径比较好,而不是分子的质心。当然,这是对于比较平整的slab 模型而言,对于团簇而言,也是如此吗?
119
+ # 可以认为是的,到时候分子的质心沿着法向向量移动就可以了。
120
+ if self._rads is not None:
121
+ return self._rads
122
+
123
+ from ase.data import covalent_radii, vdw_radii
124
+ if self.rtype in ['covalent_radii', 'natural_cutoff']:
125
+ radii = covalent_radii
126
+ elif self.rtype == 'vdw_radii':
127
+ radii = vdw_radii
128
+ else:
129
+ radii = covalent_radii
130
+
131
+ if len(self.dock_point_indices) != 0: # 使用吸附原子的半径作为半径, 返回一个列表
132
+ self._rads = np.mean([radii[self.atoms.numbers[i]]
133
+ for ids in self.dock_point_indices for i in ids])
134
+ return self._rads
135
+
136
+ if not self.is_regulated:
137
+ self.regulate()
138
+ positions = self.atoms.positions
139
+ dim_length = np.array([
140
+ max(positions[:,0]) - min(positions[:,0]),
141
+ max(positions[:,1]) - min(positions[:,1]),
142
+ max(positions[:,2]) - min(positions[:,2])
143
+ ])
144
+ shortest_dim = np.argmin(dim_length)
145
+ idx0 =np.argmin(positions[:,shortest_dim])
146
+ idx1 =np.argmax(positions[:,shortest_dim])
147
+ num0 = self.atoms.numbers[idx0]
148
+ num1 = self.atoms.numbers[idx1]
149
+ self._rads = (dim_length[shortest_dim] + radii[num0] + radii[num1]) / 2
150
+ return self._rads
151
+
152
+ @rads.setter
153
+ def rads(self, value:float) -> None:
154
+ self._rads = value
155
+
156
+ @property
157
+ def natoms(self) -> int:
158
+ return len(self.atoms)
159
+
160
+ @property
161
+ def all_dihedrals(self)->Sequence: # 只包含可以活动的二面角
162
+ if self._all_dihedrals is None:
163
+ print("Analysis all unique dihedrals by ase.geometry.analysis.Analysis")
164
+ all_dihedrals = []
165
+ for i, lst in enumerate(self.analysis.unique_dihedrals[0]):
166
+ for jkl in lst:
167
+ j,k,l = jkl
168
+ all_dihedrals.append((i, j, k, l))
169
+ self._all_dihedrals = self._filter_dihedral(all_dihedrals)
170
+ return self._all_dihedrals
171
+
172
+ @all_dihedrals.setter
173
+ def all_dihedrals(self, value:Sequence[Tuple]) -> None:
174
+ self._all_dihedrals = value
175
+
176
+ def rm_dihedral(self, value) -> Tuple:
177
+ d = self._all_dihedrals.pop(value)
178
+ return d
179
+
180
+ def add_dihedral(self, value:Tuple[int]) -> None:
181
+ if self._all_dihedrals is None:
182
+ self._all_dihedrals = [value]
183
+ else:
184
+ self._all_dihedrals.append(value)
185
+
186
+ def _filter_dihedral(self, dihedrals: Sequence[Tuple[int]]) -> Sequence[Tuple]:
187
+ # (i,j,k,l) 只保留 j,k 不同的二面角,且 i,l 尽可能是非H元素, 优先级 C > 其他重元素 > H
188
+ filtered_dihedrals = {}
189
+ # 构造 dict = {(j,k): (i,l)}
190
+ for i,j,k,l in dihedrals:
191
+ if j > k: # 翻转 j,k 和 i,l
192
+ key = (k, j)
193
+ v = (l, i)
194
+ else:
195
+ key = (j, k)
196
+ v = (i, l)
197
+ if key in filtered_dihedrals:
198
+ v_old = filtered_dihedrals[key]
199
+ for idx, xx in enumerate(zip(v, v_old)):
200
+ ii, iio = xx
201
+ inum = self.atoms.numbers[ii]
202
+ ionum = self.atoms.numbers[iio]
203
+ if ionum != 6:
204
+ if inum == 6 or (inum > ionum):
205
+ filtered_dihedrals[key][idx] = ii
206
+ else:
207
+ filtered_dihedrals[key] = v
208
+
209
+ filtered_dihedrals = [(val[0], key[0], key[1], val[1]) for key,val in filtered_dihedrals.items()]
210
+ return filtered_dihedrals
211
+
212
+ @property
213
+ def atoms_graph(self):
214
+ if self._atoms_graph is None:
215
+ self._atoms_graph = atoms2graph(self.atoms, adj=self._adj_matrix)
216
+
217
+ return self._atoms_graph
218
+
219
+ def get_dihedral_mask(self, dihedral: Tuple[int, int, int, int]) -> set:
220
+ """
221
+ 为二面角生成移动的 mask indices
222
+ :param dihedral:
223
+ :return:
224
+ """
225
+ dm = self._dihedral_mask_dct.get(dihedral, None)
226
+ if dm is None:
227
+ import networkx as nx
228
+ new_graph = self.atoms_graph.copy()
229
+ i,j,k,l = dihedral
230
+ if not new_graph.has_edge(j,k):
231
+ raise ValueError(f"{j}-{k} is not connected, please check the dihedral.")
232
+ # 删除 j-k 生成新的图
233
+ new_graph.remove_edge(j,k)
234
+ components = list(nx.connected_components(new_graph))
235
+ ncomponent = len(components)
236
+ if ncomponent == 1:
237
+ raise NotImplementedError(f"{j}-{k} is in a ring, this is not supported yet.")
238
+ elif ncomponent > 2:
239
+ raise ValueError(f"Break {j}-{k} bond produces {ncomponent} parts.")
240
+ comp1, comp2 = components[0], components[1]
241
+ if len(comp1) > len(comp2):
242
+ dm = comp1
243
+ else:
244
+ dm = comp2
245
+ self._dihedral_mask_dct[dihedral] = dm
246
+ return dm
247
+
248
+ @property
249
+ def dihedral_delta(self):
250
+ return self._dihedral_delta
251
+
252
+ @dihedral_delta.setter
253
+ def dihedral_delta(self, value):
254
+ if self._dihedral_delta !=value:
255
+ self._dihedral_delta = value
256
+ self._dihedral_grid = None
257
+ n = int(360/self.dihedral_delta) ** len(self.all_dihedrals)
258
+ print(f"Number of dihedrals before refine: {n}")
259
+
260
+ @property
261
+ def dihedral_grid(self):
262
+ if self.dihedral_delta is None:
263
+ print("Please set dihedral_delta first!")
264
+ return None
265
+
266
+ if self._dihedral_grid is None:
267
+ self._dihedral_grid = dih_grid(len(self.all_dihedrals), int(360/self.dihedral_delta))
268
+ return self._dihedral_grid
269
+
270
+ def refine_dihedral_grid(self, tolerance=0.5) -> None:
271
+ atoms_list = []
272
+ for dih_values in self.dihedral_grid:
273
+ dih_dict = {i: {'value': dih_values[idx], 'mask': self.get_dihedral_mask(i)}
274
+ for idx, i in enumerate(self.all_dihedrals)}
275
+ # 设置 二面角
276
+ atoms = set_dihedrals(self.atoms, dih_dict)
277
+ # regularize
278
+ atoms_regulate(atoms)
279
+ atoms_list.append(atoms)
280
+ # 计算分子之间的距离
281
+ n_structures = len(atoms_list)
282
+ dist_matrix = np.zeros((n_structures, n_structures))
283
+ for i in range(n_structures):
284
+ for j in range(i + 1, n_structures):
285
+ dist = ase_distance(atoms_list[i], atoms_list[j], permute=True)
286
+ # TODO: 使用 CDTree 计算相似性
287
+ # 比如,先对 H 原子求dist,然后再对其他原子分别求相似性。
288
+ # 或者分别对不同类型原子用 cdist
289
+ dist_matrix[i, j] = dist
290
+ dist_matrix[j, i] = dist
291
+
292
+ ## 排除相似的分子
293
+ selected_indices = []
294
+ selected_mask = np.zeros(n_structures, dtype=bool)
295
+ for i in range(n_structures):
296
+ if not selected_mask[i]:
297
+ selected_indices.append(i)
298
+ # 标记所有相似的结构
299
+ similar_indices = np.where(dist_matrix[i] < tolerance)[0]
300
+ selected_mask[similar_indices] = True
301
+ self._dihedral_grid = self.dihedral_grid[selected_indices]
302
+
303
+ def get_vip_dih(self)->Sequence:
304
+ # TODO: 对气相分子C空间进行采样,得到势能面,然后进行关键点识别(critical point)(盆分析),得到极小值点的集合
305
+ return []
306
+
307
+ def get_dihedrals(self, dih_value=None)->dict:
308
+ if dih_value is None: # 返回当前结构的二面角
309
+ dih_value = [self.atoms.get_dihedral(*d) for d in self.all_dihedrals]
310
+
311
+ dihedrals = {d: {'value': dih_value[i], 'mask': self.get_dihedral_mask(d)}
312
+ for i, d in enumerate(self.all_dihedrals)}
313
+ return dihedrals
314
+
315
+ @property
316
+ def symmetry_rotation_operations(self) -> Dict:
317
+ if len(self.atoms) == 1:
318
+ self._symmetry_rotations = []
319
+ return dict()
320
+ if self._symmetry_rotations is None:
321
+ self._symmetry_rotations = get_pure_rotations(self.atoms)
322
+ self._symmetry_rotation_operations = get_Cn(self.atoms)
323
+ return self._symmetry_rotation_operations
324
+
325
+ @property
326
+ def rotation_delta(self) -> float:
327
+ return self._rotation_delta
328
+
329
+ @rotation_delta.setter
330
+ def rotation_delta(self, value) -> None:
331
+ if len(self.atoms) == 1:
332
+ print("No rotation for monatomic molecule.")
333
+ self._rotation_grid = []
334
+ else:
335
+ self._rotation_delta = value
336
+ n = estimate_rotation_samples(value, self.point_group)
337
+ print(f"Number of rotation samples: {n}")
338
+ self._rotation_grid = None
339
+
340
+ @property
341
+ def rotation_grid(self):
342
+ """
343
+ :return: rotation quaternions grid
344
+ """
345
+ if len(self.atoms) == 1:
346
+ return None
347
+
348
+ if self.rotation_delta is None:
349
+ print("Please set rotation_delta first!")
350
+ return None
351
+
352
+ if self._rotation_grid is None:
353
+ _ = self.symmetry_rotation_operations
354
+ self._rotation_grid = sample_rotations_with_symmetry(avg_deg=self.rotation_delta,
355
+ point_group=self.point_group,
356
+ sym_quats=[i.as_quat(scalar_first=True)
357
+ for i in self._symmetry_rotations]
358
+ )
359
+ return self._rotation_grid
360
+
361
+ @rotation_grid.setter
362
+ def rotation_grid(self, values:Sequence) -> None: # define your own grid, or modify them.
363
+ self._rotation_grid = values
364
+
365
+ def view_rotation_grid(self, save_path=None) -> None:
366
+ view_samples(self.rotation_grid, save_path=save_path)
367
+
368
+ def view_rotation_atoms(self):
369
+ from scipy.spatial.transform import Rotation
370
+ from ase.visualize import view
371
+
372
+ if self.rotation_grid is None:
373
+ print("Please set rotation_grid first!")
374
+ return None
375
+
376
+ euler = Rotation.from_quat(self.rotation_grid, scalar_first=True).as_euler('zxz', degrees=True)
377
+ atoms_list = []
378
+ for eu in euler:
379
+ A = self.atoms.copy()
380
+ A.euler_rotate(*eu)
381
+ atoms_list.append(A)
382
+ view(atoms_list)
383
+ return None
384
+
@@ -0,0 +1,143 @@
1
+ import ase
2
+ import numpy as np
3
+ from typing import Union, Sequence
4
+
5
+ from scipy.spatial import cKDTree
6
+
7
+ from surface_construct.utils import extended_points
8
+ from surface_construct.utils.atoms import set_dihedrals, atoms_regulate
9
+ from surface_construct import Adsorbate, SurfaceGrid
10
+
11
+
12
+ class AdsGridCombiner:
13
+ def __init__(self, sg_obj:SurfaceGrid, ads_obj:Adsorbate, **kwargs):
14
+ """
15
+ :param sg_obj: 表面格点
16
+ :param ads_obj: 吸附分子,包含Atoms,主轴,内坐标列表,根据这些参数可以得到分子坐标
17
+ :param kwargs:
18
+ """
19
+ self.atoms = None
20
+ self.sg_obj = sg_obj
21
+ self.ads_obj = ads_obj
22
+ self.kwargs = kwargs
23
+ if self.sg_obj.atoms.calc is not None:
24
+ self.calc = self.sg_obj.atoms.calc
25
+ else:
26
+ if self.ads_obj.atoms.calc is not None:
27
+ self.calc = self.ads_obj.atoms.calc
28
+ else:
29
+ self.calc = None
30
+
31
+ @property
32
+ def info(self):
33
+ info = [self.sg_obj.info, self.ads_obj.info]
34
+ return '\n'.join(info)
35
+
36
+ def get_atoms(self, sidx:int, cidx:Union[int, None]=None,
37
+ ridx:Union[int, None]=None, dp_idx:int=None, **kwargs) -> Union[ase.Atoms, None]:
38
+ """
39
+ 将分子设置 dihedrals(C空间)、rotation (R空间)然后将分子的结合点(dock_point)
40
+ 放置于site(S空间)上,根据需要调整 z (Z空间)。
41
+ 分子内坐标 dihedrals = {indice:{value:v, mask:[i,j,k]}}
42
+ 若不用调整 z,则设置 zmax=site[-1],放入 kwargs
43
+ :param sidx: 格点序号
44
+ :param cidx:
45
+ :param ridx:
46
+ :param dp_idx:
47
+ :return: 组合后的 Atoms
48
+ """
49
+ xyz = self.sg_obj.points[sidx]
50
+ dih_value, rotation, dock_point = None, None, None
51
+ if cidx is not None:
52
+ dih_value = self.ads_obj.dihedral_grid[cidx]
53
+ if ridx is not None:
54
+ rotation = self.ads_obj.rotation_grid[ridx]
55
+ if dp_idx is None and len(self.ads_obj.dock_points) > 0:
56
+ dp_idx = 0
57
+ if dp_idx is not None:
58
+ dock_point = self.ads_obj.dock_points[dp_idx]
59
+ # 改变分子构象 and then rotate
60
+ ads_atoms = self.docking(
61
+ dih_value=dih_value,
62
+ rotation=rotation,
63
+ xyz=xyz,
64
+ dock_point=dock_point,
65
+ )
66
+ if kwargs.get('opt_z', False):
67
+ # get_z 去更新 ads_atoms 的坐标
68
+ ads_atoms = self._opt_z(ads_atoms, dock_point=dock_point, zmax=kwargs.get('zmax', None))
69
+ if not ads_atoms:
70
+ return None
71
+
72
+ atoms = self.sg_obj.atoms.copy()
73
+ atoms += ads_atoms
74
+ atoms.calc = self.calc
75
+ self.atoms = atoms
76
+ return atoms
77
+
78
+ def _opt_z(self, ads_atoms:ase.Atoms,
79
+ dock_point:int=None,
80
+ conflict:float=None,
81
+ zmax:float=None,
82
+ ):
83
+ """
84
+ 由于分子可能会与表面冲突,调整分子的高度可以避免
85
+ TODO: 事实上调整的是沿着格点法向向量的距离 xyz = xyz0 + r×d
86
+ :return:
87
+ """
88
+ # 该方法仅仅适用于 slab 体系,不适合 cluster 体系
89
+ # 先找到grid 的最大值点,分子的最小值点,然后把分子的COM 移动到距离5A以上的点
90
+ # 根据 ads 不同的原子类型,计算每个原子需要移动的z值,取最小的值。-> 应该不需要,用最简单的办法解决就行
91
+ max_rsub = max(self.sg_obj.rsub)
92
+ if conflict is None:
93
+ conflict = max_rsub + 0.5 # 分子与表面距离宁肯稍微大一些,不要太小。TODO: 使用距离矩阵判断,考虑元素影响
94
+ if zmax is None:
95
+ if dock_point is None: # 默认是范德华作用,单原子不需要调整(也无需dock point)
96
+ posz = ads_atoms.positions[:,2]
97
+ zmax = self.sg_obj.rads + max_rsub + (max(posz)-min(posz))/2.0 #
98
+ else:
99
+ zmax = (self.sg_obj.rads+max_rsub) * 0.5 # 键长的1.5倍
100
+ surf_atoms = self.sg_obj.atoms
101
+ surf_tree = cKDTree(extended_points(surf_atoms.positions, (1, 1, 0), surf_atoms.cell))
102
+ test_atoms = ads_atoms.copy()
103
+ delta_z = 0.05 # 每次递增 0.05
104
+ n_delta = 1
105
+ while delta_z*n_delta <= zmax:
106
+ test_atoms.positions[:,2] += delta_z
107
+ overlap = surf_tree.query_ball_point(test_atoms.positions, conflict, p=2).tolist()
108
+ len_overlap = sum(list(map(len, overlap)))
109
+ if len_overlap != 0:
110
+ n_delta += 1
111
+ else:
112
+ print(f"Update adsorbate by adding z {delta_z*n_delta} to avoid conflict to surface.")
113
+ return test_atoms
114
+
115
+ print("Cannot find z value for adsorbate without conflict!")
116
+ return False
117
+
118
+ def docking(self, xyz:np.ndarray=np.array([0.,0.,0.]),
119
+ dih_value:Sequence=None,
120
+ rotation:Sequence=None,
121
+ dock_point:int=None) -> ase.Atoms:
122
+ from scipy.spatial.transform import Rotation as R
123
+ if dih_value is not None:
124
+ dih_dict = self.ads_obj.get_dihedrals(dih_value=dih_value)
125
+ atoms = set_dihedrals(self.ads_obj.atoms,dih_dict)
126
+ atoms_regulate(atoms)
127
+ else:
128
+ atoms = self.ads_obj.atoms.copy()
129
+
130
+ if dock_point is not None: # move atoms dock point to 0
131
+ atoms.positions -= dock_point
132
+
133
+ if rotation is not None: # rotate atoms at dock point
134
+ atoms.positions = R.from_quat(rotation, scalar_first=True).apply(atoms.positions)
135
+
136
+ atoms.positions += xyz # move atoms
137
+ return atoms
138
+
139
+ def get_vip_rot(self)->Sequence:
140
+ # TODO: 根据各种不同位点的采样结果,进行能量分析,排除截断以上的旋转,得到较优的旋转空间
141
+ return []
142
+
143
+
@@ -0,0 +1,30 @@
1
+ import pymsym
2
+ import ase
3
+ from ase.build import molecule
4
+
5
+ def get_symmetry(atoms: ase.Atoms) -> str:
6
+ msym_elements = list()
7
+ for i in atoms:
8
+ msym_elements.append(pymsym.Element(name=i.symbol, coordinates=i.position.tolist()))
9
+
10
+ msym_basis_functions = list()
11
+ for element in msym_elements:
12
+ bfs = [pymsym.RealSphericalHarmonic(element=element, n=2, l=1, m=m, name=f"p{m+1}") for m in (-1, 0, 1)]
13
+ element.basis_functions = bfs
14
+ msym_basis_functions += bfs
15
+
16
+ # try:
17
+ with pymsym.Context(elements=msym_elements, basis_functions=msym_basis_functions) as ctx:
18
+ return ctx.find_symmetry()
19
+ # except Exception as e:
20
+ # print(e)
21
+ # # diff versions throw libmsym.main.Error or libmsym.libmsym.Error, so I'll drop a blanket Exception
22
+ # # incredibly, this is the desired behavior of libmsym!
23
+ # return "C1"
24
+
25
+ mol = molecule("C6H6")
26
+
27
+ print(get_symmetry(mol))
28
+
29
+ print(pymsym.get_point_group(mol.numbers.tolist(), mol.positions.tolist()))
30
+ print(pymsym.get_symmetry_number(mol.numbers.tolist(), mol.positions.tolist()))