surface-construct 0.10.2__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.
- {surface_construct-0.10.2/surface_construct.egg-info → surface_construct-0.11}/PKG-INFO +17 -1
- {surface_construct-0.10.2 → surface_construct-0.11}/README.md +16 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/setup.py +1 -1
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct/__init__.py +4 -1
- surface_construct-0.11/surface_construct/structures/adsorbate.py +384 -0
- surface_construct-0.11/surface_construct/structures/combiner.py +143 -0
- surface_construct-0.11/surface_construct/structures/pymsym_test.py +30 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct/structures/surface_grid.py +37 -11
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct/tasks/sitesampling.py +75 -44
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct/tasks/taskbase.py +3 -2
- surface_construct-0.11/surface_construct/tasks/terminations.py +0 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct/utils/__init__.py +8 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct/utils/atoms.py +90 -2
- surface_construct-0.11/surface_construct/utils/geometry.py +389 -0
- surface_construct-0.11/surface_construct/utils/pymsym_wrapper.py +73 -0
- surface_construct-0.11/surface_construct/utils/spglib_wrapper.py +194 -0
- {surface_construct-0.10.2 → surface_construct-0.11/surface_construct.egg-info}/PKG-INFO +17 -1
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct.egg-info/SOURCES.txt +7 -0
- surface_construct-0.11/tests/test_adsorbate.py +101 -0
- surface_construct-0.11/tests/test_combiner.py +61 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/tests/test_surface_grid.py +1 -3
- {surface_construct-0.10.2 → surface_construct-0.11}/tests/test_task.py +34 -4
- surface_construct-0.10.2/surface_construct/structures/__init__.py +0 -50
- surface_construct-0.10.2/surface_construct/structures/adsorbate.py +0 -38
- {surface_construct-0.10.2 → surface_construct-0.11}/LICENSE +0 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/setup.cfg +0 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct/db.py +0 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct/default_parameter.py +0 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct/sg_sampler.py +0 -0
- /surface_construct-0.10.2/surface_construct/tasks/terminations.py → /surface_construct-0.11/surface_construct/structures/__init__.py +0 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct/structures/surface.py +0 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct/tasks/__init__.py +0 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct/utils/weight_functions.py +0 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct.egg-info/dependency_links.txt +0 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct.egg-info/requires.txt +0 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/surface_construct.egg-info/top_level.txt +0 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/tests/test_sampling1.py +0 -0
- {surface_construct-0.10.2 → surface_construct-0.11}/tests/test_sampling2.py +0 -0
- {surface_construct-0.10.2 → 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.
|
|
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
|
* 完善用户界面、例子、教程
|
|
@@ -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()))
|