diff --git a/HTMACat/api.py b/HTMACat/api.py new file mode 100644 index 0000000..eee8898 --- /dev/null +++ b/HTMACat/api.py @@ -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.") + + diff --git a/HTMACat/catkit/gen/adsorption.py b/HTMACat/catkit/gen/adsorption.py index 8122660..847327c 100644 --- a/HTMACat/catkit/gen/adsorption.py +++ b/HTMACat/catkit/gen/adsorption.py @@ -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() @@ -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) @@ -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]: @@ -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] diff --git a/HTMACat/command.py b/HTMACat/command.py index faead22..88ddd7c 100644 --- a/HTMACat/command.py +++ b/HTMACat/command.py @@ -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) diff --git a/HTMACat/model/Ads.py b/HTMACat/model/Ads.py index 663977e..7763e98 100644 --- a/HTMACat/model/Ads.py +++ b/HTMACat/model/Ads.py @@ -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], @@ -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) @@ -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) @@ -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_, @@ -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): @@ -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 @@ -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(): @@ -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: @@ -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): diff --git a/example/adsorb/config.yaml b/example/adsorb/config.yaml index c44e4ad..dbe0375 100644 --- a/example/adsorb/config.yaml +++ b/example/adsorb/config.yaml @@ -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 diff --git a/example/adsorb/temp_config.yaml b/example/adsorb/temp_config.yaml new file mode 100644 index 0000000..1bdb319 --- /dev/null +++ b/example/adsorb/temp_config.yaml @@ -0,0 +1,24 @@ +Model: + ads: + - - 's: ''O''' + - 1O + - settings: + direction: asphericity + site_coords: + - - 1 + - 1 + - 2 + - - 's: ''O''' + - 1O + - settings: + direction: asphericity +StrucInfo: + struct: + dope: + Cu: + - '0' + element: Pt + facet: + - '100' + lattice_constant: 4.16 + lattice_type: fcc diff --git a/temp_config.yaml b/temp_config.yaml new file mode 100644 index 0000000..8960d23 --- /dev/null +++ b/temp_config.yaml @@ -0,0 +1,12 @@ + +StrucInfo: + struct: + element: Pt + lattice_type: fcc + lattice_constant: 4.16 + facet: ["100"] + dope: + Cu: ["0"] +Model: + ads: + - [s: 'O', 1O, settings: {site_coords: [[1,1,2]], direction: 'asphericity'}] #H2O diff --git a/test_api.py b/test_api.py new file mode 100644 index 0000000..c8b39e4 --- /dev/null +++ b/test_api.py @@ -0,0 +1,20 @@ +from HTMACat.api import construct_adsorption + +StrucInfo = { + "struct": { + "element": "Pt", + "lattice_type": "fcc", + "lattice_constant": 4.16, + "facet": ["100"], + "dope": {"Cu": ["0"]} + } +} + +Model = { + "ads": [ + [{'s': "O"}, '1O', {"settings": {"site_coords": [[1, 1, 2]], "direction": "asphericity"}}], + [{'s': "O"}, '1O', {"settings": {"direction": "asphericity"}}] + ] +} + +construct_adsorption(StrucInfo=StrucInfo, Model=Model, workdir=".")