From 80ae03c8c1a325d0a79749928dda26000a1cbbec Mon Sep 17 00:00:00 2001 From: koko <1429659362@qq.com> Date: Wed, 19 Nov 2025 12:23:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=80=E4=BA=9B=E5=B0=8F=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GPUMD/Umap/umap_make.py | 76 ++++ GPUMD/Umap/umap_make_2.py | 161 ++++++++ GPUMD/data_POSCAR/origin/pnma.vasp | 88 +++++ GPUMD/raw2xyz.py | 149 +++++++ GPUMD/swap_li.py | 180 +++++++++ GPUMD/t-SNE/t-SNE.py | 140 +++++++ Li_Conductivity/data/conductivity_results.csv | 3 + MSD/main.py | 2 +- MSD/utils/con2.py | 2 +- contrast learning/copy.py | 128 ++++++- contrast learning/split.py | 111 ++++++ corner-sharing/0923_CS.py | 2 +- corner-sharing/utils/CS_analyse.py | 2 +- corner-sharing/utils/analyze_env_st.py | 2 +- .../data/input/最新数据库核查过gsj.xlsx | Bin 0 -> 93841 bytes data_get/new_v1/data_get.py | 127 ++++++ dpgen/create_supercell_poscar.py | 79 ++++ dpgen/plus.py | 4 +- dpgen/supercell_make_p3ma.py | 240 ++++++++++++ dpgen/supercell_make_pnma.py | 115 ++++++ dpgen/supercell_make_wangshuo.py | 197 ++++++++++ mcp/main.py | 14 +- mcp/softBV_remake.py | 2 +- mcp/vasp_mcp.py | 362 ++++++++++++++++++ rss/nature_filter_rss.py | 122 ++++++ 25 files changed, 2291 insertions(+), 17 deletions(-) create mode 100644 GPUMD/Umap/umap_make.py create mode 100644 GPUMD/Umap/umap_make_2.py create mode 100644 GPUMD/data_POSCAR/origin/pnma.vasp create mode 100644 GPUMD/raw2xyz.py create mode 100644 GPUMD/swap_li.py create mode 100644 GPUMD/t-SNE/t-SNE.py create mode 100644 Li_Conductivity/data/conductivity_results.csv create mode 100644 contrast learning/split.py create mode 100644 data_get/new_v1/data/input/最新数据库核查过gsj.xlsx create mode 100644 data_get/new_v1/data_get.py create mode 100644 dpgen/create_supercell_poscar.py create mode 100644 dpgen/supercell_make_pnma.py create mode 100644 dpgen/supercell_make_wangshuo.py create mode 100644 mcp/vasp_mcp.py create mode 100644 rss/nature_filter_rss.py diff --git a/GPUMD/Umap/umap_make.py b/GPUMD/Umap/umap_make.py new file mode 100644 index 0000000..6a2f127 --- /dev/null +++ b/GPUMD/Umap/umap_make.py @@ -0,0 +1,76 @@ +from pathlib import Path +import numpy as np +import matplotlib +matplotlib.use("Agg") # 仅保存图片,不弹窗 +import matplotlib.pyplot as plt +from umap import UMAP + +def umap_dir_to_pngs(dir_path: str) -> None: + """ + 对目录内每个 .npy 文件执行 UMAP(30D->2D) 并保存散点图。 + - 输入 .npy 期望形状为 (n_samples, 30) 或 (30, n_samples) + - 输出图片保存在同目录,命名为 <原文件名>_umap.png + """ + p = Path(dir_path) + if not p.is_dir(): + raise ValueError(f"{dir_path!r} 不是有效文件夹") + + files = sorted(p.glob("*.npy")) + if not files: + print(f"目录 {p} 中未找到 .npy 文件") + return + + for f in files: + try: + data = np.load(f) + if data.ndim == 2: + if data.shape[1] == 30: + X = data + elif data.shape[0] == 30: + X = data.T + else: + print(f"[跳过] {f.name}: shape={data.shape}, 未检测到 30 维特征") + continue + else: + print(f"[跳过] {f.name}: 期望二维数组,实际 shape={data.shape}") + continue + + # 清理非数值行 + mask = np.isfinite(X).all(axis=1) + if not np.all(mask): + X = X[mask] + print(f"[提示] {f.name}: 移除了含 NaN/Inf 的样本行") + + n_samples = X.shape[0] + if n_samples < 3: + print(f"[跳过] {f.name}: 样本数过少(n={n_samples}),无法稳定降维") + continue + + # 确保 n_neighbors 合法 + n_neighbors = min(15, max(2, n_samples - 1)) + reducer = UMAP( + n_components=2, + n_neighbors=n_neighbors, + min_dist=0.1, + metric="euclidean", + random_state=42, + ) + emb = reducer.fit_transform(X) + + fig, ax = plt.subplots(figsize=(6, 5), dpi=150) + ax.scatter(emb[:, 0], emb[:, 1], s=6, c="#1f77b4", alpha=0.8, edgecolors="none") + ax.set_title(f"{f.name} • UMAP (n={len(X)}, nn={n_neighbors})", fontsize=10) + ax.set_xlabel("UMAP-1") + ax.set_ylabel("UMAP-2") + ax.grid(True, linestyle="--", linewidth=0.3, alpha=0.5) + fig.tight_layout() + + out_png = f.with_suffix("").as_posix() + "_umap.png" + fig.savefig(out_png) + plt.close(fig) + print(f"[完成] {f.name} -> {out_png}") + except Exception as e: + print(f"[错误] 处理 {f.name} 失败: {e}") + +if __name__=="__main__": + umap_dir_to_pngs("data") \ No newline at end of file diff --git a/GPUMD/Umap/umap_make_2.py b/GPUMD/Umap/umap_make_2.py new file mode 100644 index 0000000..d86eaaa --- /dev/null +++ b/GPUMD/Umap/umap_make_2.py @@ -0,0 +1,161 @@ +from pathlib import Path +import numpy as np +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +try: + from umap import UMAP +except Exception: + from umap.umap_ import UMAP + + +def umap_dir_shared_coords( + dir_path: str, + *, + metric: str = "cosine", + n_neighbors: int = 15, + min_dist: float = 0.0, + spread: float = 1.2, + standardize: bool = False, + context: bool = True, + make_joint: bool = True, + init: str = "random", # 关键:禁用谱初始化,避免告警;也可用 "pca" + jitter: float = 0.0, # 可选:拟合前加微弱噪声,如 1e-6 + random_state: int = 42 +) -> None: + """ + 在同一 UMAP 坐标系中为目录内每个 .npy 文件生成 2D 图。 + - 每个 .npy 形状为 (n_samples, 30) 或 (30, n_samples) + - 统一坐标轴范围;各自输出 *_umap_shared.png,另可输出总览图 + """ + p = Path(dir_path) + if not p.is_dir(): + raise ValueError(f"{dir_path!r} 不是有效文件夹") + + files = sorted(p.glob("*.npy")) + if not files: + print(f"目录 {p} 中未找到 .npy 文件") + return + + X_list, paths, counts = [], [], [] + for f in files: + try: + data = np.load(f) + if data.ndim != 2: + print(f"[跳过] {f.name}: 期望二维数组,实际 shape={data.shape}") + continue + + if data.shape[1] == 30: + X = data + elif data.shape[0] == 30: + X = data.T + else: + print(f"[跳过] {f.name}: shape={data.shape}, 未检测到 30 维特征") + continue + + mask = np.isfinite(X).all(axis=1) + if not np.all(mask): + X = X[mask] + print(f"[提示] {f.name}: 移除了含 NaN/Inf 的样本行") + + if X.shape[0] < 3: + print(f"[跳过] {f.name}: 样本数过少(n={X.shape[0]})") + continue + + X_list.append(X) + paths.append(f) + counts.append(X.shape[0]) + except Exception as e: + print(f"[错误] 读取 {f.name} 失败: {e}") + + if not X_list: + print("未找到可用的数据文件") + return + + X_all = np.vstack(X_list) + + if standardize: + mean = X_all.mean(axis=0) + std = X_all.std(axis=0) + std[std == 0] = 1.0 + X_all = (X_all - mean) / std + + if jitter and jitter > 0: + rng = np.random.default_rng(random_state) + X_all = X_all + rng.normal(scale=jitter, size=X_all.shape) + + reducer = UMAP( + n_components=2, + n_neighbors=int(max(2, n_neighbors)), + min_dist=float(min_dist), + spread=float(spread), + metric=metric, + init=init, # 关键改动:避免谱初始化告警 + random_state=random_state, + ) + Z_all = reducer.fit_transform(X_all) + + x_min, x_max = float(Z_all[:, 0].min()), float(Z_all[:, 0].max()) + y_min, y_max = float(Z_all[:, 1].min()), float(Z_all[:, 1].max()) + pad_x = 0.05 * (x_max - x_min) if x_max > x_min else 1.0 + pad_y = 0.05 * (y_max - y_min) if y_max > y_min else 1.0 + + base_colors = [ + "#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd", + "#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf" + ] + + start = 0 + for i, (f, n) in enumerate(zip(paths, counts)): + Zi = Z_all[start:start + n] + start += n + + fig, ax = plt.subplots(figsize=(6, 5), dpi=150) + if context: + ax.scatter(Z_all[:, 0], Z_all[:, 1], s=5, c="#cccccc", + alpha=0.35, edgecolors="none", label="All") + ax.scatter(Zi[:, 0], Zi[:, 1], s=10, + c=base_colors[i % len(base_colors)], + alpha=0.9, edgecolors="none", label=f.name) + + ax.set_title( + f"{f.name} • UMAP(shared) (nn={n_neighbors}, min={min_dist}, metric={metric}, init={init})", + fontsize=9 + ) + ax.set_xlabel("UMAP-1") + ax.set_ylabel("UMAP-2") + ax.set_xlim(x_min - pad_x, x_max + pad_x) + ax.set_ylim(y_min - pad_y, y_max + pad_y) + ax.grid(True, linestyle="--", linewidth=0.3, alpha=0.5) + if context: + ax.legend(loc="best", fontsize=8, frameon=False) + fig.tight_layout() + + out_png = f.with_suffix("").as_posix() + "_umap_shared.png" + fig.savefig(out_png) + plt.close(fig) + print(f"[完成] {f.name} -> {out_png}") + + if make_joint: + start = 0 + fig, ax = plt.subplots(figsize=(7, 6), dpi=150) + for i, (f, n) in enumerate(zip(paths, counts)): + Zi = Z_all[start:start + n]; start += n + ax.scatter(Zi[:, 0], Zi[:, 1], s=8, + c=base_colors[i % len(base_colors)], + alpha=0.85, edgecolors="none", label=f.name) + ax.set_title(f"UMAP(shared) overview (metric={metric}, nn={n_neighbors}, min={min_dist}, init={init})", + fontsize=10) + ax.set_xlabel("UMAP-1"); ax.set_ylabel("UMAP-2") + ax.set_xlim(x_min - pad_x, x_max + pad_x) + ax.set_ylim(y_min - pad_y, y_max + pad_y) + ax.grid(True, linestyle="--", linewidth=0.3, alpha=0.5) + ax.legend(loc="best", fontsize=8, frameon=False, ncol=1) + fig.tight_layout() + out_png = Path(dir_path) / "umap_shared_overview.png" + fig.savefig(out_png.as_posix()) + plt.close(fig) + print(f"[完成] 总览 -> {out_png}") +if __name__=="__main__": + umap_dir_shared_coords("data") \ No newline at end of file diff --git a/GPUMD/data_POSCAR/origin/pnma.vasp b/GPUMD/data_POSCAR/origin/pnma.vasp new file mode 100644 index 0000000..7c5ad26 --- /dev/null +++ b/GPUMD/data_POSCAR/origin/pnma.vasp @@ -0,0 +1,88 @@ +Li Y Cl + 1.0000000000000000 + 12.1082364219999992 -0.0000000000000000 0.0000000000000000 + 0.0000420925000000 12.6964871139000000 0.0000000000000000 + 0.0000111360000000 0.0000097283000000 11.1520040839999997 + Li Y Cl + 24 8 48 +Cartesian + 3.0170113299999999 11.0208475999999997 6.5429541999999996 + 9.0710813300000002 11.0208076100000003 6.5429413099999998 + 3.0372732299999998 1.6755553700000001 0.9669378900000000 + 9.0914532300000008 1.6755853700000001 0.9668849900000001 + 5.9960454600000004 8.0228419300000002 4.6273539599999998 + 12.0502254600000001 8.0228319300000006 4.6273410699999999 + 6.0439837100000000 8.0239104000000001 0.9669839400000000 + 12.0980237099999997 8.0238703999999998 0.9669410500000000 + 2.9687930800000002 11.0219791300000001 10.2032142199999996 + 9.0228930799999993 11.0219691300000004 10.2032413300000009 + 3.0851749800000001 1.6745967300000000 4.6273578600000000 + 9.1393549800000002 1.6745967399999999 4.6273049700000000 + 0.0581325900000000 4.6736939399999997 10.2033481199999994 + 6.1122525899999998 4.6736539400000003 10.2033652299999993 + 0.0102008400000000 4.6725925699999999 6.5430081500000004 + 6.0643308400000002 4.6725225699999999 6.5430152499999998 + 6.0582017800000001 11.3105425600000000 6.4882826299999996 + 0.0040517800000000 11.3105725600000007 6.4882655299999996 + 3.0311532099999998 7.7341853599999997 0.9123054900000001 + 9.0851632099999993 7.7341953600000002 0.9123126000000000 + 3.0230812999999999 4.9623175699999997 6.4882665900000003 + 9.0772212999999997 4.9622875700000000 6.4882637000000001 + 12.1043127199999994 1.3859403699999999 0.9123036700000000 + 6.0501727199999999 1.3859103699999999 0.9123265600000000 + 0.0501691200000000 4.7065010400000000 2.8276881199999999 + 6.1042791200000002 4.7064910400000004 2.8277152200000000 + 6.0040072100000001 7.9899634099999997 8.4036839699999994 + 12.0581272100000003 7.9900134100000004 8.4037010799999994 + 3.0772167300000000 1.6417582399999999 8.4037178800000003 + 9.1312967300000007 1.6417482400000001 8.4036949900000000 + 2.9769896100000000 11.0547561999999999 2.8276842100000001 + 9.0310896100000004 11.0547362000000007 2.8277013100000001 + 4.5156189400000004 10.0925444199999994 4.7485038499999996 + 10.5696489400000004 10.0924044199999994 4.7485109599999999 + 1.5387092400000000 2.6040715400000001 10.3245482299999995 + 7.5927392300000003 2.6040115400000001 10.3245253399999992 + 1.3626281400000000 9.1096203100000004 6.4718857500000002 + 7.4167081399999999 9.1095703100000005 6.4718328500000002 + 1.4883697199999999 8.9523616199999996 10.3245457500000004 + 7.5425797200000000 8.9522516299999992 10.3245128600000005 + 4.3896168400000004 9.9351631099999995 0.8958039700000000 + 10.4437368399999997 9.9351431100000003 0.8958210800000000 + 1.6644776500000000 2.7613398100000000 6.4718681100000000 + 7.7185976500000004 2.7613198099999998 6.4718352200000000 + 4.6916063499999998 3.5868826100000000 0.8958463400000000 + 10.7457363499999996 3.5868826100000000 0.8958134500000000 + 4.5657084499999998 3.7442043400000000 4.7485363400000002 + 10.6197684500000005 3.7442543399999999 4.7484934399999998 + 1.4271435299999999 5.7840809399999999 8.3327970300000000 + 7.4812435300000004 5.7840109399999999 8.3328141400000000 + 4.6270127299999997 6.9124334500000000 2.7567950600000000 + 10.6811427299999995 6.9124334500000000 2.7567721600000001 + 4.4542422500000001 0.5641837300000000 2.7567976500000002 + 10.5083422500000001 0.5641637400000000 2.7567847500000000 + 1.5999540200000000 12.1323306500000001 8.3327844399999993 + 7.6540440199999997 12.1323306500000001 8.3327815399999992 + 1.4488319300000001 5.8551694799999998 4.7388569900000004 + 7.5029319299999999 5.8551594800000002 4.7388941000000004 + 4.6052662299999998 6.8413464700000004 10.3148350900000008 + 10.6594162299999997 6.8413164799999997 10.3148222000000001 + 4.4984539600000000 0.5241951300000000 6.4733476400000001 + 10.5525539599999991 0.5241751300000000 6.4733547500000004 + 4.4759357399999997 0.4930566900000000 10.3148076700000004 + 10.5300357400000006 0.4930866900000000 10.3148047700000003 + 1.4714200500000001 5.8240679200000001 0.8973369900000000 + 7.5254900500000002 5.8240779299999996 0.8973441000000000 + 4.5827044399999997 6.8724549899999996 6.4733550900000001 + 10.6368744399999997 6.8724349900000004 6.4733521999999999 + 1.5557505300000001 12.1723877900000002 0.8973444400000000 + 7.6098205300000004 12.1722777900000008 0.8973515500000000 + 1.5782524200000001 12.2033792699999992 4.7388244200000003 + 7.6324124199999996 12.2033992700000002 4.7388615300000003 + 1.5507855200000000 2.5414585299999999 2.7575782499999999 + 7.6049855199999996 2.5414785300000000 2.7574953600000001 + 4.5034907500000001 10.1550858599999998 8.3335738300000006 + 10.5574507499999992 10.1550558599999992 8.3335109400000000 + 4.5777702700000003 3.8068257399999998 8.3335863099999994 + 10.6319002699999992 3.8067957400000001 8.3335634100000000 + 1.4764060000000001 8.8896886500000001 2.7575657800000002 + 7.5304859999999998 8.8896686500000008 2.7575728900000001 diff --git a/GPUMD/raw2xyz.py b/GPUMD/raw2xyz.py new file mode 100644 index 0000000..fccd548 --- /dev/null +++ b/GPUMD/raw2xyz.py @@ -0,0 +1,149 @@ +import os +import numpy as np + + +def convert_raw_to_gpumd_xyz(input_folder: str, output_filename: str = "gpumd_nep_training_data.xyz"): + """ + 将 DeePMD-kit 风格的 .raw 训练数据转换为 GPUMD NEP 训练所需的 extended XYZ 格式。 + 调整为 GPUMD 期望的格式,包括在注释行中添加 Properties 字段, + 并将每个原子的力数据附加到原子坐标行。 + Args: + input_folder (str): 包含 .raw 文件的文件夹路径 (例如 './set.000/'). + output_filename (str): 输出的 GPUMD extended XYZ 文件的名称。 + Raises: + FileNotFoundError: 如果必需的 .raw 文件不存在。 + ValueError: 如果数据格式不符合预期。 + """ + required_files = [ + 'box.raw', 'coord.raw', 'energy.raw', 'force.raw', + 'type.raw', 'type_map.raw', 'virial.raw' + ] + # 检查所有必需的文件是否存在 + for filename in required_files: + filepath = os.path.join(input_folder, filename) + if not os.path.exists(filepath): + raise FileNotFoundError( + f"Missing required file: {filepath}. Please ensure all .raw files are in the specified folder.") + print(f"Loading raw from folder: {input_folder}") + + # --- 1. 读取数据 --- + try: + # 读取 type_map.raw + with open(os.path.join(input_folder, 'type_map.raw'), 'r') as f: + type_map_list = [line.strip() for line in f if line.strip()] # 移除空行 + + # 首次加载 coord.raw 来确定 num_atoms + first_coord_line = np.loadtxt(os.path.join(input_folder, 'coord.raw'), max_rows=1) + if first_coord_line.ndim == 0: # 如果只有1个数字 + num_atoms = 1 + else: + num_atoms = first_coord_line.shape[0] // 3 + if num_atoms == 0: + raise ValueError(f"Could not determine num_atoms from coord.raw. It seems empty or malformed.") + + # 现在有了正确的 num_atoms,重新加载 type.raw 以获取原子类型列表 + with open(os.path.join(input_folder, 'type.raw'), 'r') as f: + all_types_lines = f.readlines() + if not all_types_lines: + raise ValueError(f"{os.path.join(input_folder, 'type.raw')} is empty or malformed.") + + # 假设所有构型的原子类型序列是相同的,我们只需要第一个构型的类型 + first_type_config = np.array([int(x) for x in all_types_lines[0].strip().split()]) + if len(first_type_config) != num_atoms: + # 尝试另一种 DeePMD 常见的 type.raw 格式:一个长序列,表示所有原子类型 + # 如果 type.raw 的行数等于原子数,我们假设每行一个原子类型 + if len(all_types_lines) == num_atoms: + atom_types_numeric = np.array([int(line.strip()) for line in all_types_lines]) + else: + raise ValueError( + f"Mismatch between num_atoms ({num_atoms}) derived from coord.raw and type.raw format. " + f"First line of type.raw has {len(first_type_config)} types, total lines {len(all_types_lines)}. " + f"Please check type.raw format and adjust script.") + else: + atom_types_numeric = first_type_config # 正常情况,第一行就是第一个构型的所有原子类型 + + atom_symbols = [type_map_list[t] for t in atom_types_numeric] + + # 读取其他数据 + boxes = np.loadtxt(os.path.join(input_folder, 'box.raw')).reshape(-1, 3, 3) + coords_flat = np.loadtxt(os.path.join(input_folder, 'coord.raw')) + energies = np.loadtxt(os.path.join(input_folder, 'energy.raw')) + forces_flat = np.loadtxt(os.path.join(input_folder, 'force.raw')) + virials_flat = np.loadtxt(os.path.join(input_folder, 'virial.raw')) # 可能是 9 个分量 + + except Exception as e: + raise ValueError(f"Error reading .raw files. Please check their format. Details: {e}") + + # 验证数据维度 + num_configs = len(energies) + expected_coord_cols = num_atoms * 3 + expected_virial_cols = 9 # DeepMD通常输出9个分量 + + if coords_flat.shape[1] != expected_coord_cols: + raise ValueError( + f"coord.raw has {coords_flat.shape[1]} columns, but expected {expected_coord_cols} (N_atoms * 3).") + if boxes.shape[0] != num_configs: + raise ValueError(f"box.raw has {boxes.shape[0]} configurations, but expected {num_configs}. Data mismatch.") + if forces_flat.shape[1] != expected_coord_cols: + raise ValueError( + f"force.raw has {forces_flat.shape[1]} columns, but expected {expected_coord_cols} (N_atoms * 3). Check file format.") + if virials_flat.shape[0] != num_configs or virials_flat.shape[1] != expected_virial_cols: + raise ValueError( + f"virial.raw has shape {virials_flat.shape}, but expected ({num_configs}, {expected_virial_cols}). Check file format.") + + coords = coords_flat.reshape(num_configs, num_atoms, 3) + forces = forces_flat.reshape(num_configs, num_atoms, 3) + virials_matrix = virials_flat.reshape(num_configs, 3, 3) + + print(f"Loaded {num_configs} configurations with {num_atoms} atoms each.") + + # --- 2. 写入到 GPUMD NEP 的 extended XYZ 格式 --- + # 确保输出路径的目录存在 + output_dir = os.path.dirname(output_filename) + if output_dir and not os.path.exists(output_dir): + os.makedirs(output_dir) + + output_filepath = output_filename # 直接使用传入的output_filename作为最终路径 + + with open(output_filepath, 'w') as f: + for i in range(num_configs): + # 第一行:原子数量 + f.write(f"{num_atoms}\n") + + # 第二行:元数据 + box_matrix_flat = boxes[i].flatten() + box_str = " ".join(f"{x:.10f}" for x in box_matrix_flat) + energy_str = f"{energies[i]:.10f}" + + virial_tensor = virials_matrix[i] + # --- 关键修改处:输出 Virial 的九个分量 --- + # 展平 3x3 矩阵以得到九个分量 + virial_gpumd_components = virial_tensor.flatten() + virial_str = " ".join(f"{x:.10f}" for x in virial_gpumd_components) + + # 构造 GPUMD 兼容的第二行 + config_type_str = f"Config_type=dpgen_iter{i:03d}" # 示例:迭代号,可以自定义 + weight_str = "Weight=1.0" + properties_str = "Properties=species:S:1:pos:R:3:forces:R:3" # 关键修改 + + f.write( + f'{config_type_str} {weight_str} Lattice="{box_str}" Energy={energy_str} Virial="{virial_str}" pbc="T T T" {properties_str}\n' + ) + + # 后续行:原子符号、坐标和力 + for j in range(num_atoms): + x, y, z = coords[i, j] + fx, fy, fz = forces[i, j] + f.write(f"{atom_symbols[j]} {x:.10f} {y:.10f} {z:.10f} {fx:.10f} {fy:.10f} {fz:.10f}\n") + + print(f"Successfully converted {num_configs} configurations to {output_filepath}") + print(f"Output file saved at: {output_filepath}") + + +# --- 如何使用这个函数 --- +if __name__ == "__main__": + # 示例用法: + input_folder_path = 'data/dpmd_data/lyc/training_data/p3m1_data/raw' + output_file_path = 'data/dpmd_data/lyc/training_data/p3m1_data/p3m1_train.xyz' + + convert_raw_to_gpumd_xyz(input_folder=input_folder_path, output_filename=output_file_path) diff --git a/GPUMD/swap_li.py b/GPUMD/swap_li.py new file mode 100644 index 0000000..eaf4b69 --- /dev/null +++ b/GPUMD/swap_li.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +# -*- coding: ascii -*- +""" +Randomly swap one Li-Y pair in a VASP5 POSCAR and write N new files. +- Keeps coordinate mode (Direct/Cartesian), Selective Dynamics flags, and Velocities. +- Requires VASP5+ POSCAR (with element symbols line). +""" + +import random +from pathlib import Path + + + +def _is_ints(tokens): + try: + _ = [int(t) for t in tokens] + return True + except ValueError: + return False + +def _find_species_index(species, target): + t = target.lower() + for i, s in enumerate(species): + if s.lower() == t: + return i + raise ValueError("Element '%s' not found in species line: %s" % (target, " ".join(species))) + +def parse_poscar(lines): + if len(lines) < 8: + raise ValueError("POSCAR too short") + + comment = lines[0].rstrip("\n") + scale = lines[1].rstrip("\n") + lv = [lines[2].rstrip("\n"), lines[3].rstrip("\n"), lines[4].rstrip("\n")] + + i = 5 + tokens = lines[i].split() + if _is_ints(tokens): + raise ValueError("VASP4 format (no element symbols line) is not supported.") + species = tokens + i += 1 + + counts_line = lines[i].rstrip("\n") + counts = [int(x) for x in counts_line.split()] + i += 1 + + selective = False + sel_line = None + if i < len(lines) and lines[i].strip().lower().startswith("s"): + selective = True + sel_line = lines[i].rstrip("\n") + i += 1 + + coord_line = lines[i].rstrip("\n") + i += 1 + + natoms = sum(counts) + pos_start = i + pos_end = i + natoms + if pos_end > len(lines): + raise ValueError("Atom count exceeds file length.") + pos_lines = [lines[j].rstrip("\n") for j in range(pos_start, pos_end)] + + # Optional Velocities section + k = pos_end + while k < len(lines) and lines[k].strip() == "": + k += 1 + + vel_header = None + vel_lines = None + vel_end = k + if k < len(lines) and lines[k].strip().lower().startswith("veloc"): + vel_header = lines[k].rstrip("\n") + vel_start = k + 1 + vel_end = vel_start + natoms + if vel_end > len(lines): + raise ValueError("Velocities section length inconsistent with atom count.") + vel_lines = [lines[j].rstrip("\n") for j in range(vel_start, vel_end)] + + tail_lines = [lines[j].rstrip("\n") for j in range(vel_end, len(lines))] if vel_end < len(lines) else [] + + # Species index ranges (by order in species list) + starts = [] + acc = 0 + for c in counts: + starts.append(acc) + acc += c + species_ranges = [] + for idx, sp in enumerate(species): + s, e = starts[idx], starts[idx] + counts[idx] + species_ranges.append((sp, s, e)) + + return { + "comment": comment, + "scale": scale, + "lv": lv, + "species": species, + "counts": counts, + "counts_line": counts_line, + "selective": selective, + "sel_line": sel_line, + "coord_line": coord_line, + "natoms": natoms, + "pos_lines": pos_lines, + "vel_header": vel_header, + "vel_lines": vel_lines, + "tail_lines": tail_lines, + "species_ranges": species_ranges, + } + +def build_poscar(data, pos_lines, vel_lines=None): + out = [] + out.append(data["comment"]) + out.append(data["scale"]) + out.extend(data["lv"]) + out.append(" ".join(data["species"])) + out.append(data["counts_line"]) + if data["selective"]: + out.append(data["sel_line"] if data["sel_line"] is not None else "Selective dynamics") + out.append(data["coord_line"]) + out.extend(pos_lines) + if data["vel_header"] is not None and vel_lines is not None: + out.append(data["vel_header"]) + out.extend(vel_lines) + if data["tail_lines"]: + out.extend(data["tail_lines"]) + return "\n".join(out) + "\n" + +def _swap_once(data, rng, li_label="Li", y_label="Y"): + si_li = _find_species_index(data["species"], li_label) + si_y = _find_species_index(data["species"], y_label) + _, li_start, li_end = data["species_ranges"][si_li] + _, y_start, y_end = data["species_ranges"][si_y] + + li_pick = rng.randrange(li_start, li_end) + y_pick = rng.randrange(y_start, y_end) + + new_pos = list(data["pos_lines"]) + new_pos[li_pick], new_pos[y_pick] = new_pos[y_pick], new_pos[li_pick] + + new_vel = None + if data["vel_lines"] is not None: + new_vel = list(data["vel_lines"]) + new_vel[li_pick], new_vel[y_pick] = new_vel[y_pick], new_vel[li_pick] + + return new_pos, new_vel, (li_pick, y_pick) + +def swap(n, input_file, output_dir): + """ + Generate n POSCAR files, each with one random Li-Y swap. + + Returns: list of Path to written files. + """ + input_path = Path(input_file) + out_dir = Path(output_dir) + out_dir.mkdir(parents=True, exist_ok=True) + + lines = input_path.read_text().splitlines() + data = parse_poscar(lines) + + rng = random.Random() + base = input_path.name + + out_paths = [] + for k in range(1, n + 1): + new_pos, new_vel, picked = _swap_once(data, rng) + txt = build_poscar(data, new_pos, new_vel) + out_path = out_dir / f"swap_{k}_{base}" + out_path.write_text(txt) + out_paths.append(out_path) + print(f"Wrote {out_path} (swapped Li idx {picked[0]} <-> Y idx {picked[1]})") + return out_paths +# --------- Editable defaults for direct run --------- +INPUT_FILE = "data_POSCAR/origin/p3m1.vasp" # path to input POSCAR +OUTPUT_DIR = "data_POSCAR/p3m1" # output directory +N = 5 # number of files to generate +# ---------------------------------------------------- +if __name__ == "__main__": + # Direct-run entry: edit INPUT_FILE/OUTPUT_DIR/N above to change behavior. + swap(n=N, input_file=INPUT_FILE, output_dir=OUTPUT_DIR) \ No newline at end of file diff --git a/GPUMD/t-SNE/t-SNE.py b/GPUMD/t-SNE/t-SNE.py new file mode 100644 index 0000000..ab782e2 --- /dev/null +++ b/GPUMD/t-SNE/t-SNE.py @@ -0,0 +1,140 @@ +from pathlib import Path +import numpy as np +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from sklearn.manifold import TSNE +from sklearn.decomposition import PCA + +def tsne_dir_shared_coords( + dir_path: str, + *, + metric: str = "euclidean", # 可试 "cosine";想保留尺度差异用 "euclidean" + perplexity: float = 50.0, # 30k~50k 样本建议 30~50 + n_iter: int = 1000, + early_exaggeration: float = 12.0, + learning_rate = "auto", + standardize: bool = False, + pca_dim: int | None = None, # 先用 PCA 降到 pca_dim(如 20) 再跑 t-SNE,可提速 + context: bool = True, + make_joint: bool = True, + init: str = "pca", + random_state: int = 42 +) -> None: + p = Path(dir_path) + if not p.is_dir(): + raise ValueError(f"{dir_path!r} 不是有效文件夹") + + files = sorted(p.glob("*.npy")) + if not files: + print(f"目录 {p} 中未找到 .npy 文件") + return + + X_list, paths, counts = [], [], [] + for f in files: + try: + data = np.load(f) + if data.ndim != 2: + print(f"[跳过] {f.name}: 期望二维数组,实际 shape={data.shape}") + continue + + # 统一到 (n_samples, 30) + if data.shape[1] == 30: + X = data + elif data.shape[0] == 30: + X = data.T + else: + print(f"[跳过] {f.name}: shape={data.shape}, 未检测到 30 维特征") + continue + + mask = np.isfinite(X).all(axis=1) + if not np.all(mask): + X = X[mask] + print(f"[提示] {f.name}: 移除了含 NaN/Inf 的样本行") + + if X.shape[0] < 3: + print(f"[跳过] {f.name}: 样本数过少(n={X.shape[0]})") + continue + + X_list.append(X) + paths.append(f) + counts.append(X.shape[0]) + except Exception as e: + print(f"[错误] 读取 {f.name} 失败: {e}") + + if not X_list: + print("未找到可用的数据文件") + return + + X_all = np.vstack(X_list) + + if standardize: + mean = X_all.mean(axis=0) + std = X_all.std(axis=0); std[std == 0] = 1.0 + X_all = (X_all - mean) / std + + if pca_dim is not None and pca_dim > 2: + X_all = PCA(n_components=pca_dim, random_state=random_state).fit_transform(X_all) + + tsne = TSNE( + n_components=2, + metric=metric, + perplexity=float(perplexity), + early_exaggeration=float(early_exaggeration), + learning_rate=learning_rate, + init=init, + random_state=random_state, + method="barnes_hut", # 适合大样本 + angle=0.5, + verbose=0, + ) + Z_all = tsne.fit_transform(X_all) + + # 统一坐标轴范围 + x_min, x_max = float(Z_all[:, 0].min()), float(Z_all[:, 0].max()) + y_min, y_max = float(Z_all[:, 1].min()), float(Z_all[:, 1].max()) + pad_x = 0.05 * (x_max - x_min) if x_max > x_min else 1.0 + pad_y = 0.05 * (y_max - y_min) if y_max > y_min else 1.0 + + colors = [ + "#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd", + "#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf" + ] + + # 分文件出图 + start = 0 + for i, (f, n) in enumerate(zip(paths, counts)): + Zi = Z_all[start:start + n]; start += n + fig, ax = plt.subplots(figsize=(6, 5), dpi=150) + if context: + ax.scatter(Z_all[:, 0], Z_all[:, 1], s=5, c="#cccccc", alpha=0.35, edgecolors="none", label="All") + ax.scatter(Zi[:, 0], Zi[:, 1], s=8, c=colors[i % len(colors)], alpha=0.9, edgecolors="none", label=f.name) + ax.set_title(f"{f.name} • t-SNE(shared) (perp={perplexity}, metric={metric})", fontsize=9) + ax.set_xlabel("t-SNE-1"); ax.set_ylabel("t-SNE-2") + ax.set_xlim(x_min - pad_x, x_max + pad_x); ax.set_ylim(y_min - pad_y, y_max + pad_y) + ax.grid(True, linestyle="--", linewidth=0.3, alpha=0.5) + if context: ax.legend(loc="best", fontsize=8, frameon=False) + fig.tight_layout() + out_png = f.with_suffix("").as_posix() + "_tsne_shared.png" + fig.savefig(out_png); plt.close(fig) + print(f"[完成] {f.name} -> {out_png}") + + # 总览图 + if make_joint: + start = 0 + fig, ax = plt.subplots(figsize=(7, 6), dpi=150) + for i, (f, n) in enumerate(zip(paths, counts)): + Zi = Z_all[start:start + n]; start += n + ax.scatter(Zi[:, 0], Zi[:, 1], s=8, c=colors[i % len(colors)], alpha=0.85, edgecolors="none", label=f.name) + ax.set_title(f"t-SNE(shared) overview (perp={perplexity}, metric={metric})", fontsize=10) + ax.set_xlabel("t-SNE-1"); ax.set_ylabel("t-SNE-2") + ax.set_xlim(x_min - pad_x, x_max + pad_x); ax.set_ylim(y_min - pad_y, y_max + pad_y) + ax.grid(True, linestyle="--", linewidth=0.3, alpha=0.5) + ax.legend(loc="best", fontsize=8, frameon=False) + fig.tight_layout() + out_png = Path(dir_path) / "tsne_shared_overview.png" + fig.savefig(out_png.as_posix()); plt.close(fig) + print(f"[完成] 总览 -> {out_png}") + +if __name__ == "__main__": + tsne_dir_shared_coords("data") \ No newline at end of file diff --git a/Li_Conductivity/data/conductivity_results.csv b/Li_Conductivity/data/conductivity_results.csv new file mode 100644 index 0000000..0c64187 --- /dev/null +++ b/Li_Conductivity/data/conductivity_results.csv @@ -0,0 +1,3 @@ +Temperature(K),Conductivity(S/m) +300,0.0148 +425,6.3173 \ No newline at end of file diff --git a/MSD/main.py b/MSD/main.py index 730baad..217e055 100644 --- a/MSD/main.py +++ b/MSD/main.py @@ -1,7 +1,7 @@ from utils.MSD import * if __name__ == '__main__': - # file_path_li = 'data/msd_li.dat' + # file_path_li = 'raw/msd_li.dat' # final_msd = plot_and_get_final_msd(file_path_li, ion_name='Li⁺') num_li_ions = 144 # !! 请务必用您体系的真实值替换此示例值 !! diff --git a/MSD/utils/con2.py b/MSD/utils/con2.py index a101a24..a80570b 100644 --- a/MSD/utils/con2.py +++ b/MSD/utils/con2.py @@ -234,7 +234,7 @@ if __name__ == "__main__": # 检查文件是否存在 msd_path = os.path.join(folder_path, "msd_li.dat") - data_path = os.path.join(folder_path, "LYC.data") + data_path = os.path.join(folder_path, "LYC.raw") if not os.path.exists(msd_path): print(f" 跳过:缺少{msd_path}") continue diff --git a/contrast learning/copy.py b/contrast learning/copy.py index 506f47a..1a82a0a 100644 --- a/contrast learning/copy.py +++ b/contrast learning/copy.py @@ -133,14 +133,134 @@ def copy_cif_with_O_or_S_robust(source_dir: str, target_dir: str, dry_run: bool print(f"模拟运行结束:如果实际运行,将会有 {copied_count} 个文件被复制。") else: print(f"成功复制了 {copied_count} 个文件到目标文件夹。") + + +def copy_cif_without_Br_or_Cl(source_dir: str, target_dir: str, dry_run: bool = False): + """ + 从源文件夹中筛选出内容不含'Br'或'Cl'元素的CIF文件,并复制到目标文件夹。 + (鲁棒版:能正确解析CIF中的元素符号列) + + :param source_dir: 源文件夹路径,包含CIF文件。 + :param target_dir: 目标文件夹路径,用于存放筛选出的文件。 + :param dry_run: 如果为True,则只打印将要复制的文件,而不实际执行复制操作。 + """ + # 1. 路径处理和验证 (与原函数相同) + source_path = Path(source_dir) + target_path = Path(target_dir) + if not source_path.is_dir(): + print(f"错误:源文件夹 '{source_dir}' 不存在或不是一个文件夹。") + return + if not dry_run and not target_path.exists(): + target_path.mkdir(parents=True, exist_ok=True) + print(f"目标文件夹 '{target_dir}' 已创建。") + + print(f"源文件夹: {source_path}") + print(f"目标文件夹: {target_path}") + if dry_run: + print("\n--- *** 模拟运行模式 (Dry Run) *** ---") + print("--- 不会执行任何实际的文件复制操作 ---") + + # 2. 开始遍历和筛选 + print("\n开始扫描源文件夹,剔除含 Br 或 Cl 的CIF文件...") + copied_count = 0 + checked_files = 0 + error_files = 0 + excluded_files = 0 + + # 遍历所有 .cif 文件 + for file_path in source_path.glob('*.cif'): + if not file_path.is_file(): + continue + + checked_files += 1 + contains_br_or_cl = False # 标记文件是否包含 Br 或 Cl + + try: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + lines = f.readlines() + + # 步骤 A: 找到元素符号在哪一列 + element_col_idx = find_element_column_index(lines) + + if element_col_idx != -1: + # 优先使用结构数据进行精确判断 + for line in lines: + line_stripped = line.strip() + # 忽略空行、注释行和定义行 + if not line_stripped or line_stripped.startswith(('#', '_', 'loop_')): + continue + + parts = line_stripped.split() + # 确保行中有足够的列 + if len(parts) > element_col_idx: + atom_symbol = parts[element_col_idx].strip() + # 检查元素是否为 Br 或 Cl(也考虑类似 Br- 的情况) + if atom_symbol.upper().startswith('BR') or atom_symbol.upper().startswith('CL'): + contains_br_or_cl = True + break # 找到一个就足够,可以停止检查这个文件 + + # 步骤 B: 如果上述方法未找到,使用化学式作为备用检查 + if not contains_br_or_cl: + # 使用 any() 来高效检查,找到一个匹配即停止 + is_in_formula = any( + line.strip().startswith(('_chemical_formula_sum', '_chemical_formula_structural')) and + (' Br' in line or ' Cl' in line) + for line in lines + ) + if is_in_formula: + contains_br_or_cl = True + + # 步骤 C: 根据检查结果决定是否复制 + if contains_br_or_cl: + # 如果包含 Br 或 Cl,则打印信息并跳过 + print(f"排除文件: '{file_path.name}' (检测到 Br 或 Cl 元素)") + excluded_files += 1 + else: + # 如果不包含 Br 或 Cl,则执行复制 + target_file_path = target_path / file_path.name + print(f"找到匹配: '{file_path.name}' (不含 Br 或 Cl)") + if not dry_run: + shutil.copy2(file_path, target_file_path) + copied_count += 1 + + except Exception as e: + error_files += 1 + print(f"!! 处理文件 '{file_path.name}' 时发生错误: {e}") + + # 3. 打印最终报告 (与原函数类似,增加了排除计数) + print("\n--- 操作总结 ---") + print(f"共检查了 {checked_files} 个.cif文件。") + print(f"排除了 {excluded_files} 个含有 Br 或 Cl 的文件。") + if error_files > 0: + print(f"处理过程中有 {error_files} 个文件发生错误。") + if dry_run: + print(f"模拟运行结束:如果实际运行,将会有 {copied_count} 个文件被复制。") + else: + print(f"成功复制了 {copied_count} 个文件到目标文件夹。") + if __name__ == '__main__': # !! 重要:请将下面的路径修改为您自己电脑上的实际路径 - source_folder = "D:/download/2025-10/data_all/input/input" - target_folder = "D:/download/2025-10/data_all/output" + # source_folder = "D:/download/2025-10/data_all/input/input" + # target_folder = "D:/download/2025-10/data_all/output" + # + # # --- 第一次运行:使用模拟模式 (Dry Run) --- + # print("================ 第一次运行: 模拟模式 ================") + # copy_cif_with_O_or_S_robust(source_folder, target_folder, dry_run=True) + # + # print("\n\n=======================================================") + # input("检查上面的模拟运行结果。如果符合预期,按回车键继续执行实际复制操作...") + # print("=======================================================") + # + # # --- 第二次运行:实际执行复制 --- + # print("\n================ 第二次运行: 实际复制模式 ================") + # copy_cif_with_O_or_S_robust(source_folder, target_folder, dry_run=False) + + source_folder = "D:/download/2025-10/data_OS/input" + target_folder = "D:/download/2025-10/data_withoutBrCl/input" # --- 第一次运行:使用模拟模式 (Dry Run) --- print("================ 第一次运行: 模拟模式 ================") - copy_cif_with_O_or_S_robust(source_folder, target_folder, dry_run=True) + copy_cif_without_Br_or_Cl(source_folder, target_folder, dry_run=True) print("\n\n=======================================================") input("检查上面的模拟运行结果。如果符合预期,按回车键继续执行实际复制操作...") @@ -148,4 +268,4 @@ if __name__ == '__main__': # --- 第二次运行:实际执行复制 --- print("\n================ 第二次运行: 实际复制模式 ================") - copy_cif_with_O_or_S_robust(source_folder, target_folder, dry_run=False) + copy_cif_without_Br_or_Cl(source_folder, target_folder, dry_run=False) diff --git a/contrast learning/split.py b/contrast learning/split.py new file mode 100644 index 0000000..f5bdec3 --- /dev/null +++ b/contrast learning/split.py @@ -0,0 +1,111 @@ +import os +import shutil +import random +from pathlib import Path + + +def split_dataset(source_dir: str, output_dir: str, test_ratio: float = 0.2): + """ + 将源文件夹中的文件按比例划分到输出文件夹下的 train 和 test 子目录中。 + + Args: + source_dir (str): 包含所有数据文件的源文件夹路径。 + output_dir (str): 用于存放'train'和'test'文件夹的目标文件夹路径。 + test_ratio (float, optional): 测试集所占的比例。默认为 0.2。 + """ + print("--- 开始执行数据集划分 ---") + + # 1. 路径处理和验证 + source_path = Path(source_dir) + output_path = Path(output_dir) + + if not source_path.is_dir(): + print(f"错误:源文件夹 '{source_dir}' 不存在或不是一个目录。") + return + + # 2. 创建输出文件夹 (train 和 test) + train_dir = output_path / 'train' + test_dir = output_path / 'test' + + try: + os.makedirs(train_dir, exist_ok=True) + os.makedirs(test_dir, exist_ok=True) + print(f"输出目录已准备好: \n 训练集 -> {train_dir}\n 测试集 -> {test_dir}") + except OSError as e: + print(f"错误:创建输出目录时发生错误: {e}") + return + + # 3. 获取所有文件并随机打乱 + all_files = [f for f in source_path.iterdir() if f.is_file()] + + if not all_files: + print(f"警告:源文件夹 '{source_dir}' 中没有文件可供划分。") + return + + random.shuffle(all_files) + total_files = len(all_files) + print(f"在源文件夹中找到 {total_files} 个文件。") + + # 4. 计算分割数量 + num_test = int(total_files * test_ratio) + num_train = total_files - num_test + + print(f"划分计划 -> 训练集: {num_train} 个文件 | 测试集: {num_test} 个文件") + + # 5. 分割文件列表 + test_files = all_files[:num_test] + train_files = all_files[num_test:] + + # 6. 定义一个复制/移动文件的辅助函数 + def copy_files(files_to_copy, destination_dir): + copied_count = 0 + for file_path in files_to_copy: + try: + # 注意:这里使用的是复制(copy),更安全。 + # 如果你确认要移动(move)并且清空源文件夹,请将 shutil.copy 改为 shutil.move + shutil.copy(file_path, destination_dir) + copied_count += 1 + except Exception as e: + print(f"处理文件 '{file_path.name}' 时出错: {e}") + return copied_count + + # 7. 复制文件到对应的文件夹 + print(f"\n正在复制文件到 'train' 文件夹...") + copied_train = copy_files(train_files, train_dir) + print(f"成功复制 {copied_train} 个文件到训练集。") + + print(f"\n正在复制文件到 'test' 文件夹...") + copied_test = copy_files(test_files, test_dir) + print(f"成功复制 {copied_test} 个文件到测试集。") + + print("\n--- 数据集划分完成! ---") + + +# --- 如何使用这个函数 --- +if __name__ == '__main__': + # --- 请在这里配置你的文件夹路径 --- + + # 你的原始数据集所在的文件夹 + # 例如: 'C:/Users/YourUser/Desktop/my_dataset' (Windows) + # 或: '/home/user/project/raw/all_images' (Linux/macOS) + SOURCE_DATA_DIR = 'D:/download/2025-10/data_OS/input/S' + + # 你希望将'train'和'test'文件夹创建在哪里 + # 例如: 'C:/Users/YourUser/Desktop/split_output' (Windows) + # 或: '/home/user/project/raw/processed' (Linux/macOS) + # 如果使用 '.', 表示在当前脚本所在的目录下创建 + OUTPUT_DIR = 'D:/download/2025-10/data_OS/train/S' + + # --- 配置完成,下面是调用函数 --- + + # 检查示例路径是否存在,如果不存在则创建并填充一些假文件用于演示 + if not os.path.exists(SOURCE_DATA_DIR): + print(f"演示目录 '{SOURCE_DATA_DIR}' 不存在,正在创建并生成100个示例文件...") + os.makedirs(SOURCE_DATA_DIR) + for i in range(100): + with open(os.path.join(SOURCE_DATA_DIR, f'file_{i + 1:03d}.txt'), 'w') as f: + f.write(f'This is file {i + 1}.') + print("示例文件创建完毕。") + + # 调用函数执行划分 + split_dataset(SOURCE_DATA_DIR, OUTPUT_DIR, test_ratio=0.2) diff --git a/corner-sharing/0923_CS.py b/corner-sharing/0923_CS.py index 7ec61d3..b5eac2f 100644 --- a/corner-sharing/0923_CS.py +++ b/corner-sharing/0923_CS.py @@ -82,7 +82,7 @@ def process_cif_folder(cif_folder_path: str, output_csv_path: str): if __name__ == "__main__": # ----- 参数配置 ----- # 请将此路径修改为您存放CIF文件的文件夹的实际路径 - CIF_DIRECTORY = "data/0921" + CIF_DIRECTORY = "raw/0921" # 输出的CSV文件名 OUTPUT_CSV = "corner_sharing_results.csv" diff --git a/corner-sharing/utils/CS_analyse.py b/corner-sharing/utils/CS_analyse.py index 587edd1..e8818dc 100644 --- a/corner-sharing/utils/CS_analyse.py +++ b/corner-sharing/utils/CS_analyse.py @@ -349,7 +349,7 @@ def check_only_corner_sharing(sharing_results: Dict[str, Dict[str, int]]) -> int else: return 0 # 没有任何共享关系,也返回 0 -# structure = Structure.from_file("../data/0921/wjy_001.cif") +# structure = Structure.from_file("../raw/0921/wjy_001.cif") # a = CS_catulate(structure,notice=True) # b = CS_count(structure,a) # print(f"{a}\n{b}") diff --git a/corner-sharing/utils/analyze_env_st.py b/corner-sharing/utils/analyze_env_st.py index 6275216..48666bc 100644 --- a/corner-sharing/utils/analyze_env_st.py +++ b/corner-sharing/utils/analyze_env_st.py @@ -205,6 +205,6 @@ def export_envs(envlist, sp='Li', envtype='both', fname=None): f.write("Site index " + str(index) + ": " + str(i) + '\n') -# struct = Structure.from_file("../data/0921/wjy_475.cif") +# struct = Structure.from_file("../raw/0921/wjy_475.cif") # site_info = extract_sites(struct, envtype="both") # export_envs(site_info, sp="Li", envtype="both") \ No newline at end of file diff --git a/data_get/new_v1/data/input/最新数据库核查过gsj.xlsx b/data_get/new_v1/data/input/最新数据库核查过gsj.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..de596559fa8fbd1a206070c4ce77e2af9597ec0a GIT binary patch literal 93841 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 literal 0 HcmV?d00001 diff --git a/data_get/new_v1/data_get.py b/data_get/new_v1/data_get.py new file mode 100644 index 0000000..b3078e1 --- /dev/null +++ b/data_get/new_v1/data_get.py @@ -0,0 +1,127 @@ +import pandas as pd +import os +import re + + +def extract_cif_from_xlsx( + xlsx_path: str, + output_dir: str, + naming_mode: str = 'formula', + name_col: int = 0, + cif_col: int = 1, + prefix: str = 'wjy' +): + """ + 从 XLSX 文件中提取 CIF 数据并保存为单独的 .cif 文件。 + + Args: + xlsx_path (str): 输入的 XLSX 文件的路径。 + output_dir (str): 输出 .cif 文件的文件夹路径。 + naming_mode (str, optional): CIF 文件的命名模式。 + 可选值为 'formula' (使用第一列的名字) 或 + 'auto' (使用前缀+自动递增编号)。 + 默认为 'formula'。 + name_col (int, optional): 包含文件名的列的索引(从0开始)。默认为 0。 + cif_col (int, optional): 包含 CIF 内容的列的索引(从0开始)。默认为 1。 + prefix (str, optional): 在 'auto' 命名模式下使用的文件名前缀。默认为 'wjy'。 + + Raises: + FileNotFoundError: 如果指定的 xlsx_path 文件不存在。 + ValueError: 如果指定的 naming_mode 无效。 + Exception: 处理过程中发生的其他错误。 + """ + # --- 1. 参数校验和准备 --- + if not os.path.exists(xlsx_path): + raise FileNotFoundError(f"错误: 输入文件未找到 -> {xlsx_path}") + + if naming_mode not in ['formula', 'auto']: + raise ValueError(f"错误: 'naming_mode' 参数必须是 'formula' 或 'auto',但收到了 '{naming_mode}'") + + # 创建输出目录(如果不存在) + os.makedirs(output_dir, exist_ok=True) + print(f"CIF 文件将保存到: {output_dir}") + + try: + # --- 2. 读取 XLSX 文件 --- + # header=None 表示第一行不是标题,将其作为数据读取 + df = pd.read_excel(xlsx_path, header=None) + + # 跳过原始文件的表头行('formula', 'cif') + if str(df.iloc[0, name_col]).strip().lower() == 'formula' and str(df.iloc[0, cif_col]).strip().lower() == 'cif': + df = df.iloc[1:] + print("检测到并跳过了表头行。") + + # --- 3. 遍历数据并生成文件 --- + success_count = 0 + for index, row in df.iterrows(): + # 获取文件名和 CIF 内容 + formula_name = str(row[name_col]) + cif_content = str(row[cif_col]) + + # 跳过内容为空的行 + if pd.isna(row[name_col]) or pd.isna(row[cif_col]) or not cif_content.strip(): + print(f"警告: 第 {index + 2} 行数据不完整,已跳过。") + continue + + # --- 4. 根据命名模式确定文件名 --- + if naming_mode == 'formula': + # 清理文件名,替换掉不适合做文件名的特殊字符 + # 例如:将 (PO4)3 替换为 _PO4_3,将 / 替换为 _ + safe_filename = re.sub(r'[\\/*?:"<>|()]', '_', formula_name) + filename = f"{safe_filename}.cif" + else: # naming_mode == 'auto' + # 使用 format 方法来确保编号格式统一,例如 001, 002 + filename = f"{prefix}_{success_count + 1:03d}.cif" + + # 构造完整的输出文件路径 + output_path = os.path.join(output_dir, filename) + + # --- 5. 写入 CIF 文件 --- + try: + with open(output_path, 'w', encoding='utf-8') as f: + f.write(cif_content) + success_count += 1 + except IOError as e: + print(f"错误: 无法写入文件 {output_path}。原因: {e}") + + print(f"\n处理完成!成功提取并生成了 {success_count} 个 CIF 文件。") + + except Exception as e: + print(f"处理 XLSX 文件时发生错误: {e}") + + +# --- 函数使用示例 --- +if __name__ == '__main__': + # 假设您的 XLSX 文件名为 'materials.xlsx',且与此脚本在同一目录下 + source_xlsx_file = 'input/cif_dataset.xlsx' + + # 检查示例文件是否存在,如果不存在则创建一个 + if not os.path.exists(source_xlsx_file): + print(f"未找到示例文件 '{source_xlsx_file}',正在创建一个...") + example_data = { + 'formula': ['Li3Al0.3Ti1.7(PO4)3', 'Li6.5La3Zr1.75W0.25O12', 'Invalid/Name*Test'], + 'cif': ['# CIF Data for Li3Al0.3...\n_atom_site_type_symbol\n Li\n Al\n Ti\n P\n O', + '# CIF Data for Li6.5La3...\n_symmetry_space_group_name_H-M \'I a -3 d\'', + '# CIF Data for Invalid Name Test'] + } + pd.DataFrame(example_data).to_excel(source_xlsx_file, index=False, header=True) + print("示例文件创建成功。") + + # --- 示例 1: 使用第一列的 'formula' 命名 --- + # print("\n--- 示例 1: 使用 'formula' 命名模式 ---") + # output_folder_1 = 'cif_by_formula' + # extract_cif_from_xlsx( + # xlsx_path=source_xlsx_file, + # output_dir=output_folder_1, + # naming_mode='formula' + # ) + + # --- 示例 2: 使用 'wjy+编号' 自动命名 --- + print("\n--- 示例 2: 使用 'auto' 命名模式 ---") + output_folder_2 = 'cif_by_auto' + extract_cif_from_xlsx( + xlsx_path=source_xlsx_file, + output_dir=output_folder_2, + naming_mode='auto', + prefix='wjy' + ) \ No newline at end of file diff --git a/dpgen/create_supercell_poscar.py b/dpgen/create_supercell_poscar.py new file mode 100644 index 0000000..da25097 --- /dev/null +++ b/dpgen/create_supercell_poscar.py @@ -0,0 +1,79 @@ +from pymatgen.core import Structure +from pymatgen.io.vasp import Poscar + +def create_supercell_poscar(cif_path, supercell_matrix, output_filename="POSCAR_supercell"): + """ + 从CIF文件读取晶体结构,根据指定的矩阵进行扩胞,并生成VASP POSCAR文件。 + + Args: + cif_path (str): 输入的CIF文件路径。 + supercell_matrix (list or tuple): 3x3的扩胞矩阵。 + - 对于简单的对角扩胞 (例如 2x2x4),使用: [[2, 0, 0], [0, 2, 0], [0, 0, 4]] + - 对于非对角扩胞 (例如 a_s=3a, b_s=2a+4b, c_s=6c),使用: [[3, 0, 0], [2, 4, 0], [0, 0, 6]] + output_filename (str): 输出的POSCAR文件名。默认为 "POSCAR_supercell"。 + + Returns: + bool: 如果成功生成文件则返回 True,否则返回 False。 + """ + try: + # 1. 从CIF文件加载结构 + # 使用 from_file 静态方法直接读取 + # primitive=False 确保我们使用CIF中定义的晶胞,而不是其原胞 + original_structure = Structure.from_file(cif_path, primitive=False) + + print("--- 原始晶胞信息 ---") + print(f" 原子数: {original_structure.num_sites}") + print(f" 化学式: {original_structure.composition.reduced_formula}") + print(f" 晶格参数 (a, b, c, α, β, γ):") + lat = original_structure.lattice + print(f" {lat.a:.4f}, {lat.b:.4f}, {lat.c:.4f}, {lat.alpha:.2f}, {lat.beta:.2f}, {lat.gamma:.2f}") + + # 2. 进行扩胞操作 + # 注意:pymatgen 会自动处理原子坐标的映射 + supercell_structure = original_structure * supercell_matrix + + print("\n--- 扩胞后信息 ---") + print(f" 扩胞矩阵: {supercell_matrix}") + print(f" 新原子数: {supercell_structure.num_sites}") + print(f" 新化学式: {supercell_structure.composition.reduced_formula}") + print(f" 新晶格参数 (a, b, c, α, β, γ):") + super_lat = supercell_structure.lattice + print(f" {super_lat.a:.4f}, {super_lat.b:.4f}, {super_lat.c:.4f}, {super_lat.alpha:.2f}, {super_lat.beta:.2f}, {super_lat.gamma:.2f}") + + # 3. 创建Poscar对象并写入文件 + # comment 参数可以设置POSCAR文件的第一行注释 + poscar = Poscar(supercell_structure, comment=f"Supercell from {cif_path}") + poscar.write_file(output_filename) + + print(f"\n成功!已将扩胞结构写入文件: {output_filename}") + return True + + except Exception as e: + print(f"发生错误: {e}") + return False + +# --- 使用示例 --- + +# 假设您的CIF文件名为 "origin.cif",并且与此脚本在同一目录下。 +# 如果您在复现 Wang Shuo 的工作,他们可能使用了不同的扩胞方案。 +# 例如,用于MD模拟的大超胞是 2x2x4 [source_id: 3]。 +# 而用于DP-GEN探索的小超胞是 1x1x2 [source_id: 3]。 + +# 示例1:生成用于DP-GEN探索的 1x1x2 小超胞 (60个原子) +print("="*40) +print("正在生成 1x1x2 超胞 (用于 DP-GEN 探索)...") +matrix_1x1x2 = [[1, 0, 0], [0, 1, 0], [0, 0, 2]] +create_supercell_poscar("data/P3ma/origin.cif", matrix_1x1x2, "data/P3ma/output/POSCAR_1x1x2_60atoms") + +# 示例2:生成用于LAMMPS MD模拟的 2x2x4 大超胞 (480个原子) +# print("\n" + "="*40) +# print("正在生成 2x2x4 超胞 (用于 LAMMPS MD 模拟)...") +# matrix_2x2x4 = [[2, 0, 0], [0, 2, 0], [0, 0, 4]] +# create_supercell_poscar("origin.cif", matrix_2x2x4, "POSCAR_2x2x4_480atoms") +# +# # 示例3:生成 Geng 等人研究中使用的大超胞 (2160个原子) [source_id: 1] +# # 这个扩胞矩阵 a_s = 3a_0, b_s = 2a_0 + 4b_0, c_s = 6c_0 [source_id: 1] +# print("\n" + "="*40) +# print("正在生成非对角扩胞超胞 (Geng et al.)...") +# matrix_geng = [[3, 0, 0], [2, 4, 0], [0, 0, 6]] +# create_supercell_poscar("origin.cif", matrix_geng, "POSCAR_Geng_2160atoms") \ No newline at end of file diff --git a/dpgen/plus.py b/dpgen/plus.py index 8d496af..4dabe50 100644 --- a/dpgen/plus.py +++ b/dpgen/plus.py @@ -147,5 +147,5 @@ def make_pnma_poscar_from_cif(cif_path: str, print(f"写出 {out_poscar};总原子数 = {len(s)};组成 = {comp}") if __name__=="__main__": - # make_model3_poscar_from_cif("data/P3ma/model3.cif","data/P3ma/supercell_model4.poscar") - make_pnma_poscar_from_cif("data/Pnma/origin.cif","data/Pnma/supercell_pnma.poscar",seed=42) \ No newline at end of file + # make_model3_poscar_from_cif("raw/P3ma/model3.cif","raw/P3ma/supercell_model4.poscar") + make_pnma_poscar_from_cif("data/Pnma/origin.cif","raw/Pnma/supercell_pnma.poscar",seed=42) \ No newline at end of file diff --git a/dpgen/supercell_make_p3ma.py b/dpgen/supercell_make_p3ma.py index e69de29..7a73566 100644 --- a/dpgen/supercell_make_p3ma.py +++ b/dpgen/supercell_make_p3ma.py @@ -0,0 +1,240 @@ +import pymatgen.core as mg +from pymatgen.io.cif import CifParser +from pymatgen.transformations.standard_transformations import SupercellTransformation +import random +import os + + +def create_ordered_structure_from_disordered(disordered_structure): + """ + 手动将包含部分占位的无序结构转换为有序结构,借鉴plus.py的思路。 + """ + s = disordered_structure.copy() + + # 识别需要处理的部分占位 + # 根据 model3.cif, Y2(z≈0.488, occ=0.75), Y3(z≈-0.065, occ=0.25), Li2(z≈0.5, occ=0.5) [model3.cif] + y2_indices, y3_indices, li2_indices = [], [], [] + + for i, site in enumerate(s.sites): + # 使用z坐标来识别特定的部分占位 + z = site.frac_coords[2] + if site.species_string == "Y": + if abs(z - 0.488) < 0.05: + y2_indices.append(i) + elif abs(z - (-0.065)) < 0.05 or abs(z - (1 - 0.065)) < 0.05: + y3_indices.append(i) + elif site.species_string == "Li": + if abs(z - 0.5) < 0.05: + li2_indices.append(i) + + # 根据占位率随机选择要保留的原子 + def choose_keep(indices, keep_fraction): + num_to_keep = int(round(len(indices) * keep_fraction)) + return set(random.sample(indices, num_to_keep)) + + keep_y2 = choose_keep(y2_indices, 0.75) + keep_y3 = choose_keep(y3_indices, 0.25) + keep_li2 = choose_keep(li2_indices, 0.50) + + # 找出所有需要删除的原子索引 + to_remove_indices = [i for i in y2_indices if i not in keep_y2] + to_remove_indices.extend([i for i in y3_indices if i not in keep_y3]) + to_remove_indices.extend([i for i in li2_indices if i not in keep_li2]) + + # 从后往前删除,避免索引错位 + s.remove_sites(sorted(to_remove_indices, reverse=True)) + + # --- 关键修复步骤 --- + # 最终清理,确保所有位点都是有序的 + for i, site in enumerate(s.sites): + if not site.is_ordered: + # 将Composition对象转换为字典,然后找到占位率最高的元素 [plus.py] + species_dict = site.species.as_dict() + main_specie = max(species_dict.items(), key=lambda item: item[1])[0] + s.replace(i, main_specie) + + return s + + +def create_supercells_from_file(cif_path, output_path="."): + """ + 根据给定的CIF文件路径,生成三种不同尺寸和缺陷的超胞,并保存为POSCAR文件。 + """ + if not os.path.exists(cif_path): + print(f"错误: 文件 '{cif_path}' 不存在。") + return + + print(f"正在从 {cif_path} 读取结构...") + parser = CifParser(cif_path) + disordered_structure = parser.parse_structures(primitive=False)[0] + + structure = create_ordered_structure_from_disordered(disordered_structure) + print(f"成功将无序结构转换为一个包含 {len(structure)} 个原子的有序单胞。") + + os.makedirs(output_path, exist_ok=True) + + # 任务一:生成60原子超胞 (无缺陷) + print("\n--- 正在生成 60原子无缺陷超胞 (1x1x2) ---") + tf_60 = SupercellTransformation([[1, 0, 0], [0, 1, 0], [0, 0, 2]]) + sc_60_no_defect = tf_60.apply_transformation(structure) + print(f"原子总数: {len(sc_60_no_defect)}, 化学式: {sc_60_no_defect.composition.reduced_formula}") + sc_60_no_defect.to(fmt="poscar", filename=os.path.join(output_path, "POSCAR_60_no_defect")) + print(f"已保存文件: {os.path.join(output_path, 'POSCAR_60_no_defect')}") + + # 任务二:生成60原子超胞 (含一对反位缺陷) + print("\n--- 正在生成 60原子含一对反位缺陷超胞 ---") + sc_60_defect = sc_60_no_defect.copy() + li_indices = [i for i, site in enumerate(sc_60_defect.sites) if site.species_string == 'Li'] + y_indices = [i for i, site in enumerate(sc_60_defect.sites) if site.species_string == 'Y'] + + if li_indices and y_indices: + li_swap_idx, y_swap_idx = random.choice(li_indices), random.choice(y_indices) + sc_60_defect.replace(li_swap_idx, "Y") + sc_60_defect.replace(y_swap_idx, "Li") + print(f"成功引入一对反位缺陷。浓度: {2 / (len(li_indices) + len(y_indices)) * 100:.2f}%") + sc_60_defect.to(fmt="poscar", filename=os.path.join(output_path, "POSCAR_60_antisite_defect")) + print(f"已保存文件: {os.path.join(output_path, 'POSCAR_60_antisite_defect')}") + + # 任务三:生成90原子超胞 (含一对反位缺陷) + print("\n--- 正在生成 90原子含一对反位缺陷超胞 ---") + tf_90 = SupercellTransformation([[1, 0, 0], [0, 1, 0], [0, 0, 3]]) + sc_90_no_defect = tf_90.apply_transformation(structure) + sc_90_defect = sc_90_no_defect.copy() + li_indices = [i for i, site in enumerate(sc_90_defect.sites) if site.species_string == 'Li'] + y_indices = [i for i, site in enumerate(sc_90_defect.sites) if site.species_string == 'Y'] + + if li_indices and y_indices: + li_swap_idx, y_swap_idx = random.choice(li_indices), random.choice(y_indices) + sc_90_defect.replace(li_swap_idx, "Y") + sc_90_defect.replace(y_swap_idx, "Li") + print(f"原子总数: {len(sc_90_defect)}, 浓度: {2 / (len(li_indices) + len(y_indices)) * 100:.2f}%") + sc_90_defect.to(fmt="poscar", filename=os.path.join(output_path, "POSCAR_90_antisite_defect")) + print(f"已保存文件: {os.path.join(output_path, 'POSCAR_90_antisite_defect')}") + + +def create_ordered_p3ma_structure(disordered_structure): + """ + 手动将P3ma相的无序结构(包含Y2, Y3, Li2的部分占位)转换为有序结构。 + """ + s = disordered_structure.copy() + + # 根据 model3.cif, 识别Y2(z≈0.488, occ=0.75), Y3(z≈-0.065, occ=0.25), Li2(z≈0.5, occ=0.5) [model3.cif] + y2_indices, y3_indices, li2_indices = [], [], [] + + for i, site in enumerate(s.sites): + z = site.frac_coords[2] + if site.species_string == "Y": + if abs(z - 0.488) < 0.05: + y2_indices.append(i) + elif abs(z - (-0.065)) < 0.05 or abs(z - (1 - 0.065)) < 0.05: + y3_indices.append(i) + elif site.species_string == "Li": + if abs(z - 0.5) < 0.05: + li2_indices.append(i) + + # 根据占位率随机选择要保留的原子 + def choose_keep(indices, keep_fraction): + num_to_keep = int(round(len(indices) * keep_fraction)) + return set(random.sample(indices, num_to_keep)) + + keep_y2 = choose_keep(y2_indices, 0.75) + keep_y3 = choose_keep(y3_indices, 0.25) + keep_li2 = choose_keep(li2_indices, 0.50) + + # 找出所有需要删除的原子索引 + to_remove_indices = [i for i in y2_indices if i not in keep_y2] + to_remove_indices.extend([i for i in y3_indices if i not in keep_y3]) + to_remove_indices.extend([i for i in li2_indices if i not in keep_li2]) + + s.remove_sites(sorted(to_remove_indices, reverse=True)) + + # 最终清理,确保所有位点都是有序的 + for i, site in enumerate(s.sites): + if not site.is_ordered: + species_dict = site.species.as_dict() + main_specie = max(species_dict.items(), key=lambda item: item[1])[0] + s.replace(i, main_specie) + + return s + + +def create_multiple_p3ma_supercells(cif_path, num_configs=5, output_path="."): + """ + 读取P3ma相CIF,为不同尺寸的超胞生成多个具有不同反位缺陷位置的构型。 + """ + if not os.path.exists(cif_path): + print(f"错误: 文件 '{cif_path}' 不存在。") + return + + print(f"正在从 {cif_path} 读取P3ma结构...") + parser = CifParser(cif_path) + disordered_structure = parser.parse_structures(primitive=False)[0] + + structure = create_ordered_p3ma_structure(disordered_structure) + print(f"成功将无序P3ma结构转换为一个包含 {len(structure)} 个原子的有序单胞。") + + os.makedirs(output_path, exist_ok=True) + + target_sizes = [60, 90] + for size in target_sizes: + print(f"\n--- 正在为约 {size} 原子的版本生成 {num_configs} 个不同构型 ---") + + # 1. 构建基准超胞 + if size == 60: + tf = SupercellTransformation([[1, 0, 0], [0, 1, 0], [0, 0, 2]]) + filename_suffix = "60_approx" + else: # size == 90 + tf = SupercellTransformation([[1, 0, 0], [0, 1, 0], [0, 0, 3]]) + filename_suffix = "90_approx" + + base_supercell = tf.apply_transformation(structure) + print(f"已生成基准超胞,实际原子数: {len(base_supercell)}") + + li_indices = [i for i, site in enumerate(base_supercell.sites) if site.species_string == 'Li'] + y_indices = [i for i, site in enumerate(base_supercell.sites) if site.species_string == 'Y'] + + if not li_indices or not y_indices: + print("错误:在超胞中未找到足够的Li或Y原子来引入缺陷。") + continue + + # 2. 循环生成多个独特的缺陷构型 + used_pairs = set() + for i in range(num_configs): + defect_supercell = base_supercell.copy() + + # 确保随机选择的交换对是全新的 + # 增加一个尝试次数上限,防止在原子数很少时陷入死循环 + max_tries = len(li_indices) * len(y_indices) + for _ in range(max_tries): + li_swap_idx = random.choice(li_indices) + y_swap_idx = random.choice(y_indices) + pair = tuple(sorted((li_swap_idx, y_swap_idx))) + if pair not in used_pairs: + used_pairs.add(pair) + break + else: + print(f" 警告: 未能找到更多独特的交换对,已停止在第 {i} 个构型。") + break + + # 引入缺陷 + defect_supercell.replace(li_swap_idx, "Y") + defect_supercell.replace(y_swap_idx, "Li") + + print(f" 配置 {i}: 成功引入一对反位缺陷 (Li at index {li_swap_idx} <-> Y at index {y_swap_idx})。") + + # 3. 保存为带编号的POSCAR文件 + poscar_filename = f"POSCAR_P3ma_{filename_suffix}_antisite_defect_{i}" + poscar_path = os.path.join(output_path, poscar_filename) + defect_supercell.to(fmt="poscar", filename=poscar_path) + print(f" 已保存文件: {poscar_path}") + +if __name__ == '__main__': + # --- 使用方法 --- + # 1. 将您的CIF文件保存,例如命名为 "Li3YCl6.cif" + # 2. 将文件名作为参数传递给函数 + cif_file_path = "data/P3ma/model3.cif" # 修改为您的CIF文件名 + output_directory = "raw/P3ma/output" # 可以指定一个输出目录 + + # create_supercells_from_file(cif_file_path, output_directory) + create_multiple_p3ma_supercells(cif_file_path,output_path=output_directory) + print("所有任务完成!") \ No newline at end of file diff --git a/dpgen/supercell_make_pnma.py b/dpgen/supercell_make_pnma.py new file mode 100644 index 0000000..f95e48b --- /dev/null +++ b/dpgen/supercell_make_pnma.py @@ -0,0 +1,115 @@ +import pymatgen.core as mg +from pymatgen.io.cif import CifParser +from pymatgen.transformations.standard_transformations import SupercellTransformation +import random +import os + + +def create_ordered_pnma_structure(disordered_structure): + """ + 手动将Pnma相的无序结构(主要为Li的部分占位)转换为有序结构。 + """ + s = disordered_structure.copy() + + # 根据origin.cif, Li位点的占位率为0.75 [5] + partial_li_indices = [i for i, site in enumerate(s.sites) if "Li" in site.species and not site.is_ordered] + + # 根据0.75的占位率随机选择要保留的Li原子 + num_to_keep = int(round(len(partial_li_indices) * 0.75)) + keep_indices = set(random.sample(partial_li_indices, num_to_keep)) + + # 找出需要删除的原子索引 + to_remove_indices = [i for i in partial_li_indices if i not in keep_indices] + + s.remove_sites(sorted(to_remove_indices, reverse=True)) + + # 重新创建一个新的、完全有序的结构,避免任何副作用 + ordered_species = [] + ordered_coords = [] + for site in s.sites: + # 只取每个位点的主要元素 + main_specie = site.species.elements[0] + ordered_species.append(main_specie) + ordered_coords.append(site.frac_coords) + + final_structure = mg.Structure(s.lattice, ordered_species, ordered_coords) + + return final_structure + + +def create_multiple_pnma_supercells(cif_path, num_configs=3, output_path="."): + """ + 读取Pnma相CIF,为不同尺寸的超胞生成多个具有不同反位缺陷位置的构型。 + """ + if not os.path.exists(cif_path): + print(f"错误: 文件 '{cif_path}' 不存在。") + return + + print(f"正在从 {cif_path} 读取Pnma结构...") + parser = CifParser(cif_path) + disordered_structure = parser.parse_structures(primitive=False)[0] + + structure = create_ordered_pnma_structure(disordered_structure) + print(f"成功将无序Pnma结构转换为一个包含 {len(structure)} 个原子的有序单胞。") + + os.makedirs(output_path, exist_ok=True) + + target_sizes = [60, 90] + for size in target_sizes: + print(f"\n--- 正在为约 {size} 原子的版本生成 {num_configs} 个不同构型 ---") + + # 1. 构建基准超胞 + if size == 60: + tf = SupercellTransformation([[1, 0, 0], [0, 1, 0], [0, 0, 2]]) + filename_suffix = "60_approx" + else: # size == 90 + tf = SupercellTransformation([[1, 0, 0], [0, 1, 0], [0, 0, 3]]) + filename_suffix = "90_approx" + + base_supercell = tf.apply_transformation(structure) + print(f"已生成基准超胞,实际原子数: {len(base_supercell)}") + + li_indices = [i for i, site in enumerate(base_supercell.sites) if site.species_string == 'Li'] + y_indices = [i for i, site in enumerate(base_supercell.sites) if site.species_string == 'Y'] + + if not li_indices or not y_indices: + print("错误:在超胞中未找到足够的Li或Y原子来引入缺陷。") + continue + + # 2. 循环生成多个独特的缺陷构型 + used_pairs = set() + for i in range(num_configs): + defect_supercell = base_supercell.copy() + + # 确保随机选择的交换对是全新的 + while True: + li_swap_idx = random.choice(li_indices) + y_swap_idx = random.choice(y_indices) + # 使用排序后的元组作为键,确保(a,b)和(b,a)被视为相同 + pair = tuple(sorted((li_swap_idx, y_swap_idx))) + if pair not in used_pairs: + used_pairs.add(pair) + break + + # 引入缺陷 + defect_supercell.replace(li_swap_idx, "Y") + defect_supercell.replace(y_swap_idx, "Li") + + print(f" 配置 {i}: 成功引入一对反位缺陷 (Li at index {li_swap_idx} <-> Y at index {y_swap_idx})。") + + # 3. 保存为带编号的POSCAR文件 + poscar_filename = f"POSCAR_Pnma_{filename_suffix}_antisite_defect_{i}" + poscar_path = os.path.join(output_path, poscar_filename) + defect_supercell.to(fmt="poscar", filename=poscar_path) + print(f" 已保存文件: {poscar_path}") + + +if __name__ == '__main__': + # 请将您的Pnma相CIF文件保存,并修改此路径 + # 这里我们使用您提供的参考文件名 'origin.cif' + cif_file_path = "data/Pnma/origin.cif" + output_directory = "raw/Pnma/output" + + create_multiple_pnma_supercells(cif_file_path, num_configs=3, output_path=output_directory) + + print("\nPnma相处理完成!") \ No newline at end of file diff --git a/dpgen/supercell_make_wangshuo.py b/dpgen/supercell_make_wangshuo.py new file mode 100644 index 0000000..18c6a5e --- /dev/null +++ b/dpgen/supercell_make_wangshuo.py @@ -0,0 +1,197 @@ +import random +from collections import defaultdict, Counter + +from pymatgen.core import Structure, Element +from pymatgen.io.lammps.data import LammpsData + +# ASE 兜底(可选) +try: + from ase.io import write as ase_write + from ase import Atoms as ASEAtoms + HAS_ASE = True +except Exception: + HAS_ASE = False + +# ===== 用户参数 ===== +cif_filename = "data/P3ma/origin.cif" # 你的输入 CIF(含部分占位)[5] +supercell_matrix = [[1, 0, 0], [0, 1, 0], [0, 0, 2]] # 2×2×4 超胞(总复制数=16)[3] +out_lammps = "lyc_P3m1_1x1x2_from_cif_ordered.vasp" # 输出 LAMMPS raw +seed = 2025 +strict_count = True # 严格配额:每个父位点在超胞内按占位概率分配整数个原子/空位 +# ==================== + +random.seed(seed) + +def species_to_probs(site): + """ + 将站点的物种占位转换为 [(species or None(vac), prob)] 列表,prob 归一化为和=1。 + 若总占位 < 1,补一个 vacancy(None)。 + 去掉氧化态,仅保留元素。 + """ + sp_items = site.species.items() + total = 0.0 + pairs = [] + for spc, occ in sp_items: + # 转成 Element(剔除氧化态) + try: + e = Element(spc.symbol) if hasattr(spc, "symbol") else Element(str(spc)) + except Exception: + e = Element(str(spc)) + pairs.append((e, float(occ))) + total += float(occ) + if total < 1.0 - 1e-10: + pairs.append((None, 1.0 - total)) # vacancy + total = 1.0 + # 归一化 + if abs(total - 1.0) > 1e-10: + pairs = [(e, p / total) for (e, p) in pairs] + return pairs + +def draw_counts_from_probs(n, probs): + """ + 给定复制数 n 和概率 probs,返回 {species/None: count},使计数和为 n。 + 先按四舍五入,再用残差修正到总和= n。 + """ + # 初分配 + counts = {sp: int(round(p * n)) for sp, p in probs} + s = sum(counts.values()) + if s == n: + return counts + + # 残差排序:需要增加则按概率大的优先加;需要减少则按概率小的优先减 + if n > s: + need = n - s + probs_sorted = sorted(probs, key=lambda x: x[1], reverse=True) + for i in range(need): + sp = probs_sorted[i % len(probs_sorted)][0] + counts[sp] = counts.get(sp, 0) + 1 + else: + need = s - n + probs_sorted = sorted(probs, key=lambda x: x[1]) # 先减概率小的 + idx = 0 + while need > 0 and idx < 50 * len(probs_sorted): + sp = probs_sorted[idx % len(probs_sorted)][0] + if counts.get(sp, 0) > 0: + counts[sp] -= 1 + need -= 1 + idx += 1 + return counts + +def collapse_disorder_to_ordered_supercell(struct, M, strict=True): + """ + 处理步骤: + 1) 给原胞每个位点打 parent_id + 2) 扩胞到 M + 3) 以父位点为组,在组内(复制数 n)按占位概率分配整数个 species/空位到每个复制位点 + - 有序位点:所有复制直接保留 + - 无序位点/部分占位:严格配额或独立抽样 + 4) 返回完全有序的超胞 Structure + """ + s0 = struct.copy() + s0.add_site_property("parent_id", list(range(s0.num_sites))) + + sc = s0.copy() + sc.make_supercell(M) + + # 按父位点分组 + groups = defaultdict(list) + for i, site in enumerate(sc.sites): + pid = sc.site_properties["parent_id"][i] + groups[pid].append(i) + + new_species = [] + new_fracs = [] + new_lat = sc.lattice + + for pid, idx_list in groups.items(): + # 用该组第一个复制的站点定义占位 + site0 = sc[idx_list[0]] + # 有序站点:直接全部保留(只有一种元素,且占位为1) + if site0.is_ordered: + species_elem = list(site0.species.keys())[0] + for i in idx_list: + new_species.append(species_elem) + new_fracs.append(sc[i].frac_coords) + continue + + # 无序/部分占位:概率分配 + probs = species_to_probs(site0) + n = len(idx_list) + + if strict: + counts = draw_counts_from_probs(n, probs) + # 构造分配池并打乱 + pool = [] + for sp, c in counts.items(): + pool += [sp] * c + random.shuffle(pool) + # 分配到每个复制位点 + for i, sp in zip(idx_list, pool): + if sp is None: + continue # vacancy -> 删除该位点 + new_species.append(sp) + new_fracs.append(sc[i].frac_coords) + else: + # 独立抽样 + import bisect + species_list = [sp for sp, p in probs] + cum = [] + ssum = 0.0 + for _, p in probs: + ssum += p + cum.append(ssum) + for i in idx_list: + r = random.random() + j = bisect.bisect_left(cum, r) + sp = species_list[j] + if sp is None: + continue + new_species.append(sp) + new_fracs.append(sc[i].frac_coords) + + ordered_sc = Structure(new_lat, new_species, new_fracs, to_unit_cell=True, coords_are_cartesian=False) + # 去除可能残留的氧化态(LAMMPS atomic 不需要) + try: + ordered_sc.remove_oxidation_states() + except Exception: + pass + return ordered_sc + +# 1) 读取 CIF(含部分占位) +s_in = Structure.from_file(cif_filename, primitive=False) +print(f"读入: {cif_filename}, 原胞位点: {s_in.num_sites}, 有序?: {s_in.is_ordered}") + +# 2) 在 2×2×4 超胞上固化部分占位 -> 完全有序超胞 +ordered_sc = collapse_disorder_to_ordered_supercell(s_in, supercell_matrix, strict=strict_count) +print(f"生成有序超胞: 位点数={ordered_sc.num_sites}, 有序?: {ordered_sc.is_ordered}") + +# 3) 打印元素计数,核对化学计量 +elem_count = Counter([sp.symbol for sp in ordered_sc.species]) +print("元素计数:", dict(elem_count)) + +# 4) 写 LAMMPS raw(pymatgen,失败则 ASE 兜底) +wrote = False +try: + ldata = LammpsData.from_structure(ordered_sc, atom_style="atomic") + ldata.write_file(out_lammps) + wrote = True + print(f"已写出 LAMMPS raw: {out_lammps} (pymatgen)") +except Exception as e: + print("pymatgen 写 LAMMPS raw 失败:", e) + +if not wrote and HAS_ASE: + try: + ase_atoms = ASEAtoms( + symbols=[sp.symbol for sp in ordered_sc.species], + positions=ordered_sc.cart_coords, + cell=ordered_sc.lattice.matrix, + pbc=True + ) + ase_write(out_lammps, ase_atoms, format="lammps-raw", atom_style="atomic") + wrote = True + print(f"已写出 LAMMPS raw: {out_lammps} (ASE)") + except Exception as e: + print("ASE 写 LAMMPS raw 也失败:", e) + +if not wrote: + print("写 LAMMPS raw 失败,请把错误信息发我。") \ No newline at end of file diff --git a/mcp/main.py b/mcp/main.py index b882f00..5dd7f32 100644 --- a/mcp/main.py +++ b/mcp/main.py @@ -11,15 +11,16 @@ from starlette.routing import Mount from system_tools import create_system_mcp from materialproject_mcp import create_materials_mcp from softBV_remake import create_softbv_mcp -from paper_search_mcp import create_paper_search_mcp +# from paper_search_mcp import create_paper_search_mcp from topological_analysis_models import create_topological_analysis_mcp - +from vasp_mcp import create_vasp_mcp # 创建 MCP 实例 system_mcp = create_system_mcp() materials_mcp = create_materials_mcp() softbv_mcp = create_softbv_mcp() -paper_search_mcp = create_paper_search_mcp() +# paper_search_mcp = create_paper_search_mcp() topological_analysis_mcp = create_topological_analysis_mcp() +vasp_mcp = create_vasp_mcp() # 在 Starlette 的 lifespan 中启动 MCP 的 session manager @contextlib.asynccontextmanager async def lifespan(app: Starlette): @@ -27,8 +28,9 @@ async def lifespan(app: Starlette): await stack.enter_async_context(system_mcp.session_manager.run()) await stack.enter_async_context(materials_mcp.session_manager.run()) await stack.enter_async_context(softbv_mcp.session_manager.run()) - await stack.enter_async_context(paper_search_mcp.session_manager.run()) + # await stack.enter_async_context(paper_search_mcp.session_manager.run()) await stack.enter_async_context(topological_analysis_mcp.session_manager.run()) + await stack.enter_async_context(vasp_mcp.session_manager.run()) yield # 服务器运行期间 # 退出时自动清理 @@ -39,8 +41,9 @@ app = Starlette( Mount("/system", app=system_mcp.streamable_http_app()), Mount("/materials", app=materials_mcp.streamable_http_app()), Mount("/softBV", app=softbv_mcp.streamable_http_app()), - Mount("/papersearch",app=paper_search_mcp.streamable_http_app()), + # Mount("/papersearch",app=paper_search_mcp.streamable_http_app()), Mount("/topologicalAnalysis",app=topological_analysis_mcp.streamable_http_app()), + Mount("/vasp",app=vasp_mcp.streamable_http_app()), ], ) @@ -52,4 +55,5 @@ app = Starlette( # http://localhost:8000/softBV # http://localhost:8000/papersearch # http://localhost:8000/topologicalAnalysis +# http://localhost:8000/vasp # 如果需要浏览器客户端访问(CORS 暴露 Mcp-Session-Id),请参考 README 中的 CORS 配置示例 [1] \ No newline at end of file diff --git a/mcp/softBV_remake.py b/mcp/softBV_remake.py index ba2533d..b5d7c0e 100644 --- a/mcp/softBV_remake.py +++ b/mcp/softBV_remake.py @@ -560,7 +560,7 @@ def _parse_print_cube_output(raw_text: str) -> PrintCubeResult: matrix.append(parts) return matrix - # Find key lines and parse data + # Find key lines and parse raw name = re.search(r"CELL: name: (.*)", raw_text).group(1).strip() total_atoms = int(re.search(r"CELL: total atom: (\d+)", raw_text).group(1)) diff --git a/mcp/vasp_mcp.py b/mcp/vasp_mcp.py new file mode 100644 index 0000000..1f6ab77 --- /dev/null +++ b/mcp/vasp_mcp.py @@ -0,0 +1,362 @@ +# vasp_mcp.py +import os +import posixpath +import warnings +from dataclasses import dataclass +from typing import Any, AsyncIterator +from contextlib import asynccontextmanager + +import asyncssh +from pymatgen.core import Structure +from io import StringIO + +from mcp.server.fastmcp import FastMCP, Context +from mcp.server.session import ServerSession +from pymatgen.io.cif import CifParser + +# --- VASP 特定配置 --- +REMOTE_HOST = os.getenv("REMOTE_HOST", '202.121.182.208') +REMOTE_USER = os.getenv("REMOTE_USER", 'koko125') +PRIVATE_KEY_PATH = os.getenv("PRIVATE_KEY_PATH", 'D:/tool/tool/id_rsa.txt') + +# VASP 赝势库和沙箱路径 +POTCAR_BASE_PATH = "/cluster/home/koko125/tool/potcar_mcp" +DEFAULT_SANDBOX = f"/cluster/home/{REMOTE_USER}/sandbox" +VASP_ENV_SCRIPT = "/cluster/home/koko125/intel/oneapi/setvars.sh" +VASP_MPI_RUN_CMD = "mpirun -np 4 /cluster/home/koko125/vasp/bin_cpu/vasp_std" +def shell_quote(arg: str) -> str: + """安全地引用 shell 参数""" + return "'" + str(arg).replace("'", "'\"'\"'") + "'" + + +# --- 定义共享上下文 --- +@dataclass +class VaspContext: + ssh_connection: asyncssh.SSHClientConnection + potcar_base: str + sandbox_path: str + env_script: str + mpi_run_cmd: str # <-- 添加这一行 + +# --- 定义生命周期管理器 --- +@asynccontextmanager +async def vasp_lifespan(_server: FastMCP) -> AsyncIterator[VaspContext]: + """建立 SSH 连接并注入 VASP 上下文。""" + conn: asyncssh.SSHClientConnection | None = None + try: + conn = await asyncssh.connect( + REMOTE_HOST, username=REMOTE_USER, client_keys=[PRIVATE_KEY_PATH], known_hosts=None + ) + yield VaspContext( + ssh_connection=conn, + potcar_base=POTCAR_BASE_PATH, + sandbox_path=DEFAULT_SANDBOX, + env_script=VASP_ENV_SCRIPT, + mpi_run_cmd=VASP_MPI_RUN_CMD + ) + finally: + if conn: + conn.close() + await conn.wait_closed() + + +# --- VASP MCP 工厂函数 --- +def create_vasp_mcp() -> FastMCP: + """创建包含 VASP 辅助工具的 MCP 实例。""" + mcp = FastMCP( + name="VASP POTCAR Tools", + instructions="用于查询和准备 VASP POTCAR 文件的专用工具集。", + lifespan=vasp_lifespan, + streamable_http_path="/", + ) + + # 沿用 system_tools.py 中的安全路径拼接函数,确保目标路径安全 + def _safe_join_sandbox(sandbox_root: str, relative_path: str) -> str: + rel = (relative_path or ".").strip().lstrip("/") + combined = posixpath.normpath(posixpath.join(sandbox_root, rel)) + root_norm = sandbox_root.rstrip("/") + if combined != root_norm and not combined.startswith(root_norm + "/"): + raise ValueError("路径越界:目标路径必须在沙箱目录内") + if ".." in combined.split("/"): + raise ValueError("非法路径:不允许使用 '..'") + return combined + + # --- 工具 1: 列出可用的赝势库类型 --- + @mcp.tool() + async def list_potcar_types(ctx: Context[ServerSession, VaspContext]) -> list[str]: + """ + 列出中央赝势库中所有可用的赝势类型 (例如 'PBE_potpaw', 'PAW_GGA_PBE')。 + """ + app_ctx = ctx.request_context.lifespan_context + conn = app_ctx.ssh_connection + try: + # 使用 ls -F, 目录会带上 '/' 后缀 + result = await conn.run(f"ls -F {shell_quote(app_ctx.potcar_base)}", check=True) + potcar_types = [ + name.strip('/') for name in result.stdout.strip().split() if name.endswith('/') + ] + await ctx.info(f"发现可用赝势库: {potcar_types}") + return potcar_types + except Exception as e: + await ctx.error(f"列出 POTCAR 类型失败: {e}") + return [] + + # --- 工具 2 (新): 查询指定赝势库中的可用元素 --- + @mcp.tool() + async def query_potcar_elements(ctx: Context[ServerSession, VaspContext], potcar_type: str) -> list[str]: + """ + 查询指定类型的赝势库中包含哪些元素的赝势。 + """ + app_ctx = ctx.request_context.lifespan_context + conn = app_ctx.ssh_connection + + # 安全地构建源目录路径 + source_dir = posixpath.join(app_ctx.potcar_base, potcar_type) + if ".." in potcar_type or "/" in potcar_type: + await ctx.error(f"非法的赝势库类型: {potcar_type}") + return [] + + await ctx.info(f"查询目录 '{source_dir}' 中的可用元素...") + try: + result = await conn.run(f"ls -F {shell_quote(source_dir)}", check=True) + elements = [ + name.strip('/') for name in result.stdout.strip().split() if name.endswith('/') + ] + return elements + except asyncssh.ProcessError as e: + msg = f"查询元素失败: 赝势库 '{potcar_type}' 可能不存在。Stderr: {e.stderr}" + await ctx.error(msg) + return [] + except Exception as e: + await ctx.error(f"查询 POTCAR 元素时发生未知错误: {e}") + return [] + + # --- 工具 3 (新): 从中央库安全地复制 POTCAR 文件到沙箱 --- + @mcp.tool() + async def copy_potcar_file( + ctx: Context[ServerSession, VaspContext], + potcar_type: str, + element: str, + destination_path: str + ) -> dict[str, str]: + """ + 从中央赝势库安全地复制一个指定元素的 POTCAR 文件到用户沙箱中的目标路径。 + 例如, 将 'PBE_potpaw' 库中的 'Si' 赝势复制到 'sio2_relax/POTCAR_Si'。 + """ + app_ctx = ctx.request_context.lifespan_context + conn = app_ctx.ssh_connection + + try: + # 1. 安全地构建源文件路径 (只允许访问 potcar_base 下的子目录) + if ".." in potcar_type or "/" in potcar_type or ".." in element or "/" in element: + raise ValueError("非法的赝势库类型或元素名称。") + source_file = posixpath.join(app_ctx.potcar_base, potcar_type, element, "POTCAR") + + # 2. 安全地构建目标文件路径 (必须在沙箱内) + dest_file_abs = _safe_join_sandbox(app_ctx.sandbox_path, destination_path) + + # 3. 执行 cp 命令 + cmd = f"cp {shell_quote(source_file)} {shell_quote(dest_file_abs)}" + await ctx.info(f"执行安全复制: cp {source_file} -> {dest_file_abs}") + await conn.run(cmd, check=True) + + return {"status": "success", "source": source_file, "destination": destination_path} + + except asyncssh.ProcessError as e: + msg = f"复制 POTCAR 失败。请检查 potcar_type 和 element 是否正确。Stderr: {e.stderr}" + await ctx.error(msg) + return {"status": "error", "message": msg} + except ValueError as e: + await ctx.error(f"路径验证失败: {e}") + return {"status": "error", "message": str(e)} + except Exception as e: + await ctx.error(f"复制 POTCAR 时发生未知错误: {e}") + return {"status": "error", "message": str(e)} + + # (可选) 我们仍然可以保留 cif_to_poscar 工具,因为它对于确定元素顺序非常有用 + @mcp.tool() + def cif_to_poscar(cif_content: str, sort_structure: bool = True) -> dict[str, str]: + """将 CIF 文件内容转换为 VASP 的 POSCAR 文件内容,并提供有序的元素列表。""" + # ... (此工具代码与之前版本相同) + try: + structure = Structure.from_str(cif_content, fmt="cif") + if sort_structure: + structure = structure.get_sorted_structure() + + elements = [site.specie.symbol for site in structure] + unique_elements = sorted(set(elements), key=elements.index) + + poscar_string_io = StringIO() + structure.to(fmt="poscar", file_obj=poscar_string_io) + poscar_content = poscar_string_io.getvalue() + + return { + "status": "success", + "poscar_content": poscar_content, + "elements": " ".join(unique_elements) + } + except Exception as e: + return {"status": "error", "message": f"CIF 转换失败: {e}"} + + @mcp.tool() + def cif_to_poscar(cif_content: str,ctx: Context[ServerSession, VaspContext], sort_structure: bool = True) -> dict[str, str]: + """ + 将 CIF 文件内容稳健地转换为 VASP 的 POSCAR 文件内容。 + 该工具会尝试多种解析策略来处理格式不规范的CIF文件。 + 如果成功,返回 POSCAR 内容和用于生成 POTCAR 的有序元素列表。 + 如果失败,返回详细的错误信息。 + + Args: + cif_content (str): 包含晶体结构的 CIF 文件完整内容。 + sort_structure (bool): 是否对原子进行排序以匹配 Pymatgen 的 POTCAR 约定。默认为 True。 + """ + + structure = None + last_exception = None + + # 忽略 Pymatgen 可能产生的警告,避免污染输出 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + # --- 策略 1: 标准解析 --- + try: + ctx.debug("尝试策略 1: 标准 CIF 解析...") + structure = Structure.from_str(cif_content, fmt="cif") + ctx.info("策略 1 成功: 标准解析完成。") + except Exception as e: + ctx.warning(f"策略 1 失败: {e}") + last_exception = e + + # --- 策略 2: 宽松解析 (忽略化合价检查) --- + if structure is None: + try: + ctx.debug("尝试策略 2: 宽松解析 (不检查化合价)...") + # 使用底层的 CifParser 并禁用化合价检查 + parser = CifParser.from_string(cif_content, check_valence=False) + structure = parser.get_structures(primitive=True)[0] + ctx.info("策略 2 成功: 宽松解析完成。") + except Exception as e: + ctx.warning(f"策略 2 失败: {e}") + last_exception = e + + # --- 策略 3: 使用原始坐标,不进行对称性处理 --- + if structure is None: + try: + ctx.debug("尝试策略 3: 使用原始坐标 (primitive=False)...") + parser = CifParser.from_string(cif_content) + # 获取文件中的原始结构,而不是计算出的原胞 + structure = parser.get_structures(primitive=False)[0] + ctx.info("策略 3 成功: 已使用原始坐标。") + except Exception as e: + ctx.warning(f"策略 3 失败: {e}") + last_exception = e + + # --- 如果所有策略都失败 --- + if structure is None: + error_message = ( + "无法从提供的 CIF 内容中解析出晶体结构。所有解析策略均已失败。\n" + f"最后遇到的错误是: {last_exception}\n" + "建议: 请检查 CIF 文件格式是否严重损坏。AI 可以尝试重新生成 CIF,或直接请求用户提供 POSCAR 文件内容。" + ) + ctx.error(error_message) + return {"status": "error", "message": error_message} + + # --- 成功后处理 --- + try: + # 排序结构以确保元素顺序与 pymatgen 的 POTCAR 生成逻辑一致 + if sort_structure: + structure = structure.get_sorted_structure() + + # 从结构中获取有序的元素列表 + elements = [site.specie.symbol for site in structure.composition.elements] + + # 生成 POSCAR 内容 + poscar_string_io = StringIO() + # 使用 vasp5 格式,确保元素行存在 + structure.to(fmt="poscar", file_obj=poscar_string_io) + poscar_content = poscar_string_io.getvalue() + + return { + "status": "success", + "poscar_content": poscar_content, + "elements": " ".join(elements) # 以空格分隔的字符串形式提供有序元素 + } + except Exception as e: + # 这种情况很少见,但可能在结构后处理时发生 + final_error = f"结构解析成功,但在生成POSCAR时出错: {e}" + ctx.error(final_error) + return {"status": "error", "message": final_error} + + @mcp.tool() + async def test_vasp_run( + ctx: Context[ServerSession, VaspContext], + job_directory: str + ) -> dict[str, Any]: + """ + 在指定目录中启动 VASP 的“精简模式”(--lite)以进行快速测试。 + 此模式会完整解析所有输入文件(INCAR, POSCAR等)并检查参数, + 但不会开始实际的离子或电子步计算,通常在数秒内完成。 + + Args: + job_directory (str): 包含所有 VASP 输入文件的远程沙箱子目录。 + """ + app_ctx = ctx.request_context.lifespan_context + conn = app_ctx.ssh_connection + + try: + # 安全地构建工作目录的绝对路径 + workdir_abs = _safe_join_sandbox(app_ctx.sandbox_path, job_directory) + + await ctx.info(f"在 '{workdir_abs}' 中开始 VASP 输入文件验证 (精简模式)...") + + # 关键:在 VASP 执行命令后附加 --lite 标志 + # 注意: mpirun [options] [args] + # 我们需要将 --lite 附加到 vasp_std 后面 + # 假设 app_ctx.mpi_run_cmd 是 "mpirun -np 4 .../vasp_std" + command_with_lite = f"{app_ctx.mpi_run_cmd} --lite" + + # 构建完整的 shell 命令,以激活环境并执行 + # 这里的 f-string 已被修正,以避免多行和嵌套问题 + inner_command = ( + f"cd {shell_quote(workdir_abs)}; " + f"source {shell_quote(app_ctx.env_script)}; " + f"{command_with_lite}" + ) + full_shell_command = f"bash -lc {shell_quote(inner_command)}" + + # 使用简单的 conn.run 等待命令完成。check=False 因为我们想自己处理非零退出 + proc = await conn.run(full_shell_command, check=False) + + # 分析结果 + stdout = proc.stdout or "" + stderr = proc.stderr or "" + + # VASP 成功完成初始化并正常退出的标志通常是这条信息 + success_indicator = "General timing and accounting informations for this job" + + if proc.exit_status == 0 and success_indicator in stdout: + test_passed = True + conclusion = "测试成功:VASP 输入文件有效,所有参数均被正确解析。" + await ctx.info(conclusion) + else: + test_passed = False + conclusion = "测试失败:VASP 报告了错误或未能正常完成初始化。请检查下面的 stderr 输出。" + await ctx.warning(conclusion) + + return { + "status": "completed", + "test_passed": test_passed, + "conclusion": conclusion, + "exit_status": proc.exit_status, + "stdout_preview": "\n".join(stdout.splitlines()[-20:]), # 只看最后20行,避免刷屏 + "stderr": stderr + } + + except Exception as e: + msg = f"执行 test_vasp_run 工具时发生意外错误: {e}" + await ctx.error(msg) + return {"status": "error", "message": str(e)} + + # 确保这个 _safe_join_sandbox 辅助函数存在于 create_vasp_mcp 函数内部或可被访问 + + return mcp \ No newline at end of file diff --git a/rss/nature_filter_rss.py b/rss/nature_filter_rss.py new file mode 100644 index 0000000..3956578 --- /dev/null +++ b/rss/nature_filter_rss.py @@ -0,0 +1,122 @@ +import Bfeedparser +import requests +from feedgen.feed import FeedGenerator +from datetime import datetime, timezone +import time + +# --- 1. 配置区 --- + +# 你的关键词列表,不区分大小写 +KEYWORDS = ['solid-state battery', 'lithium metal', 'anode-free', 'electrolyte'] + +# 你想监控的 Nature 系列期刊的 RSS 源 +SOURCE_FEEDS = { + 'Nature': 'https://www.nature.com/nature/rss/current', + 'Nat Commun': 'https://www.nature.com/ncomms/rss/current', + 'Nat Energy': 'https://www.nature.com/nenergy/rss/current', + 'Nat Mater': 'https://www.nature.com/nmat/rss/current', + 'Nat Nanotechnol': 'https://www.nature.com/nnano/rss/current', + 'Nat Sustain': 'https://www.nature.com/natsustain/rss/current', + 'Nat Chem': 'https://www.nature.com/nchem/rss/current', + 'Nat Synth': 'https://www.nature.com/natsynth/rss/current', + 'Nat Catal': 'https://www.nature.com/natcatal/rss/current', + 'Nat Rev Mater': 'https://www.nature.com/natrevmat/rss/current', + 'Nat Rev Chem': 'https://www.nature.com/natrevchem/rss/current', + 'Nat Rev Earth Environ': 'https://www.nature.com/natrevearthenviron/rss/current', +} + +# 输出的 RSS 文件路径,确保 ttrss 能通过 web 服务器访问到它 +OUTPUT_FILE = '/var/www/html/rss/nature_filtered_feed.xml' + + +# --- 2. 脚本核心逻辑 --- +N +def fetch_and_filter(): + """获取所有源,过滤文章,返回一个匹配文章的列表""" + + print(f"Starting feed fetch at {datetime.now()}") + + matched_articles = [] + # 使用集合来存储已添加文章的链接,防止重复 + seen_links = set() + + headers = { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } + + for name, url in SOURCE_FEEDS.items(): + print(f" -> Fetching from {name}...") + try: + # 使用 requests 获取内容,可以更好地处理网络问题和伪装 User-Agent + response = requests.get(url, headers=headers, timeout=15) + response.raise_for_status() # 确保请求成功 + + # 使用 feedparser 解析获取到的内容 + feed = feedparser.parse(response.content) + + for entry in feed.entries: + # 检查文章链接是否已处理过 + if entry.link in seen_links: + continue + + # 将标题和摘要拼接在一起,方便搜索 + content_to_check = (entry.title + ' ' + entry.get('summary', '')).lower() + + # 检查是否有任何一个关键词出现在内容中 + if any(keyword.lower() in content_to_check for keyword in KEYWORDS): + print(f" [MATCH FOUND] in {name}: {entry.title}") + + # 为了在 RSS 阅读器中更好地展示,我们在标题前加上来源期刊 + entry.title = f"[{name}] {entry.title}" + matched_articles.append(entry) + seen_links.add(entry.link) + + # 友好请求,避免过于频繁 + time.sleep(1) + + except requests.RequestException as e: + print(f" [ERROR] Could not fetch {name}: {e}") + except Exception as e: + print(f" [ERROR] An unexpected error occurred for {name}: {e}") + + print(f"\nFound {len(matched_articles)} matching articles in total.") + return matched_articles + + +def generate_filtered_feed(articles): + """根据过滤后的文章列表生成新的 RSS 文件""" + + fg = FeedGenerator() + fg.title('My Filtered Nature Research Feed') + fg.link(href='https://www.nature.com', rel='alternate') + fg.description(f"Custom RSS feed for Nature journals, filtered by keywords: {', '.join(KEYWORDS)}") + + # 按发布日期对文章进行排序(从新到旧) + articles.sort(key=lambda x: x.get('published_parsed') or x.get('updated_parsed'), reverse=True) + + for entry in articles: + fe = fg.add_entry() + fe.id(entry.link) # 使用文章链接作为唯一ID + fe.title(entry.title) + fe.link(href=entry.link) + # feedparser 已经帮我们解析好了摘要 + fe.description(entry.get('summary', 'No summary available.')) + + # 处理发布日期 + pub_date = entry.get('published_parsed') + if pub_date: + # 转换为带时区的 datetime 对象 + fe.published(datetime.fromtimestamp(time.mktime(pub_date)).replace(tzinfo=timezone.utc)) + + # 写入文件 + fg.rss_file(OUTPUT_FILE, pretty=True) + print(f"Successfully generated new RSS feed at {OUTPUT_FILE}") + + +# --- 3. 主程序入口 --- +if __name__ == "__main__": + filtered_articles = fetch_and_filter() + if filtered_articles: + generate_filtered_feed(filtered_articles) + else: + print("No new matching articles found. RSS file not updated.")