Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions HTMACat/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import os
import yaml
import tempfile
from pathlib import Path
from HTMACat.model.Construct_adsorption_yaml import Construct_adsorption_yaml
from rich import print


def construct_adsorption(
config_yaml: str = None,
StrucInfo=None,
Species=None,
Model=None,
workdir="./"
):
"""
构建吸附构型。
支持两种调用方式:
1. construct_adsorption(config_yaml="config.yaml")
2. construct_adsorption(StrucInfo=dict, Model=dict, [Species=dict])
"""

print("[HTMACat] Construct adsorption configuration ...")
workdir = Path(workdir).resolve()
workdir.mkdir(parents=True, exist_ok=True)
os.chdir(workdir)

# ============= 模式1:直接读取 config.yaml 文件 =============
if config_yaml and os.path.exists(config_yaml):
Construct_adsorption_yaml(config_yaml)
print("✅ Adsorption configuration generated successfully!")
return

# ============= 模式2:使用 Python 字典输入 =============
if StrucInfo and Model:
config_data = {"StrucInfo": StrucInfo, "Model": Model}
if Species:
config_data["Species"] = Species

# 临时 YAML 文件(不会污染用户目录)
with tempfile.NamedTemporaryFile("w", delete=False, suffix=".yaml") as tmp:
yaml.dump(config_data, tmp)
tmp_path = tmp.name

# 调用原有逻辑
Construct_adsorption_yaml(tmp_path)

# 清理临时文件
os.remove(tmp_path)
print("✅ Adsorption configuration generated successfully!")
return

# ============= 参数错误 =============
raise ValueError("❌ You must provide either config_yaml path or StrucInfo+Model dicts.")


