DrugOps / molgate_ui /creadrug_export.py
chihsing's picture
Upload 180 files
af894e0 verified
Raw
History Blame Contribute Delete
11.6 kB
"""
Export a MolGate / DrugOps session into a CreaDrug24 case pack.
Writes under CREADRUG_ROOT/input/cases/<caseId>/ and updates input/cases_molgate.json.
"""
from __future__ import annotations
import json
import re
import shutil
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from molgate_ui.index_status import safe_session_dir, safe_session_file
from molgate_ui.paths import SESSIONS, creadrug24_root
CASES_REGISTRY = "input/cases_molgate.json"
def _read_json(p: Path) -> dict[str, Any] | None:
if not p.is_file():
return None
try:
return json.loads(p.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return None
def _first_existing(sd: Path, rel_paths: list[str]) -> Path | None:
for rel in rel_paths:
fp = safe_session_file(sd, rel)
if fp:
return fp
return None
def _resolve_drug_id(sd: Path, meta: dict[str, Any] | None, s3: dict[str, Any] | None) -> int | None:
for src in (meta, s3):
if not src:
continue
raw = src.get("drug_id") or src.get("_catalog_drug_id")
if raw is not None:
try:
return int(raw)
except (TypeError, ValueError):
pass
man = _read_json(sd / "molgate_run_manifest.json")
if man:
raw = man.get("drug_id")
if raw is not None:
try:
return int(raw)
except (TypeError, ValueError):
pass
return None
def _drug_name(meta: dict[str, Any] | None, s3: dict[str, Any] | None, dr: dict[str, Any] | None) -> str:
for src in (meta, s3, dr):
if not src:
continue
for key in ("drug_label", "drug_name", "target_drug", "common_name"):
val = src.get(key)
if isinstance(val, str) and val.strip():
return val.strip()
return ""
def _target_label(meta: dict[str, Any] | None, s3: dict[str, Any] | None, dr: dict[str, Any] | None) -> str:
target = str(
(meta or {}).get("target")
or (s3 or {}).get("target")
or (dr or {}).get("target")
or ""
).strip().upper()
if target.endswith("_PROXY"):
return target.split("_")[0]
parts = target.split("_")
return parts[0] if parts else target
def _default_case_id(session_id: str, drug_id: int | None) -> str:
if drug_id is not None and drug_id > 0:
return f"M{drug_id:02d}" if drug_id < 100 else f"M{drug_id}"
sid = re.sub(r"[^a-fA-F0-9]", "", session_id)[:8]
return f"M_{sid or 'session'}"
def _sanitize_case_id(case_id: str) -> str:
raw = (case_id or "").strip().upper()
if not raw:
raise ValueError("case_id 不可為空")
if not re.match(r"^[A-Z][A-Z0-9_\-]{0,31}$", raw):
raise ValueError(f"case_id 格式不合法:{case_id}")
return raw
def _build_docking_log(dr: dict[str, Any] | None, vina_log: Path | None) -> list[str]:
lines: list[str] = []
if vina_log and vina_log.is_file():
try:
tail = vina_log.read_text(encoding="utf-8", errors="replace").splitlines()[-12:]
lines.extend(t.strip() for t in tail if t.strip())
except OSError:
pass
if dr:
aff = dr.get("best_affinity")
mode = dr.get("best_pose_mode", 1)
tier = dr.get("confidence_tier", "")
qc = dr.get("qc_pass")
if aff is not None:
lines.insert(0, f"Mode {mode}: {aff} kcal/mol")
if tier:
lines.append(f"[STAGE 26] Confidence tier {tier}")
if qc is not None:
lines.append(f"[STAGE 25] QC {'PASS' if qc else 'WARN'}")
return lines or ["[DrugOps export] docking log unavailable"]
def _build_summary(dr: dict[str, Any] | None, qc: dict[str, Any] | None) -> str:
if dr:
flags = dr.get("flags") or []
flag_txt = f" Flags: {'; '.join(flags)}" if flags else ""
return (
f"DrugOps export · best pose #{dr.get('best_pose_mode', 1)} · "
f"affinity {dr.get('best_affinity')} kcal/mol · "
f"tier {dr.get('confidence_tier', '—')}.{flag_txt}"
)
if qc:
return f"DrugOps export · QC status {qc.get('status', '—')}"
return "DrugOps session export."
def build_case_entry(
case_id: str,
case_dir_rel: str,
*,
drug_id: int | None,
drug_name: str,
smiles: str,
target_name: str,
index_key: str,
pdb_id: str,
dr: dict[str, Any] | None,
qc: dict[str, Any] | None,
vina_log: Path | None,
pose_ext: str,
) -> dict[str, Any]:
aff = dr.get("best_affinity") if dr else None
tier = str(dr.get("confidence_tier") or "—") if dr else "—"
qc_pass = dr.get("qc_pass") if dr else None
status = "COMPLETE" if aff is not None else "PARTIAL"
if qc and str(qc.get("status", "")).upper() in ("FAIL", "ERROR"):
status = "QC-FLAG"
return {
"caseId": case_id,
"catalogDrugId": drug_id,
"drugName": drug_name or index_key or case_id,
"formula": "",
"smiles": smiles or "",
"targetName": target_name or index_key,
"indexKey": index_key,
"pdbId": pdb_id,
"receptorPdbUrl": f"{case_dir_rel}/receptor.pdb",
"dockedPdbqtUrl": f"{case_dir_rel}/best_pose.{pose_ext}",
"pocketConfigUrl": f"{case_dir_rel}/stage_23_pocket_center.json",
"dockingResultUrl": f"{case_dir_rel}/docking_result.json",
"bestAffinity": aff,
"confidenceTier": tier,
"status": status if qc_pass is not False else "QC-FLAG",
"summary": _build_summary(dr, qc),
"dockingLog": _build_docking_log(dr, vina_log),
"source": "drugops",
"exportedAt": datetime.now(timezone.utc).isoformat(),
}
def _update_registry(creadrug_root: Path, entry: dict[str, Any]) -> Path:
reg_path = creadrug_root / CASES_REGISTRY
reg_path.parent.mkdir(parents=True, exist_ok=True)
payload: dict[str, Any] = {"schema_version": "1.0", "cases": []}
if reg_path.is_file():
try:
existing = json.loads(reg_path.read_text(encoding="utf-8"))
if isinstance(existing, dict) and isinstance(existing.get("cases"), list):
payload = existing
elif isinstance(existing, list):
payload = {"schema_version": "1.0", "cases": existing}
except (json.JSONDecodeError, OSError):
pass
cases: list[dict[str, Any]] = payload.setdefault("cases", [])
cid = entry["caseId"]
replaced = False
for i, c in enumerate(cases):
if c.get("caseId") == cid:
cases[i] = entry
replaced = True
break
if not replaced:
cases.append(entry)
payload["updated_at"] = datetime.now(timezone.utc).isoformat()
reg_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
return reg_path
def export_session_to_creadrug(
session_id: str,
*,
sessions_root: Path | None = None,
creadrug_root: Path | None = None,
case_id: str | None = None,
overwrite: bool = True,
) -> dict[str, Any]:
"""Copy session artifacts into CreaDrug24 and register the case."""
sid = (session_id or "").strip()
sd = safe_session_dir(sessions_root or SESSIONS, sid)
if not sd:
return {"ok": False, "error": f"session not found: {sid}"}
root = (creadrug_root or creadrug24_root()).resolve()
if not root.is_dir():
return {"ok": False, "error": f"CreaDrug24 root not found: {root}"}
meta = _read_json(sd / "session_meta.json")
s1 = _read_json(sd / "stage_01_smiles.json")
s3 = _read_json(sd / "stage_03_index.json")
dr = _read_json(sd / "docking_result.json")
qc = _read_json(sd / "stage_25_qc.json")
pocket = _read_json(sd / "stage_23_pocket_center.json") or _read_json(sd / "pocket_config.json")
receptor_src = _first_existing(sd, ["protein/cleaned.pdb", "protein/apo_for_docking.pdb"])
pose_pdbqt = _first_existing(sd, ["docking/output.pdbqt", "best_pose.pdbqt"])
pose_pdb = _first_existing(sd, ["best_pose.pdb"])
if not receptor_src:
return {"ok": False, "error": "缺少 receptor(protein/cleaned.pdb)"}
if not pose_pdbqt and not pose_pdb:
return {"ok": False, "error": "缺少 docking pose(docking/output.pdbqt 或 best_pose.pdb)"}
if not dr:
return {"ok": False, "error": "缺少 docking_result.json(請先跑完 Module 5 Engine A)"}
drug_id = _resolve_drug_id(sd, meta, s3)
cid = _sanitize_case_id(case_id or _default_case_id(sid, drug_id))
case_dir = root / "input" / "cases" / cid
if case_dir.exists() and not overwrite:
return {"ok": False, "error": f"case 已存在:{cid}(加 overwrite=true 覆寫)"}
case_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(receptor_src, case_dir / "receptor.pdb")
pose_ext = "pdbqt"
if pose_pdbqt:
shutil.copy2(pose_pdbqt, case_dir / "best_pose.pdbqt")
pose_ext = "pdbqt"
if pose_pdb:
shutil.copy2(pose_pdb, case_dir / "best_pose.pdb")
if not pose_pdbqt:
pose_ext = "pdb"
for src_name, dst_name in (
("docking_result.json", "docking_result.json"),
("final_report.json", "final_report.json"),
("stage_24_vina.json", "stage_24_vina.json"),
("docking/vina.log", "vina.log"),
):
src = safe_session_file(sd, src_name)
if src:
shutil.copy2(src, case_dir / dst_name)
if pocket:
(case_dir / "stage_23_pocket_center.json").write_text(
json.dumps(pocket, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
dr_export = dict(dr)
dr_export["output_pdbqt"] = f"input/cases/{cid}/best_pose.{pose_ext}"
(case_dir / "docking_result.json").write_text(
json.dumps(dr_export, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
manifest = {
"caseId": cid,
"session_id": sid,
"catalogDrugId": drug_id,
"exported_at": datetime.now(timezone.utc).isoformat(),
"source": "drugops",
"session_dir": str(sd),
"creadrug_case_dir": str(case_dir),
}
(case_dir / "case_manifest.json").write_text(
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
case_dir_rel = f"input/cases/{cid}"
index_key = str(
(s3 or {}).get("index_key")
or (meta or {}).get("target")
or (dr or {}).get("target")
or ""
).strip().upper()
pdb_id = str(
(meta or {}).get("pdb_id")
or (s3 or {}).get("pdb_id")
or (dr or {}).get("pdb_id")
or ""
).strip().upper()
smiles = str(
(s1 or {}).get("canonical_smiles")
or (s1 or {}).get("input_smiles")
or (meta or {}).get("smiles_input")
or ""
).strip()
entry = build_case_entry(
cid,
case_dir_rel,
drug_id=drug_id,
drug_name=_drug_name(meta, s3, dr),
smiles=smiles,
target_name=_target_label(meta, s3, dr),
index_key=index_key,
pdb_id=pdb_id,
dr=dr,
qc=qc,
vina_log=case_dir / "vina.log",
pose_ext=pose_ext,
)
reg_path = _update_registry(root, entry)
return {
"ok": True,
"session_id": sid,
"case_id": cid,
"case_dir": str(case_dir),
"registry": str(reg_path),
"entry": entry,
"creadrug_root": str(root),
}