Download engines/molgate_module5_engineB.py from chihsing/DrugOps: direct link, hf CLI and curl.
- Browser
- Download file 12.9 kB
-
https://huggingface.co/spaces/chihsing/DrugOps/resolve/main/engines/molgate_module5_engineB.py
- Command line
-
hf download hf://spaces/chihsing/DrugOps/engines/molgate_module5_engineB.py
-
curl -L -o molgate_module5_engineB.py https://huggingface.co/spaces/chihsing/DrugOps/resolve/main/engines/molgate_module5_engineB.py
12.9 kB
| """ | |
| MolGate — Module 5: Docking & Output (Engine B) | |
| Stage 27: 最終人工審核 | |
| 輸入:docking_result.json(Engine A 契約) | |
| 輸出:stage_27_review.json、final_report.json | |
| 決策: | |
| ACCEPT → 寫入 final_report.json,pipeline 完成 | |
| RETRY → 標記需重跑的 Module,不寫最終報告 | |
| FLAG → 標記問題,結果存檔但不視為完成 | |
| 依賴:無額外依賴 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| from datetime import datetime | |
| from pathlib import Path | |
| from typing import Any | |
| _PKG_ROOT = Path(__file__).resolve().parent | |
| _UI_ROOT = _PKG_ROOT.parent | |
| if str(_PKG_ROOT) not in sys.path: | |
| sys.path.insert(0, str(_PKG_ROOT)) | |
| if str(_UI_ROOT) not in sys.path: | |
| sys.path.insert(0, str(_UI_ROOT)) | |
| from molgate_ui.console_utf8 import configure_console_utf8, should_prompt_interactive | |
| configure_console_utf8() | |
| SCHEMA_VERSION = "0.2" | |
| RETRY_TARGETS = { | |
| "1": ("Module 1", "SMILES / PDB 輸入問題"), | |
| "2": ("Module 2", "配體準備問題(質子化、手性)"), | |
| "3": ("Module 3", "蛋白準備問題(去水、殘基 override)"), | |
| "4": ("Module 4", "口袋定義問題(配體選擇、座標)"), | |
| "5": ("Module 5", "Docking 參數問題(exhaustiveness、box size)"), | |
| } | |
| # ── 工具函式 ────────────────────────────────────────── | |
| def save_stage(session_dir: Path, stage: int, name: str, data: dict[str, Any]) -> dict[str, Any]: | |
| data = dict(data) | |
| data["_stage"] = stage | |
| data["_name"] = name | |
| data["_timestamp"] = datetime.now().isoformat() | |
| data["_schema_version"] = SCHEMA_VERSION | |
| path = session_dir / f"stage_{stage:02d}_{name}.json" | |
| path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8") | |
| return data | |
| def load_docking_result(session_dir: Path) -> dict[str, Any]: | |
| path = session_dir / "docking_result.json" | |
| if not path.exists(): | |
| raise FileNotFoundError("找不到 docking_result.json,請先跑 Engine A") | |
| data = json.loads(path.read_text(encoding="utf-8")) | |
| if not data.get("ready_for_review"): | |
| raise ValueError("docking_result.json 未標記 ready_for_review=True,請重跑 Engine A") | |
| sv = str(data.get("schema_version") or "").strip() | |
| if sv and sv != SCHEMA_VERSION: | |
| print(f" ⚠ docking_result.schema_version={sv!r},建議重跑 Module 5 Engine A({SCHEMA_VERSION})") | |
| return data | |
| def display_result(dr: dict[str, Any]): | |
| """顯示 docking 結果摘要。""" | |
| print("\n" + "─" * 55) | |
| print(" DOCKING 結果摘要") | |
| print("─" * 55) | |
| target = dr.get("target", "?") | |
| pdb_id = dr.get("pdb_id", "?") | |
| het_id = dr.get("het_id", "?") | |
| chain = dr.get("chain", "?") | |
| td = dr.get("target_drug", "") | |
| cn = dr.get("common_name", "") | |
| print(f" Target:{target} | PDB:{pdb_id}") | |
| if td: | |
| print(f" Docking 藥物:{td}") | |
| if cn: | |
| print(f" 共晶配體:{cn} ({het_id} chain {chain})") | |
| else: | |
| print(f" 共晶配體:{het_id} chain {chain}") | |
| print(f"\n Best pose:Mode {dr['best_pose_mode']} | " | |
| f"{dr['best_affinity']:.3f} kcal/mol") | |
| print(f" Confidence tier:{dr['confidence_tier']}") | |
| print(f" QC pass:{dr['qc_pass']}") | |
| cutoff = float(dr.get("out_of_pocket_rmsd_lb_cutoff", 8.0)) | |
| oop_modes = dr.get("out_of_pocket_modes") or [] | |
| poses = dr.get("all_poses", []) | |
| if poses: | |
| print(f"\n 所有 pose:") | |
| for p in poses: | |
| rmsd_warn = " ⚠ 口袋外" if p["rmsd_lb"] > cutoff else "" | |
| print(f" Mode {p['mode']:2d}: {p['affinity']:7.3f} kcal/mol | " | |
| f"RMSD lb={p['rmsd_lb']:.2f}{rmsd_warn}") | |
| if oop_modes: | |
| print(f" ⚠ OUT_OF_POCKET cutoff:{cutoff:.1f} Å | modes={oop_modes}") | |
| flags = dr.get("flags", []) | |
| if flags: | |
| print(f"\n Flags:") | |
| for f in flags: | |
| print(f" ⚠ {f}") | |
| print("─" * 55) | |
| def prompt_decision() -> tuple[str, str]: | |
| """人工審核決策介面。""" | |
| print("\n ── Stage 27:最終審核 ──") | |
| print(" [1] ACCEPT → 接受結果,寫入最終報告") | |
| print(" [2] RETRY → 標記問題,指定重跑 Module") | |
| print(" [3] FLAG → 標記問題,結果存檔但不視為完成") | |
| print() | |
| while True: | |
| try: | |
| raw = input(" 請選擇 [1/2/3]:").strip() | |
| if raw in ("1", "2", "3"): | |
| break | |
| except KeyboardInterrupt: | |
| sys.exit(0) | |
| print(" 請輸入 1、2 或 3") | |
| note = "" | |
| if raw == "1": | |
| try: | |
| note = input(" 備註(選填,直接 Enter 跳過):").strip() | |
| except KeyboardInterrupt: | |
| pass | |
| return "ACCEPT", note | |
| if raw == "2": | |
| print("\n 指定需重跑的 Module:") | |
| for k, (mod, reason) in RETRY_TARGETS.items(): | |
| print(f" [{k}] {mod}:{reason}") | |
| retry_mod = "" | |
| retry_reason = "" | |
| while True: | |
| try: | |
| m = input(" 請選擇 Module [1-5]:").strip() | |
| if m in RETRY_TARGETS: | |
| retry_mod, retry_reason = RETRY_TARGETS[m] | |
| break | |
| except KeyboardInterrupt: | |
| sys.exit(0) | |
| print(" 請輸入 1~5") | |
| try: | |
| note = input(f" 問題說明(選填):").strip() | |
| except KeyboardInterrupt: | |
| pass | |
| return "RETRY", f"重跑 {retry_mod}:{retry_reason}。{note}".strip() | |
| if raw == "3": | |
| try: | |
| note = input(" FLAG 原因(選填):").strip() | |
| except KeyboardInterrupt: | |
| pass | |
| return "FLAG", note | |
| return "FLAG", "" | |
| def build_non_interactive_note( | |
| decision: str, | |
| note: str | None, | |
| retry_module: str | None = None, | |
| ) -> str: | |
| """API/自動化模式下組裝 note。""" | |
| base = (note or "").strip() | |
| if decision == "RETRY": | |
| if retry_module and retry_module in RETRY_TARGETS: | |
| mod, reason = RETRY_TARGETS[retry_module] | |
| prefix = f"重跑 {mod}:{reason}" | |
| return f"{prefix}。{base}".strip() | |
| return base or "重跑(未指定模組)" | |
| return base | |
| def write_final_report( | |
| session_dir: Path, | |
| dr: dict[str, Any], | |
| note: str, | |
| ) -> Path: | |
| """寫入最終報告。""" | |
| report = { | |
| "schema_version": SCHEMA_VERSION, | |
| "status": "COMPLETE", | |
| "target": dr.get("target", ""), | |
| "pdb_id": dr.get("pdb_id", ""), | |
| "het_id": dr.get("het_id", ""), | |
| "chain": dr.get("chain", ""), | |
| "pocket_rule": dr.get("pocket_rule", ""), | |
| "best_affinity": dr["best_affinity"], | |
| "best_pose_mode": dr["best_pose_mode"], | |
| "confidence_tier": dr["confidence_tier"], | |
| "qc_pass": dr["qc_pass"], | |
| "flags": dr.get("flags", []), | |
| "all_poses": dr.get("all_poses", []), | |
| "output_pdbqt": dr.get("output_pdbqt", ""), | |
| "pocket_center": dr.get("pocket_center", {}), | |
| "out_of_pocket_rmsd_lb_cutoff": dr.get("out_of_pocket_rmsd_lb_cutoff"), | |
| "out_of_pocket_modes": dr.get("out_of_pocket_modes", []), | |
| "reviewer_note": note, | |
| "reviewed_at": datetime.now().isoformat(), | |
| } | |
| for key in ("common_name", "target_drug"): | |
| if dr.get(key): | |
| report[key] = dr[key] | |
| path = session_dir / "final_report.json" | |
| path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8") | |
| return path | |
| def _update_manifest_module5_engine_b( | |
| session_dir: Path, | |
| decision: str, | |
| report_path: Path | None, | |
| ) -> None: | |
| try: | |
| from molgate_manifest import read_manifest, rel_path, write_manifest | |
| except ImportError: | |
| return | |
| mp = session_dir / "molgate_run_manifest.json" | |
| if not mp.is_file(): | |
| return | |
| try: | |
| m = read_manifest(session_dir) | |
| pl = dict(m.get("pipeline") or {}) | |
| pl["module5_engine_b"] = { | |
| "status": decision.lower(), | |
| "completed_at": datetime.now().isoformat(), | |
| "stages": ["27"], | |
| } | |
| m["pipeline"] = pl | |
| if report_path: | |
| paths = dict(m.get("paths") or {}) | |
| rmap = dict(paths.get("relative") or {}) | |
| rmap["final_report"] = rel_path(session_dir, report_path) | |
| paths["relative"] = rmap | |
| m["paths"] = paths | |
| summ = dict(m.get("summary") or {}) | |
| summ["pipeline_complete"] = True | |
| summ["final_report"] = rmap["final_report"] | |
| m["summary"] = summ | |
| write_manifest(session_dir, m) | |
| except Exception: | |
| pass | |
| # ── Stage 27:最終人工審核 ──────────────────────────── | |
| def stage27_review( | |
| session_dir: Path, | |
| dr: dict[str, Any], | |
| *, | |
| decision_override: str | None = None, | |
| note_override: str | None = None, | |
| retry_module: str | None = None, | |
| ) -> dict[str, Any]: | |
| print("\n[Stage 27] 最終人工審核...") | |
| display_result(dr) | |
| if decision_override: | |
| decision = decision_override.strip().upper() | |
| if decision not in ("ACCEPT", "RETRY", "FLAG"): | |
| raise ValueError(f"不支援 decision={decision_override!r},需為 ACCEPT/RETRY/FLAG") | |
| note = build_non_interactive_note(decision, note_override, retry_module) | |
| else: | |
| if not should_prompt_interactive(): | |
| raise ValueError( | |
| "Stage 27 需人工審核:請在 UI 選擇 ACCEPT/RETRY/FLAG,或使用 --decision" | |
| ) | |
| decision, note = prompt_decision() | |
| report_path = None | |
| status = decision | |
| if decision == "ACCEPT": | |
| report_path = write_final_report(session_dir, dr, note) | |
| print(f"\n ✓ 結果接受") | |
| print(f" ✓ 最終報告:{report_path}") | |
| elif decision == "RETRY": | |
| print(f"\n ↩ 標記重跑:{note}") | |
| elif decision == "FLAG": | |
| print(f"\n ⚑ 標記問題:{note or '(無說明)'}") | |
| print(f" → Status: {status}") | |
| result = { | |
| "status": status, | |
| "decision": decision, | |
| "note": note, | |
| "final_report": str(report_path.as_posix()) if report_path else None, | |
| "pipeline_complete": decision == "ACCEPT", | |
| } | |
| r = save_stage(session_dir, 27, "review", result) | |
| _update_manifest_module5_engine_b(session_dir, decision, report_path) | |
| return r | |
| # ── 主流程 ──────────────────────────────────────────── | |
| def run_engine_b( | |
| session_dir: Path, | |
| *, | |
| decision: str | None = None, | |
| note: str | None = None, | |
| retry_module: str | None = None, | |
| ): | |
| print("=" * 55) | |
| print(" MolGate — Module 5 Engine B") | |
| print(" Stage 27:最終人工審核") | |
| print("=" * 55) | |
| try: | |
| dr = load_docking_result(session_dir) | |
| except (FileNotFoundError, ValueError) as e: | |
| print(f"❌ {e}") | |
| sys.exit(1) | |
| try: | |
| result = stage27_review( | |
| session_dir, | |
| dr, | |
| decision_override=decision, | |
| note_override=note, | |
| retry_module=retry_module, | |
| ) | |
| except Exception as e: | |
| print(f"\n❌ Pipeline stopped: {e}") | |
| sys.exit(1) | |
| print("\n" + "=" * 55) | |
| decision = result["decision"] | |
| if decision == "ACCEPT": | |
| print(" Pipeline 完成 ✓") | |
| print(f" 最終報告:{session_dir / 'final_report.json'}") | |
| elif decision == "RETRY": | |
| print(f" ↩ 需重跑:{result['note']}") | |
| else: | |
| print(f" ⚑ 已標記:{result['note'] or '(無說明)'}") | |
| print("=" * 55) | |
| return result | |
| # ── CLI ────────────────────────────────────────────── | |
| if __name__ == "__main__": | |
| ap = argparse.ArgumentParser(description="MolGate Module 5 Engine B") | |
| ap.add_argument("--session-dir", required=True) | |
| ap.add_argument( | |
| "--decision", | |
| choices=("ACCEPT", "RETRY", "FLAG"), | |
| default=None, | |
| help="非互動模式決策(供 API/UI 使用)", | |
| ) | |
| ap.add_argument( | |
| "--note", | |
| default=None, | |
| help="非互動模式備註(可選)", | |
| ) | |
| ap.add_argument( | |
| "--retry-module", | |
| choices=("1", "2", "3", "4", "5"), | |
| default=None, | |
| help="decision=RETRY 時指定重跑模組(1-5)", | |
| ) | |
| args = ap.parse_args() | |
| sd = Path(args.session_dir) | |
| if not sd.is_dir(): | |
| print(f"❌ Session 目錄不存在:{sd}") | |
| sys.exit(1) | |
| run_engine_b( | |
| sd, | |
| decision=args.decision, | |
| note=args.note, | |
| retry_module=args.retry_module, | |
| ) | |