80 changes: 70 additions & 10 deletions HTMACat/catkit/gen/adsorption.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,43 @@ def _single_adsorption(
adsorption_vector, flag = utils.solve_normal_vector_linearsvc(atoms.get_positions(), bond)
### print('adsorption_vector:\n', adsorption_vector)
atoms.rotate(adsorption_vector, [0, 0, 1])
if site_coord is not None:
# ========== 模拟 asphericity / hetero 的处理 ==========
site_coord = np.array(site_coord, dtype=float)
final_positions = slab.get_positions()
max_z = np.max(final_positions[:, 2])

z_coordinates = atoms.get_positions()[:, 2]
min_z = np.min(z_coordinates)

base_position = [0.0, 0.0, 0.0]
# 用输入 site_coord 的 z 来定义分子高度
effective_distance = site_coord[2]
base_position[2] = round(max_z - min_z + effective_distance, 2)

# 用输入的 site_coord 作为 xy 坐标
base_position[0] = round(site_coord[0], 1)
base_position[1] = round(site_coord[1], 1)

# 平移分子
atoms.translate(base_position)

# 再做一次 xy 校正,确保 bond 原子落在 site_coord
vec_tmp = site_coord - atoms.get_positions()[bond]
atoms.translate([vec_tmp[0], vec_tmp[1], 0])
n = len(slab)
slab += atoms
for metal_index in self.index[u]:
slab.graph.add_edge(metal_index, bond + n)
return slab

# 如果没传 site_coord,就保持 utils.trilaterate 的默认逻辑
elif direction_mode == 'asphericity':
# 如果没有提供 site_coord,就使用默认位置:base_position 的 xy,加上传入的 z_bias
if site_coord is None:
site_coord = [base_position[0], base_position[1], z_bias]
else:
site_coord = np.array(site_coord, dtype=float)
# 根据分子的形状调整朝向,使其“平躺”在表面上
masses = atoms.get_masses()
positions = atoms.get_positions()
Expand Down Expand Up @@ -655,11 +691,7 @@ def _single_adsorption(
final_positions = slab.get_positions()
max_z = np.max(final_positions[:, 2])


if abs(site_coord[2]) < 1e-6: # 如果为 0 或非常接近 0
effective_distance = 2
else:
effective_distance = site_coord[2]
effective_distance = site_coord[2]

base_position[2] = round(max_z - min_z + effective_distance, 2)

Expand Down Expand Up @@ -724,16 +756,44 @@ def _single_adsorption(

# zjwang 20240815
if direction_mode == 'hetero':
if site_coord is None:
site_coord = [base_position[0], base_position[1], z_bias]
else:
site_coord = np.array(site_coord, dtype=float)
final_positions = slab.get_positions()
max_z = np.max(slab.get_positions()[:,2]) #获取slabz轴最大值
n = len(slab)
slabs_list = []
score_configurations = [] # 各个吸附构型(slab)的“分数”
for ia,a in enumerate(atoms_list):
for ia, a_orig in enumerate(atoms_list):
slabs_list.append(copy.deepcopy(slab))
dealt_positions = a.get_positions()
min_z = np.min(dealt_positions[:,2]) #得到分子z轴最小值
base_position[2] = max_z - min_z + 2 # 吸附物种最低原子处于slab以上2.2A,随后再尝试降低
a = copy.deepcopy(a_orig) # 确保每次处理原始未改动的 adsorbate
z_coordinates = a.get_positions()[:, 2]
min_z = np.min(z_coordinates)
# Step 1: Z 方向平移
effective_distance = site_coord[2]
base_position = [0.0, 0.0, 0.0]
base_position[2] = round(max_z - min_z + effective_distance, 2)

# 计算 slab 中心坐标
slab_positions = slab.get_positions()
center_x, center_y = utils.center_slab(slab_positions)
# 使用输入的 site_coord 作为 xy 坐标
base_position[0] = round(site_coord[0], 1)
base_position[1] = round(site_coord[1], 1)

# 打印检查
print(f"min_z (adsorbate): {min_z}, max_z (slab): {max_z}, new base_position: {base_position}")

# 平移吸附分子
a.translate(base_position)

# 将分子平移到指定的 site_coord 位置
xy_translation = site_coord[:2] - a.get_positions()[bond][:2]
a.translate([xy_translation[0], xy_translation[1], 0]) # 只移动 x-y,不改变 z
print('Final base_position:', base_position)

# 组合 slab 和吸附物
slabs_list[-1] += a
# Add graph connections
for metal_index in self.index[u]:
Expand Down Expand Up @@ -772,7 +832,7 @@ def _single_adsorption(
for i,idx in enumerate(np.argsort(score_configurations)[::-1]):
f.write(str(i).ljust(4)+': '+str(idx).ljust(8)+str(score_configurations[idx])+'\n')
return slabs_list

#lbx
if base_position[2] == 0.0:
#z_coordinates = rotated_positions[:, 2]
Expand Down
28 changes: 16 additions & 12 deletions HTMACat/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,24 @@ def main_command():
@htmat.command(context_settings=CONTEXT_SETTINGS)
def ads(
in_dir: str = typer.Option("./", "-i", "--inputdir", help="relative directory of input file"),
out_dir: str = typer.Option(
"./", "-o", "--outputdir", help="relative directory of output file"
),
):
"""Construct adsorption configuration."""
print("Construct adsorption configuration ... ...")
wordir = Path(in_dir).resolve()
outdir = Path(out_dir).resolve()
StrucInfo = "config.yaml"
if not outdir == wordir:
outdir.mkdir(parents=True, exist_ok=True)
shutil.copy(wordir / StrucInfo, outdir)
os.chdir(outdir)
Construct_adsorption_yaml(StrucInfo)
import os
from HTMACat.api import construct_adsorption
from rich import print

print("[bold green]Construct adsorption configuration via API...[/bold green]")

# 找到配置文件路径
config_path = os.path.join(in_dir, "config.yaml")
if not os.path.exists(config_path):
print(f"[red]Error:[/red] config.yaml not found in {in_dir}")
raise typer.Exit(code=1)

# 直接传入路径,不需要读取内容
construct_adsorption(config_yaml=config_path)

print("[bold green]✅ Adsorption configuration generated successfully![/bold green]")


@htmat.command(context_settings=CONTEXT_SETTINGS)
Expand Down
57 changes: 41 additions & 16 deletions HTMACat/model/Ads.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def __init__(self, species: list, sites: list, settings={}, spec_ads_stable=None
"NH2": [2],
"NH": [2, 4],
"N": [2, 4],
"O": [2, 4],
"O": [1,2, 3],
"OH": [2, 4],
"NO": [2, 4],
"H2O": [1],
Expand Down Expand Up @@ -136,15 +136,30 @@ def dist_of_nearest_diff_neigh_site(self, slab, site_coords):
imagesites_distances = [np.sqrt(np.sum(np.square(v[:2]-coord_images[0][:2]))) for v in coord_images]
d = np.min(imagesites_distances[1:])
return d


def adjust_fractional_coordinates(self, atoms):
cell = atoms.get_cell()
fractional_coords = atoms.get_scaled_positions()

# 调整x坐标范围
fractional_coords[:, 0] += 0.5
fractional_coords[:, 0] %= 1.0 # 将x坐标重新映射到0到1之间
# 调整y坐标范围
fractional_coords[:, 1] += 0.5
fractional_coords[:, 1] %= 1.0 # 将y坐标重新映射到0到1之间

# 将调整后的分数坐标应用回结构
atoms.set_scaled_positions(fractional_coords)

def Construct_single_adsorption(self, ele=None):
_direction_mode = self.settings['direction']
_rotation_mode = self.settings['rotation']
print('>>>>>>>>> ', self.settings['rotation'])
_z_bias = float(self.settings['z_bias'])
# generate surface adsorption configuration
slab_ad = []
slabs = self.substrate.construct()
# 判断是否提供了自定义位点坐标
has_custom_site_coords = 'site_coords' in self.settings.keys()
for i, slab in enumerate(slabs):
if 'site_coords' in self.settings.keys():
coordinates = np.array(self.settings['site_coords'], dtype=np.float64)
Expand Down Expand Up @@ -173,7 +188,7 @@ def Construct_single_adsorption(self, ele=None):
for j, coord in enumerate(coordinates):
vec_to_neigh_imgsite = self.vec_to_nearest_neighbor_site(slab=slab, site_coords=[coord])
site_ = j
coord_ = None
coord_ = coord if has_custom_site_coords else None
if 'site_coords' in self.settings.keys():
coord_ = coord
# confirm z coord (height of the adsorbate)
Expand All @@ -185,18 +200,22 @@ def Construct_single_adsorption(self, ele=None):
direction_mode=_direction_mode,
site_coord = coord_,
z_bias=_z_bias)
if type(tmp) == list:
if isinstance(tmp, list):
for ii, t in enumerate(tmp):
if not has_custom_site_coords:
self.adjust_fractional_coordinates(t)
slab_ad += [t]
else:
slab_ad += [tmp]
if not has_custom_site_coords:
self.adjust_fractional_coordinates(tmp)
slab_ad.append(tmp)
#if len(bond_atom_ids) > 1:
# slab_ad += [builder._single_adsorption(ads_use, bond=bond_id, site_index=j, direction_mode='decision_boundary', direction_args=bond_atom_ids)]
else:
for j, coord in enumerate(coordinates):
vec_to_neigh_imgsite = self.vec_to_nearest_neighbor_site(slab=slab, site_coords=[coord])
site_ = j
coord_ = None
coord_ = coord if has_custom_site_coords else None
if 'site_coords' in self.settings.keys():
coord_ = coord
tmp = builder._single_adsorption(ads_use, bond=0, site_index=site_,
Expand All @@ -205,11 +224,17 @@ def Construct_single_adsorption(self, ele=None):
direction_mode=_direction_mode,
site_coord = coord_,
z_bias=_z_bias)
if type(tmp) == list:
if isinstance(tmp, list):
for ii, t in enumerate(tmp):
slab_ad += [t]
if not has_custom_site_coords:
# 调整分数坐标范围
self.adjust_fractional_coordinates(t)
slab_ad.append(t)
else:
slab_ad += [tmp]
if not has_custom_site_coords:
# 调整分数坐标范围
self.adjust_fractional_coordinates(tmp)
slab_ad.append(tmp)
return slab_ad

def Construct_double_adsorption(self):
Expand Down Expand Up @@ -244,7 +269,7 @@ def get_settings(cls,settings_dict={}):
default_settings = {'conform_rand':1,
'direction':'default',
'rotation':'vnn',
'z_bias':float(0),
'z_bias':float(2.0),
}
for key,value in settings_dict.items():
default_settings[key] = value
Expand All @@ -263,13 +288,10 @@ def construct(self):
if ([self.get_sites()[0][0], self.get_sites()[1][0]] == ['1','1']):
ele = [''.join(self.get_sites()[0][1:]),''.join(self.get_sites()[1][1:])]
slabs_ads = self.Construct_coadsorption_11(ele=ele)
print('a')
elif ([self.get_sites()[0][0], self.get_sites()[1][0]] == ['1','2']):
slabs_ads = self.Construct_coadsorption_12()
print('b')
elif ([self.get_sites()[0][0], self.get_sites()[1][0]] == ['2','2']):
slabs_ads = self.Construct_coadsorption_22()
print('c')
else:
raise ValueError("Supports only '1' or '2' adsorption sites for coads!")### end
if self.substrate.is_dope():
Expand Down Expand Up @@ -312,9 +334,8 @@ def Construct_coadsorption_11(self, ele=['','']):
_direction_mode = 'bond_atom'
if 'rotation' in self.settings.keys():
_rotation_mode = self.settings['rotation']
print(">>> self.settings['rotation'] =", self.settings['rotation'])
else:
_rotation_mode = 'xzq'
_rotation_mode = 'vnn'
if 'site_locate_ads1' in self.settings.keys():
_site_locate_ads1 = self.settings['site_locate_ads1']
else:
Expand Down Expand Up @@ -457,17 +478,21 @@ def Construct_coadsorption_11(self, ele=['','']):
bind_surfatoms,
bind_surfatoms_symb,
) = get_binding_adatom(adslab)
print(f"Adsorption {j+1}: {adspecie, bind_type_symb}")
adspecie_tmp, bind_type_symb_tmp = [], []
for k, spe in enumerate(adspecie):
if spe in ads_type.keys():
adspecie_tmp += [spe]
bind_type_symb_tmp += [bind_type_symb[k]]
print(f"Adsorption configuration1 {j+1}: {adspecie_tmp, bind_type_symb_tmp}")
if len(adspecie_tmp) < 2:
# print('Can not identify the config!')
slab_ad_final += [adslab]
elif typ.get(bind_type_symb_tmp[0]) in ads_type.get(adspecie_tmp[0]) and \
typ.get(bind_type_symb_tmp[1]) in ads_type.get(adspecie_tmp[1]):
slab_ad_final += [adslab]
print(len(slab_ad_final))
print(slab_ad_final)
return slab_ad_final

def Construct_coadsorption_12(self):
Expand Down
22 changes: 20 additions & 2 deletions example/adsorb/config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
StrucInfo:
file: CONTCAR
element: Pt
lattice_type: fcc
lattice_constant: 4.16
facet:
- '100'
dope:
Cu:
- '0'
Model:
ads:
- [f: '1.xyz', 1Si, settings: {site_coords: [[5.9, 8.1, 0.0]], direction: 'asphericity'}]
- - 's: ''O'''
- 1O
- settings:
site_coords:
- - 1
- 1
- 2
direction: asphericity
- - 's: ''O'''
- 1O
- settings:
direction: asphericity
Loading
Loading