diff --git a/config/param.yaml b/config/param.yaml index d89c207..f132216 100644 --- a/config/param.yaml +++ b/config/param.yaml @@ -1,13 +1,13 @@ # param.yaml -project: "LGPS" +project: "LYC_model1" # 1. 初始文件定义 (对应 data/ 目录) files: - poscar: "POSCAR" + poscar: "model1.vasp" potcar: "POTCAR" initial_pot: "nep89.txt" # 第一轮 MD 用的势函数 - label: "Li Ge P S" + label: "Li Y Cl" # 2. 迭代流程控制 iterations: @@ -37,9 +37,33 @@ iterations: # 逻辑:cp template/03.train/nep.in - name: "03.train" executor: "nep_local" - - # --- 第二轮 --- - id: 1 + steps: + # Step 1: MD (预热 + 采样) + # 逻辑:会把 nep.txt (来自 initial_pot) 和 model.xyz 准备好 + - name: "00.md" + sub_tasks: + # 你提到可能有预热,也可能有加工,这里支持串行执行 + - template_sub: "preheat" # 使用 template/00.md/preheat/run.in + - template_sub: "production" # 使用 template/00.md/production/run.in + executor: "gpumd" # 对应 machine.yaml + + # Step 2: 筛选 + - name: "01.select" + method: "distance" + params: [90, 120] + + # Step 3: SCF (VASP) + # 逻辑:cp template/02.scf/INCAR; check KPOINTS; cp data/POTCAR + - name: "02.scf" + executor: "vasp_std" # 对应 machine.yaml (可能调用 vasp_std.sh) + + # Step 4: 训练 + # 逻辑:cp template/03.train/nep.in + - name: "03.train" + executor: "nep_local" + # --- 第二轮 --- + - id: 2 steps: - name: "00.md" sub_tasks: @@ -57,11 +81,20 @@ iterations: - name: "03.train" executor: "nep_local" - name: "04.predict" - # 定义温度和时间列表 + # [新增] 自定义模型文件 (位于 data/ 目录下),不填则使用当前训练结果 +# custom_nep: "nep_final_best.txt" + + # [新增] 自定义预测结构 (位于 data/ 目录下),不填则使用 00.md 的结果 + # 注意:这里填写 .vasp 文件,程序会自动转化为 model.xyz + custom_poscar: "model1_supercell.vasp" + conditions: - - {T: 500, time: "1ns"} - - {T: 600, time: "1ns"} # 支持不同温度不同时长 - - {T: 700, time: "1ns"} - - {T: 800, time: "1ns"} - - {T: 900, time: "1ns"} # 支持不同温度不同时长 - - {T: 1000, time: "1ns"} \ No newline at end of file + - {T: 375, time: "15ns"} + - { T: 400, time: "5ns" } + - { T: 425, time: "2ns" } + - { T: 450, time: "1ns" } + - { T: 500, time: "1ns" } + - { T: 600, time: "1ns" } + - { T: 700, time: "1ns" } + - { T: 800, time: "1ns" } + - { T: 900, time: "1ns" } \ No newline at end of file diff --git a/src/utils.py b/src/utils.py index c5e1a5d..497acd0 100644 --- a/src/utils.py +++ b/src/utils.py @@ -36,16 +36,43 @@ def setup_logger(work_dir, log_file="autonep.log"): return logger +# src/utils.py 中的 Notifier 类 + class Notifier: - """(预留) 通知模块""" + def __init__(self, project_name="AutoNEP"): + self.project_name = project_name - def __init__(self, url=None): - self.url = url + def send(self, title, message, priority=3): + """ + 调用本地 'post' 命令发送通知 + Args: + title: 标题 + message: 内容 + priority: 优先级 (0-3: Low/Log, 4-7: Med/Done, 8+: High/Err) + """ + import subprocess - def send(self, title, msg, priority=5): - # 暂时只打印日志,不实际发送 - logging.info(f"[[Notification]] {title}: {msg}") + # 自动加上项目前缀 + full_title = f"[{self.project_name}] {title}" + # 构造命令: post -a task -t "Title" -m "Msg" -p Priority + cmd = [ + "post", + "-a", "task", # 固定推送到 task 频道 + "-t", full_title, + "-m", message, + "-p", str(priority) + ] + + try: + # 执行命令,不输出到屏幕,防止干扰主程序日志 + subprocess.run(cmd, check=False, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + # 同时在本地日志留档 + logging.info(f"[[Notification Sent]] P{priority} | {title}: {message}") + except FileNotFoundError: + logging.warning("Command 'post' not found. Notification skipped.") + except Exception as e: + logging.warning(f"Failed to send notification: {e}") # src/utils.py 添加在最后 diff --git a/src/workflow.py b/src/workflow.py index c89f561..37db84b 100644 --- a/src/workflow.py +++ b/src/workflow.py @@ -25,14 +25,16 @@ class Workflow: # 初始化状态追踪 os.makedirs(self.workspace, exist_ok=True) self.tracker = StateTracker(self.workspace) - + project_name = self.param.get('project', 'AutoNEP') + self.notifier = Notifier(project_name) + self.notifier.send("Workflow Init", "AutoNEP framework initialized.", 1) # 初始变量 self.current_nep_pot = os.path.join(self.data_dir, self.param['files']['initial_pot']) self.current_train_set = os.path.join(self.workspace, "accumulated_train.xyz") def run(self): self.logger.info(f"Workflow Started: {self.param['project']}") - + self.notifier.send("Workflow Start", f"Starting project: {self.param['project']}", 5) for iteration in self.param['iterations']: iter_id = iteration['id'] iter_name = f"iter_{iter_id:02d}" @@ -73,6 +75,7 @@ class Workflow: cmd = f"{kit_path} -addlabel {std_poscar_name} {atom_labels}" if run_cmd_with_log(cmd, step_dir, "init.log"): + self.notifier.send(f"Step Done: {step_name}", f"project {self.param['project']} Finished task {task_id_init}", 2) self.tracker.mark_done(task_id_init) else: self.logger.error("Custom POSCAR initialization failed.") @@ -93,6 +96,7 @@ class Workflow: cmd = f"{kit_path} -addlabel {poscar_name} {atom_labels}" if run_cmd_with_log(cmd, step_dir, "init.log"): + self.notifier.send(f"Step Done: {step_name}", f"project {self.param['project']} Finished task {task_id_init}", 2) self.tracker.mark_done(task_id_init) else: self.logger.error("Initialization failed.") @@ -111,11 +115,13 @@ class Workflow: if os.path.exists(prev_model_src): self.logger.info(f"Copying model.xyz from {prev_iter_name}...") shutil.copy(prev_model_src, os.path.join(step_dir, "model.xyz")) + self.notifier.send(f"Step Done: {step_name}", f"project {self.param['project']} Finished task {task_id_init}", 2) self.tracker.mark_done(task_id_init) # 标记完成 else: self.logger.error(f"Previous model.xyz not found: {prev_model_src}") return else: + self.notifier.send(f"Step Done: {step_name}", f"project {self.param['project']} Finished task {task_id_init}", 2) self.tracker.mark_done(task_id_init) # 确保 gpumdkit 路径可用 kit_path = self.machine.config['paths'].get('gpumdkit', 'gpumdkit.sh') @@ -147,6 +153,7 @@ class Workflow: if run_cmd_with_log(kit_path, preheat_dir, "step_exec.log", input_str=input_str_201): if os.path.exists(os.path.join(preheat_dir, "sampled_structures.xyz")): + self.notifier.send(f"Step Done: {step_name}", f"project {self.param['project']} Finished task {task_id_preheat}", 2) self.tracker.mark_done(task_id_preheat) else: self.logger.error("sampled_structures.xyz not generated.") @@ -216,6 +223,7 @@ class Workflow: run_cmd_with_log("cat sample_*/dump.xyz > dump.xyz", prod_dir, "step_exec.log") self.last_dump_path = os.path.join(prod_dir, "dump.xyz") + self.notifier.send(f"Step Done: {step_name}", f"project {self.param['project']} Finished task {task_id_prod}", 2) self.tracker.mark_done(task_id_prod) else: self.logger.info("Skipping Production (Already Done).") @@ -395,7 +403,7 @@ class Workflow: src_png = os.path.join(step_dir, "select.png") if os.path.exists(src_png): shutil.copy(src_png, os.path.join(output_dir, "select.png")) - + self.notifier.send(f"Step Done: {step_name}", f"project {self.param['project']} Finished task {task_id_select}", 2) self.tracker.mark_done(task_id_select) # === 分支 B: 随机筛选 (保持不变) === @@ -409,6 +417,7 @@ class Workflow: f.write(f"Random,{min_n},{max_n},Executed\n") src_png = os.path.join(step_dir, "select.png") if os.path.exists(src_png): shutil.copy(src_png, os.path.join(output_dir, "select.png")) + self.notifier.send(f"Step Done: {step_name}", f"project {self.param['project']} Finished task {task_id_select}", 2) self.tracker.mark_done(task_id_select) else: self.logger.error("Random selection failed.") @@ -562,6 +571,7 @@ class Workflow: self.logger.info(f"VASP data collected: {res_file}") # 保存这个路径供 Train 使用 self.new_data_chunk = res_file + self.notifier.send(f"Step Done: {step_name}", f"project {self.param['project']} Finished task {task_id_scf}", 2) self.tracker.mark_done(task_id_scf) else: self.logger.error("NEP-dataset.xyz not found after collection.") @@ -683,7 +693,7 @@ class Workflow: dst_png = os.path.join(output_dir, file) shutil.copy(src_png, dst_png) self.logger.info(f"Archived plot: {file}") - + self.notifier.send(f"Step Done: {step_name}", f"project {self.param['project']} Finished task {task_id_train}", 2) self.tracker.mark_done(task_id_train) else: @@ -706,48 +716,108 @@ class Workflow: self.logger.info("=== Step: 04.predict (Arrhenius) ===") os.makedirs(step_dir, exist_ok=True) - # 准备输出目录 output_dir = os.path.join(iter_path, "05.output") os.makedirs(output_dir, exist_ok=True) - # 1. 准备基础文件 (model.xyz, nep.txt) - # 注意:预测通常使用最新的 nep.txt 和 原始/最新 model.xyz - # 这里使用 current_nep_pot 和 初始 model.xyz (保证一致性) - model_src = os.path.join(iter_path, "00.md", "model.xyz") # 假设 00.md 里有 - if not os.path.exists(model_src): - # 如果没有,尝试从 data 目录拿 - model_src = os.path.join(self.data_dir, self.param['files']['poscar']) - # 这里简化处理,最好确保 00.md 产生了 model.xyz + # ------------------------------------------------- + # 1. 准备 NEP 势函数 (支持自定义) + # ------------------------------------------------- + custom_nep_name = step_conf.get('custom_nep') + nep_src = "" - # 2. 遍历温度点执行模拟 + if custom_nep_name: + # 优先使用 data/ 下的自定义模型 + nep_src = os.path.join(self.data_dir, custom_nep_name) + if not os.path.exists(nep_src): + self.logger.error(f"Custom NEP file not found: {nep_src}") + return + self.logger.info(f"Using Custom NEP model: {custom_nep_name}") + else: + # 默认使用当前流程的训练结果 + nep_src = self.current_nep_pot + self.logger.info("Using current training result for prediction.") + + if not os.path.exists(nep_src): + self.logger.error(f"NEP potential source missing: {nep_src}") + return + + # ------------------------------------------------- + # 2. 准备 model.xyz (支持自定义 VASP -> 转化) + # ------------------------------------------------- + custom_poscar_name = step_conf.get('custom_poscar') + final_model_xyz = os.path.join(step_dir, "model.xyz") # 统一存放在 step_dir 根目录备用 + + kit_path = self.machine.config['paths'].get('gpumdkit', 'gpumdkit.sh') + + if custom_poscar_name: + # === Case A: 使用自定义 VASP 结构 === + self.logger.info(f"Using Custom POSCAR for prediction: {custom_poscar_name}") + poscar_src = os.path.join(self.data_dir, custom_poscar_name) + + if os.path.exists(poscar_src): + # 复制 VASP 文件到当前目录 + shutil.copy(poscar_src, os.path.join(step_dir, custom_poscar_name)) + + # 获取 label 并执行转化 + atom_labels = self.param['files'].get('label', '') + if not atom_labels: + self.logger.error("Labels missing in config (files.label), cannot convert POSCAR.") + return + + cmd = f"{kit_path} -addlabel {custom_poscar_name} {atom_labels}" + self.logger.info(f"Converting POSCAR to model.xyz: {cmd}") + + if not run_cmd_with_log(cmd, step_dir, "convert_model.log"): + self.logger.error("Failed to convert custom POSCAR.") + return + + if not os.path.exists(final_model_xyz): + self.logger.error("model.xyz not generated after conversion.") + return + else: + self.logger.error(f"Custom POSCAR not found: {poscar_src}") + return + else: + # === Case B: 使用流程默认结构 (00.md) === + self.logger.info("Using default structure from 00.md") + default_src = os.path.join(iter_path, "00.md", "model.xyz") + + if os.path.exists(default_src): + shutil.copy(default_src, final_model_xyz) + else: + # 尝试兜底:用 data 里的初始 POSCAR + self.logger.warning("00.md/model.xyz not found. Trying initial POSCAR...") + # 这里省略复杂的再次转化逻辑,建议保证 00.md 跑通 + self.logger.error(f"Default model.xyz source missing: {default_src}") + return + + # ------------------------------------------------- + # 3. 遍历温度点执行模拟 + # ------------------------------------------------- conditions = step_conf.get('conditions', []) if not conditions: self.logger.error("No conditions defined for 04.predict") continue - # 发送开始通知 - self.notifier.send("Predict Start", f"Starting {len(conditions)} tasks in {iter_name}", 5) + self.notifier.send("Predict Start", f"Tasks: {len(conditions)}", 5) for cond in conditions: temp = cond['T'] time_str = cond['time'] steps = parse_time_to_steps(time_str) - # [新增] 计算 MSD Window - # 公式: 10 * window * 20 = steps => window = steps / 200 + # 自动计算 MSD Window (Steps / 200) msd_window = int(steps / 200) sub_dir_name = f"{temp}K" sub_work_dir = os.path.join(step_dir, sub_dir_name) os.makedirs(sub_work_dir, exist_ok=True) - self.logger.info( - f"-> Running Prediction: {temp}K, {time_str} ({steps} steps, MSD window={msd_window})") + self.logger.info(f"-> Running Prediction: {temp}K, {time_str}") - # 分发文件 - if os.path.exists(model_src): - shutil.copy(model_src, os.path.join(sub_work_dir, "model.xyz")) - shutil.copy(self.current_nep_pot, os.path.join(sub_work_dir, "nep.txt")) + # 分发准备好的 model.xyz 和 nep.txt + shutil.copy(final_model_xyz, os.path.join(sub_work_dir, "model.xyz")) + shutil.copy(nep_src, os.path.join(sub_work_dir, "nep.txt")) # 生成 run.in template_path = os.path.join(self.template_dir, "04.predict", "run.in") @@ -755,7 +825,6 @@ class Workflow: with open(template_path, 'r') as f: content = f.read() - # [修改] 替换参数,增加 MSD_WINDOW content = content.replace("{T}", str(temp)) content = content.replace("{STEPS}", str(steps)) content = content.replace("{MSD_WINDOW}", str(msd_window)) @@ -766,34 +835,28 @@ class Workflow: self.logger.error(f"Template not found: {template_path}") continue - # 执行 GPUMD - # 这里不使用 nohup 模式,因为是串行跑,直接记录日志即可 if not run_cmd_with_log("gpumd", sub_work_dir, "predict.log"): self.logger.error(f"Prediction failed at {temp}K") - # 也可以选择 continue 或者 return - # 3. 后处理分析 (gpumdkit.sh -plt sigma) + # ------------------------------------------------- + # 4. 后处理分析 + # ------------------------------------------------- self.logger.info("Running Analysis (gpumdkit -plt sigma)...") - kit_path = self.machine.config['paths'].get('gpumdkit', 'gpumdkit.sh') - # 捕获输出用于解析 analysis_log = os.path.join(step_dir, "sigma_analysis.log") cmd_analyze = f"{kit_path} -plt sigma" - # 我们需要捕获 stdout 内容 process = subprocess.Popen( cmd_analyze, shell=True, cwd=step_dir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True ) stdout, _ = process.communicate() - # 将输出写入日志文件备份 with open(analysis_log, 'w') as f: f.write(stdout) - # 4. 解析输出并生成报告 - # 目标格式: CSV + # 解析输出并生成报告 (Regex Logic) csv_data = [] ea_val = "N/A" sigma_300k = "N/A" @@ -803,26 +866,16 @@ class Workflow: for line in lines: line = line.strip() - # 解析 Ea if "Ea:" in line: - # 格式: 04.predict, Ea: 0.305 eV match = re.search(r"Ea:\s+([\d\.]+)\s+eV", line) if match: ea_val = match.group(1) - - # 解析 300K Sigma if "at 300K" in line: - # 格式: at 300K, 04.predict: Sigma = 1.816e-03 S/cm match = re.search(r"Sigma\s*=\s*([\d\.eE\+\-]+)", line) if match: sigma_300k = match.group(1) - - # 解析表格 if "----------------" in line: - in_table = not in_table # 切换状态 + in_table = not in_table continue - if in_table: - # T (K) Sigma (S/cm) Sigma·T (K·S/cm) - # 425 3.656e-02 1.554e+01 parts = line.split() if len(parts) >= 3 and parts[0].isdigit(): csv_data.append({ @@ -831,30 +884,21 @@ class Workflow: "Sigma*T": parts[2] }) - # 写入 CSV 到 output report_path = os.path.join(output_dir, "conductivity_report.csv") with open(report_path, 'w') as f: - # 写头部汇总信息 - f.write(f"# Extracted from gpumdkit output\n") f.write(f"# Ea (eV):,{ea_val}\n") - f.write(f"# Extrapolated Sigma@300K (S/cm):,{sigma_300k}\n") - f.write("\n") - # 写表格数据 + f.write(f"# Sigma@300K (S/cm):,{sigma_300k}\n\n") f.write("Temperature(K),Sigma(S/cm),Sigma*T(K*S/cm)\n") for row in csv_data: f.write(f"{row['T(K)']},{row['Sigma(S/cm)']},{row['Sigma*T']}\n") - self.logger.info(f"Report saved to {report_path}") - self.notifier.send("Predict Done", f"Ea: {ea_val} eV, Sigma@300K: {sigma_300k}", 5) + self.notifier.send("Predict Done", f"Ea: {ea_val} eV", 5) - # 5. 归档 Arrhenius.png src_png = os.path.join(step_dir, "Arrhenius.png") if os.path.exists(src_png): shutil.copy(src_png, os.path.join(output_dir, "Arrhenius.png")) - self.logger.info("Archived Arrhenius.png") - else: - self.logger.warning("Arrhenius.png not found.") - + self.notifier.send(f"Step Done: {step_name}", f"project {self.param['project']} Finished task {task_id_predict}", 2) self.tracker.mark_done(task_id_predict) else: - self.logger.info("Skipping Predict (Already Done).") \ No newline at end of file + self.logger.info("Skipping Predict (Already Done).") + self.notifier.send("Workflow end", f"project {self.param['project']} success", 5) \ No newline at end of file diff --git a/template/00.md/production/run.in b/template/00.md/production/run.in index ead2fd4..179dc75 100644 --- a/template/00.md/production/run.in +++ b/template/00.md/production/run.in @@ -1,7 +1,7 @@ potential ./nep.txt -velocity 400 +velocity 350 -ensemble npt_mttk temp 400 800 aniso 0 0 +ensemble npt_mttk temp 350 900 aniso 0 0 dump_thermo 10 dump_exyz 100 run 500000 diff --git a/template/02.scf/INCAR b/template/02.scf/INCAR index 500ea1d..4508fea 100644 --- a/template/02.scf/INCAR +++ b/template/02.scf/INCAR @@ -1,24 +1,29 @@ -SYSTEM = LiGePS static SCF -ISTART = 0 -ICHARG = 2 -ENCUT = 520 -NPAR = 8 -PREC = Normal -GGA = PE -EDIFF = 1E-6 -ALGO = Normal -NELM = 120 -NSW = 0 -IBRION = -1 -ISPIN = 1 -ISMEAR = 0 -SIGMA = 0.05 -LASPH = .TRUE. -LREAL = .FALSE. -ADDGRID = .TRUE. -ISYM = 2 -LCHARG = .FALSE. -LWAVE = .FALSE. +SYSTEM = Li3YCl6 Training Data Generation -KSPACING= 0.25 -KGAMMA = .TRUE. +! --- 电子步基础设置 --- +ENCUT = 520 ! [anie202215544-sup-0001-misc_information (1).pdf] 明确指定 +PREC = Accurate ! 保证力计算的精度 +ALGO = Normal ! 或者 Normal +LREAL = .FALSE. ! 对于生成 Force training data,Projection 最好关掉(False) + +! --- 电子收敛标准 --- +! 文献提到能量收敛 1e-4,但为了训练势函数,建议稍微严一点 +EDIFF = 1E-6 +ISYM = 0 ! 关闭对称性,防止 MD/采样 过程中因为对称性导致力计算的人为约束 + +! --- 展宽设置 (绝缘体/半导体) --- +ISMEAR = 0 ! Gaussian Smearing +SIGMA = 0.05 ! 配合 ISMEAR=0 使用,文献常用值 + +! --- 关键:optB88-vdW 泛函设置 --- +! [anie202215544-sup-0001-misc_information (1).pdf] 明确指出使用 optB88-vdW +! 以下参数必须完全照抄,不能改动 +LUSE_VDW = .TRUE. ! 开启 vdW 修正 +AGGAC = 0.0000 ! 必须为 0 +GGA = BO ! optB88 基于 MK 交换泛函 +PARAM1 = 0.18333333 ! optB88 特有参数 +PARAM2 = 0.22000000 ! optB88 特有参数 + +! --- 输出控制 --- +LWAVE = .FALSE. ! 训练数据不需要波函数,节省空间 +LCHARG = .FALSE. ! 不需要电荷密度 diff --git a/template/02.scf/INCAR_unifrom b/template/02.scf/INCAR_unifrom new file mode 100644 index 0000000..f8d0b8b --- /dev/null +++ b/template/02.scf/INCAR_unifrom @@ -0,0 +1,24 @@ +SYSTEM = static SCF +ISTART = 0 +ICHARG = 2 +ENCUT = 520 +NPAR = 8 +PREC = Normal +GGA = PE +EDIFF = 1E-6 +ALGO = Normal +NELM = 120 +NSW = 0 +IBRION = -1 +ISPIN = 1 +ISMEAR = 0 +SIGMA = 0.05 +LASPH = .TRUE. +LREAL = .FALSE. +ADDGRID = .TRUE. +ISYM = 2 +LCHARG = .FALSE. +LWAVE = .FALSE. + +KSPACING= 0.25 +KGAMMA = .TRUE. diff --git a/template/02.scf/KPOINTS b/template/02.scf/KPOINTS new file mode 100644 index 0000000..e12e2b1 --- /dev/null +++ b/template/02.scf/KPOINTS @@ -0,0 +1,5 @@ +Automatic Mesh for Training Data +0 ! 0 表示自动生成 +Gamma ! 必须用 Gamma centered(六角/三角晶系推荐) +3 3 3 ! 或者 3 3 3,取决于你的计算资源,2x2x2 通常够用 +0 0 0 ! shift (通常为0) \ No newline at end of file diff --git a/template/02.scf/vdw_kernel.bindat b/template/02.scf/vdw_kernel.bindat new file mode 100644 index 0000000..c302016 Binary files /dev/null and b/template/02.scf/vdw_kernel.bindat differ diff --git a/template/03.train/nep.in b/template/03.train/nep.in index 2aa3aee..884ff31 100644 --- a/template/03.train/nep.in +++ b/template/03.train/nep.in @@ -1,4 +1,4 @@ -type 4 Li Ge P S +type 4 Li Y Cl zbl 2 cutoff 6 5 generation 100000