Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- __pycache__/app.cpython-310.pyc +0 -0
- app.py +155 -154
- app.py.backup_before_final_ui +754 -0
- app.py.backup_old_tabs +754 -0
__pycache__/app.cpython-310.pyc
CHANGED
|
Binary files a/__pycache__/app.cpython-310.pyc and b/__pycache__/app.cpython-310.pyc differ
|
|
|
app.py
CHANGED
|
@@ -4,6 +4,7 @@ import subprocess
|
|
| 4 |
from pathlib import Path
|
| 5 |
import uuid
|
| 6 |
from datetime import datetime
|
|
|
|
| 7 |
import gradio as gr
|
| 8 |
import pandas as pd
|
| 9 |
from huggingface_hub import snapshot_download
|
|
@@ -11,7 +12,10 @@ from rdkit import Chem
|
|
| 11 |
from rdkit.Chem import Descriptors, Crippen, Lipinski, rdMolDescriptors, QED, AllChem
|
| 12 |
|
| 13 |
|
| 14 |
-
|
|
|
|
|
|
|
|
|
|
| 15 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 16 |
|
| 17 |
CACHE_DIR = Path("bundle_cache")
|
|
@@ -22,6 +26,7 @@ BUNDLE_ROOT = None
|
|
| 22 |
|
| 23 |
|
| 24 |
def download_bundle_light():
|
|
|
|
| 25 |
global EXAMPLE_JSON, API_SPEC_TEXT, BACKEND_CONTRACT_TEXT, BUNDLE_ROOT
|
| 26 |
|
| 27 |
try:
|
|
@@ -97,6 +102,7 @@ def compute_row(smiles: str):
|
|
| 97 |
"docking_score": None,
|
| 98 |
"docking_status": "not_run",
|
| 99 |
"docking_pose_file": None,
|
|
|
|
| 100 |
"note": "Invalid SMILES",
|
| 101 |
}
|
| 102 |
|
|
@@ -116,10 +122,43 @@ def compute_row(smiles: str):
|
|
| 116 |
"docking_score": None,
|
| 117 |
"docking_status": "not_run",
|
| 118 |
"docking_pose_file": None,
|
|
|
|
| 119 |
"note": "CPU demo: DeepPurpose and real docking are disabled in this Space.",
|
| 120 |
}
|
| 121 |
|
| 122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
def score_smiles(smiles_text):
|
| 124 |
smiles_list = []
|
| 125 |
for part in str(smiles_text).replace(",", "\n").splitlines():
|
|
@@ -131,22 +170,24 @@ def score_smiles(smiles_text):
|
|
| 131 |
return pd.DataFrame(), {
|
| 132 |
"status": "error",
|
| 133 |
"message": "Please enter at least one SMILES.",
|
| 134 |
-
}
|
| 135 |
|
| 136 |
-
rows = [compute_row(s) for s in smiles_list]
|
| 137 |
df = pd.DataFrame(rows)
|
| 138 |
|
|
|
|
| 139 |
response = {
|
| 140 |
"status": "completed",
|
| 141 |
"mode": "score_smiles_cpu_demo",
|
| 142 |
"docking_mode": "off",
|
| 143 |
"summary": {
|
| 144 |
"num_input": len(smiles_list),
|
| 145 |
-
"num_valid": int(df["valid"].sum()),
|
| 146 |
-
"num_returned": len(df),
|
| 147 |
},
|
| 148 |
"columns": list(df.columns),
|
| 149 |
-
"results":
|
|
|
|
| 150 |
"warnings": [
|
| 151 |
"This Space is a CPU demo.",
|
| 152 |
"DeepPurpose pred_pAff_mean is not computed in this Space version.",
|
|
@@ -154,26 +195,8 @@ def score_smiles(smiles_text):
|
|
| 154 |
"Outputs are computational predictions only.",
|
| 155 |
],
|
| 156 |
}
|
| 157 |
-
csv_path = write_results_csv(df)
|
| 158 |
-
response["download_csv_file"] = csv_path
|
| 159 |
-
return df, response, csv_path
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
def get_example_table():
|
| 163 |
-
if EXAMPLE_JSON and "results" in EXAMPLE_JSON:
|
| 164 |
-
return pd.DataFrame(EXAMPLE_JSON["results"])
|
| 165 |
-
return pd.DataFrame([
|
| 166 |
-
{"message": "Example response not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable."}
|
| 167 |
-
])
|
| 168 |
-
|
| 169 |
|
| 170 |
-
|
| 171 |
-
if EXAMPLE_JSON:
|
| 172 |
-
return EXAMPLE_JSON
|
| 173 |
-
return {
|
| 174 |
-
"status": "not_loaded",
|
| 175 |
-
"message": "Example response not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable.",
|
| 176 |
-
}
|
| 177 |
|
| 178 |
|
| 179 |
def get_api_spec():
|
|
@@ -192,6 +215,7 @@ def get_contract():
|
|
| 192 |
"message": "backend_contract.json not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable.",
|
| 193 |
}
|
| 194 |
|
|
|
|
| 195 |
def _write_reinvent_sampling_config(job_id, num_molecules):
|
| 196 |
if BUNDLE_ROOT is None:
|
| 197 |
raise RuntimeError("Bundle root is not loaded. Check HF_TOKEN and BUNDLE_REPO.")
|
|
@@ -284,17 +308,13 @@ def _enrich_reinvent_csv(raw_csv, return_top_k):
|
|
| 284 |
else:
|
| 285 |
row["NLL"] = None
|
| 286 |
|
| 287 |
-
# Keep these stable for frontend/backend filters.
|
| 288 |
-
row["pred_pAff_mean"] = None
|
| 289 |
-
row["docking_score"] = None
|
| 290 |
-
row["docking_status"] = "not_run"
|
| 291 |
-
row["docking_pose_file"] = None
|
| 292 |
-
|
| 293 |
rows.append(row)
|
| 294 |
|
|
|
|
| 295 |
df = pd.DataFrame(rows)
|
| 296 |
|
| 297 |
preferred_cols = [
|
|
|
|
| 298 |
"SMILES",
|
| 299 |
"SMILES_state",
|
| 300 |
"NLL",
|
|
@@ -312,6 +332,7 @@ def _enrich_reinvent_csv(raw_csv, return_top_k):
|
|
| 312 |
"docking_score",
|
| 313 |
"docking_status",
|
| 314 |
"docking_pose_file",
|
|
|
|
| 315 |
"note",
|
| 316 |
]
|
| 317 |
|
|
@@ -329,8 +350,8 @@ def generate_job_demo(num_molecules, return_top_k, docking_mode, generation_back
|
|
| 329 |
"""
|
| 330 |
Generation endpoint for the Space.
|
| 331 |
|
| 332 |
-
|
| 333 |
-
|
| 334 |
"""
|
| 335 |
job_id = f"gen_{uuid.uuid4().hex[:8]}"
|
| 336 |
created_at = datetime.utcnow().isoformat() + "Z"
|
|
@@ -341,7 +362,7 @@ def generate_job_demo(num_molecules, return_top_k, docking_mode, generation_back
|
|
| 341 |
|
| 342 |
try:
|
| 343 |
if generation_backend == "live_reinvent_cpu":
|
| 344 |
-
raw_csv,
|
| 345 |
df = _enrich_reinvent_csv(raw_csv, return_top_k)
|
| 346 |
mode = "live_reinvent_cpu_sampling"
|
| 347 |
status = "completed"
|
|
@@ -365,16 +386,18 @@ def generate_job_demo(num_molecules, return_top_k, docking_mode, generation_back
|
|
| 365 |
clean["docking_score"] = None
|
| 366 |
clean["docking_status"] = "not_run"
|
| 367 |
clean["docking_pose_file"] = None
|
|
|
|
| 368 |
if "pred_pAff_mean" not in clean:
|
| 369 |
clean["pred_pAff_mean"] = None
|
| 370 |
clean_rows.append(clean)
|
| 371 |
|
|
|
|
| 372 |
df = pd.DataFrame(clean_rows)
|
| 373 |
mode = "generate_job_cpu_demo"
|
| 374 |
status = "completed"
|
| 375 |
warnings.extend([
|
| 376 |
"This is a CPU demo generation response.",
|
| 377 |
-
"Live REINVENT generation was not executed because
|
| 378 |
"The response shape matches the planned backend contract.",
|
| 379 |
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 380 |
])
|
|
@@ -385,6 +408,8 @@ def generate_job_demo(num_molecules, return_top_k, docking_mode, generation_back
|
|
| 385 |
status = "failed"
|
| 386 |
warnings.append(str(e))
|
| 387 |
|
|
|
|
|
|
|
| 388 |
output_dir = Path("outputs")
|
| 389 |
output_dir.mkdir(exist_ok=True)
|
| 390 |
csv_path = output_dir / f"{job_id}_results.csv"
|
|
@@ -415,7 +440,7 @@ def generate_job_demo(num_molecules, return_top_k, docking_mode, generation_back
|
|
| 415 |
"num_returned": int(len(df)),
|
| 416 |
},
|
| 417 |
"columns": list(df.columns),
|
| 418 |
-
"results":
|
| 419 |
"download_csv_file": str(csv_path),
|
| 420 |
"warnings": warnings,
|
| 421 |
}
|
|
@@ -423,14 +448,6 @@ def generate_job_demo(num_molecules, return_top_k, docking_mode, generation_back
|
|
| 423 |
return job_summary, df, response, str(csv_path)
|
| 424 |
|
| 425 |
|
| 426 |
-
def write_results_csv(df: pd.DataFrame, filename: str = "score_smiles_results.csv"):
|
| 427 |
-
out_dir = Path("outputs")
|
| 428 |
-
out_dir.mkdir(parents=True, exist_ok=True)
|
| 429 |
-
out_path = out_dir / filename
|
| 430 |
-
df.to_csv(out_path, index=False)
|
| 431 |
-
return str(out_path)
|
| 432 |
-
|
| 433 |
-
|
| 434 |
def smiles_to_3d_file(smiles: str, file_format: str = "sdf"):
|
| 435 |
smiles = str(smiles).strip()
|
| 436 |
file_format = str(file_format).lower().strip()
|
|
@@ -498,6 +515,7 @@ def smiles_to_3d_file(smiles: str, file_format: str = "sdf"):
|
|
| 498 |
|
| 499 |
return str(out_path), meta
|
| 500 |
|
|
|
|
| 501 |
loaded_ok, loaded_msg = download_bundle_light()
|
| 502 |
|
| 503 |
|
|
@@ -508,65 +526,26 @@ with gr.Blocks(title="EGFR AI Demo") as demo:
|
|
| 508 |
|
| 509 |
Private CPU demo for the EGFR AI backend workflow.
|
| 510 |
|
| 511 |
-
This demo
|
| 512 |
-
|
| 513 |
-
Real AutoDock-GPU docking is disabled in this CPU Space, but docking fields are present so backend/frontend teams can implement database columns and filters.
|
| 514 |
-
"""
|
| 515 |
-
)
|
| 516 |
-
|
| 517 |
-
gr.Markdown(f"Bundle load status: {loaded_msg}")
|
| 518 |
-
|
| 519 |
-
with gr.Tab("Score SMILES"):
|
| 520 |
-
gr.Markdown(
|
| 521 |
-
"""
|
| 522 |
-
Enter one or more SMILES.
|
| 523 |
|
| 524 |
-
|
|
|
|
|
|
|
| 525 |
|
| 526 |
-
|
| 527 |
-
- pred_pAff_mean is an integration field in this Space version.
|
| 528 |
-
- real docking is disabled in this CPU Space.
|
| 529 |
-
- docking_status is returned as not_run.
|
| 530 |
"""
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
smiles_input = gr.Textbox(
|
| 534 |
-
label="SMILES input",
|
| 535 |
-
value="CCO\nc1ccccc1",
|
| 536 |
-
lines=5,
|
| 537 |
-
)
|
| 538 |
-
|
| 539 |
-
score_btn = gr.Button("Score SMILES")
|
| 540 |
-
score_table = gr.Dataframe(label="Results table")
|
| 541 |
-
score_json = gr.JSON(label="Backend-style JSON response")
|
| 542 |
-
score_csv = gr.File(label="Download CSV")
|
| 543 |
|
|
|
|
| 544 |
|
| 545 |
-
|
| 546 |
-
score_smiles,
|
| 547 |
-
inputs=smiles_input,
|
| 548 |
-
outputs=[score_table, score_json, score_csv],
|
| 549 |
-
)
|
| 550 |
-
with gr.Tab("Generate Job Demo"):
|
| 551 |
gr.Markdown(
|
| 552 |
"""
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
Generation backend options:
|
| 556 |
-
|
| 557 |
-
- demo_rows: uses packaged example rows. Fast and stable.
|
| 558 |
-
- live_reinvent_cpu: tries to run real REINVENT sampling from the packaged checkpoint on CPU.
|
| 559 |
|
| 560 |
-
|
| 561 |
|
| 562 |
-
|
| 563 |
-
2. Generated molecule results table.
|
| 564 |
-
3. Full backend-style generation JSON.
|
| 565 |
-
4. Download CSV.
|
| 566 |
-
|
| 567 |
-
Notes:
|
| 568 |
-
- DeepPurpose live scoring is not enabled yet in this Space version.
|
| 569 |
-
- Real docking is disabled in this CPU Space, so docking_status is returned as not_run.
|
| 570 |
"""
|
| 571 |
)
|
| 572 |
|
|
@@ -574,6 +553,7 @@ Notes:
|
|
| 574 |
label="generation_backend",
|
| 575 |
choices=["demo_rows", "live_reinvent_cpu"],
|
| 576 |
value="demo_rows",
|
|
|
|
| 577 |
)
|
| 578 |
|
| 579 |
gen_num_molecules = gr.Number(
|
|
@@ -592,13 +572,14 @@ Notes:
|
|
| 592 |
label="docking_mode",
|
| 593 |
choices=["off", "top_k", "all"],
|
| 594 |
value="off",
|
|
|
|
| 595 |
)
|
| 596 |
|
| 597 |
-
gen_btn = gr.Button("Run
|
| 598 |
|
| 599 |
gen_summary = gr.JSON(label="Job summary")
|
| 600 |
-
gen_table = gr.Dataframe(label="Generated
|
| 601 |
-
gen_json = gr.JSON(label="
|
| 602 |
gen_file = gr.File(label="Download CSV")
|
| 603 |
|
| 604 |
gen_btn.click(
|
|
@@ -607,34 +588,41 @@ Notes:
|
|
| 607 |
outputs=[gen_summary, gen_table, gen_json, gen_file],
|
| 608 |
)
|
| 609 |
|
| 610 |
-
|
| 611 |
-
with gr.Tab("Example Generate Response"):
|
| 612 |
gr.Markdown(
|
| 613 |
"""
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
It shows the expected response shape after:
|
| 617 |
|
| 618 |
-
|
| 619 |
"""
|
| 620 |
)
|
| 621 |
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
|
|
|
| 629 |
)
|
| 630 |
|
| 631 |
-
|
| 632 |
-
gr.Markdown(
|
| 633 |
"""
|
| 634 |
-
|
| 635 |
|
| 636 |
-
|
| 637 |
|
|
|
|
|
|
|
|
|
|
| 638 |
- SDF
|
| 639 |
- MOL
|
| 640 |
- PDB
|
|
@@ -644,8 +632,8 @@ Important: this creates a 3D conformer from SMILES. It is not a docked pose insi
|
|
| 644 |
)
|
| 645 |
|
| 646 |
smiles_3d_input = gr.Textbox(
|
| 647 |
-
label="SMILES",
|
| 648 |
-
value="
|
| 649 |
lines=2,
|
| 650 |
)
|
| 651 |
|
|
@@ -664,44 +652,71 @@ Important: this creates a 3D conformer from SMILES. It is not a docked pose insi
|
|
| 664 |
inputs=[smiles_3d_input, format_input],
|
| 665 |
outputs=[ligand_file, ligand_meta],
|
| 666 |
)
|
| 667 |
-
|
| 668 |
-
with gr.Tab("
|
| 669 |
gr.Markdown(
|
| 670 |
"""
|
| 671 |
-
##
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
|
| 673 |
-
|
| 674 |
|
| 675 |
-
|
| 676 |
-
- top_k
|
| 677 |
-
- all
|
| 678 |
|
| 679 |
-
|
| 680 |
|
| 681 |
-
|
| 682 |
|
| 683 |
-
|
| 684 |
-
- docking_status: not_run
|
| 685 |
-
- docking_pose_file: null
|
| 686 |
|
| 687 |
-
|
|
|
|
|
|
|
| 688 |
|
| 689 |
-
|
| 690 |
-
-
|
| 691 |
-
-
|
| 692 |
-
-
|
| 693 |
-
- docking_error TEXT NULL
|
| 694 |
|
| 695 |
-
Recommended
|
| 696 |
|
| 697 |
-
-
|
| 698 |
-
-
|
| 699 |
-
-
|
| 700 |
-
-
|
|
|
|
| 701 |
"""
|
| 702 |
)
|
| 703 |
|
| 704 |
-
contract_btn = gr.Button("Load
|
| 705 |
contract_json = gr.JSON(label="backend_contract.json")
|
| 706 |
|
| 707 |
contract_btn.click(
|
|
@@ -709,21 +724,7 @@ Recommended frontend and filter behavior:
|
|
| 709 |
outputs=contract_json,
|
| 710 |
)
|
| 711 |
|
| 712 |
-
|
| 713 |
-
gr.Markdown(
|
| 714 |
-
"""
|
| 715 |
-
This tab loads the API response specification from the private artifact bundle.
|
| 716 |
-
|
| 717 |
-
It explains the expected backend/frontend response format for:
|
| 718 |
-
|
| 719 |
-
- generated molecules
|
| 720 |
-
- scored SMILES
|
| 721 |
-
- docking fields
|
| 722 |
-
- frontend filters
|
| 723 |
-
"""
|
| 724 |
-
)
|
| 725 |
-
|
| 726 |
-
spec_btn = gr.Button("Load API response spec")
|
| 727 |
spec_output = gr.Markdown(label="API_RESPONSE_SPEC.md")
|
| 728 |
|
| 729 |
spec_btn.click(
|
|
|
|
| 4 |
from pathlib import Path
|
| 5 |
import uuid
|
| 6 |
from datetime import datetime
|
| 7 |
+
|
| 8 |
import gradio as gr
|
| 9 |
import pandas as pd
|
| 10 |
from huggingface_hub import snapshot_download
|
|
|
|
| 12 |
from rdkit.Chem import Descriptors, Crippen, Lipinski, rdMolDescriptors, QED, AllChem
|
| 13 |
|
| 14 |
|
| 15 |
+
# Prefer setting these in Hugging Face Space settings:
|
| 16 |
+
# Secret: HF_TOKEN
|
| 17 |
+
# Variable: BUNDLE_REPO = Ailixir-AI-Team/egfr-ai-bundle
|
| 18 |
+
BUNDLE_REPO = os.getenv("BUNDLE_REPO", "Ailixir-AI-Team/egfr-ai-bundle")
|
| 19 |
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 20 |
|
| 21 |
CACHE_DIR = Path("bundle_cache")
|
|
|
|
| 26 |
|
| 27 |
|
| 28 |
def download_bundle_light():
|
| 29 |
+
"""Download only the light docs/examples/config needed by the Space."""
|
| 30 |
global EXAMPLE_JSON, API_SPEC_TEXT, BACKEND_CONTRACT_TEXT, BUNDLE_ROOT
|
| 31 |
|
| 32 |
try:
|
|
|
|
| 102 |
"docking_score": None,
|
| 103 |
"docking_status": "not_run",
|
| 104 |
"docking_pose_file": None,
|
| 105 |
+
"ligand_3d_file": None,
|
| 106 |
"note": "Invalid SMILES",
|
| 107 |
}
|
| 108 |
|
|
|
|
| 122 |
"docking_score": None,
|
| 123 |
"docking_status": "not_run",
|
| 124 |
"docking_pose_file": None,
|
| 125 |
+
"ligand_3d_file": None,
|
| 126 |
"note": "CPU demo: DeepPurpose and real docking are disabled in this Space.",
|
| 127 |
}
|
| 128 |
|
| 129 |
|
| 130 |
+
def add_ligand_ids_to_rows(rows, prefix="lig"):
|
| 131 |
+
output = []
|
| 132 |
+
for i, row in enumerate(rows, start=1):
|
| 133 |
+
clean = dict(row)
|
| 134 |
+
if not clean.get("ligand_id"):
|
| 135 |
+
clean["ligand_id"] = f"{prefix}_{i:04d}"
|
| 136 |
+
output.append(clean)
|
| 137 |
+
return output
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def add_ligand_ids_to_df(df: pd.DataFrame, prefix="lig"):
|
| 141 |
+
df = df.copy()
|
| 142 |
+
if "ligand_id" not in df.columns:
|
| 143 |
+
df.insert(0, "ligand_id", [f"{prefix}_{i:04d}" for i in range(1, len(df) + 1)])
|
| 144 |
+
return df
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def clean_records_from_df(df: pd.DataFrame):
|
| 148 |
+
if df is None or df.empty:
|
| 149 |
+
return []
|
| 150 |
+
clean_df = df.astype(object).where(pd.notnull(df), None)
|
| 151 |
+
return clean_df.to_dict(orient="records")
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
def write_results_csv(df: pd.DataFrame, filename: str = "score_smiles_results.csv"):
|
| 155 |
+
out_dir = Path("outputs")
|
| 156 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 157 |
+
out_path = out_dir / filename
|
| 158 |
+
df.to_csv(out_path, index=False)
|
| 159 |
+
return str(out_path)
|
| 160 |
+
|
| 161 |
+
|
| 162 |
def score_smiles(smiles_text):
|
| 163 |
smiles_list = []
|
| 164 |
for part in str(smiles_text).replace(",", "\n").splitlines():
|
|
|
|
| 170 |
return pd.DataFrame(), {
|
| 171 |
"status": "error",
|
| 172 |
"message": "Please enter at least one SMILES.",
|
| 173 |
+
}, None
|
| 174 |
|
| 175 |
+
rows = add_ligand_ids_to_rows([compute_row(s) for s in smiles_list])
|
| 176 |
df = pd.DataFrame(rows)
|
| 177 |
|
| 178 |
+
csv_path = write_results_csv(df)
|
| 179 |
response = {
|
| 180 |
"status": "completed",
|
| 181 |
"mode": "score_smiles_cpu_demo",
|
| 182 |
"docking_mode": "off",
|
| 183 |
"summary": {
|
| 184 |
"num_input": len(smiles_list),
|
| 185 |
+
"num_valid": int(df["valid"].sum()) if "valid" in df.columns else 0,
|
| 186 |
+
"num_returned": int(len(df)),
|
| 187 |
},
|
| 188 |
"columns": list(df.columns),
|
| 189 |
+
"results": clean_records_from_df(df),
|
| 190 |
+
"download_csv_file": csv_path,
|
| 191 |
"warnings": [
|
| 192 |
"This Space is a CPU demo.",
|
| 193 |
"DeepPurpose pred_pAff_mean is not computed in this Space version.",
|
|
|
|
| 195 |
"Outputs are computational predictions only.",
|
| 196 |
],
|
| 197 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
+
return df, response, csv_path
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
|
| 201 |
|
| 202 |
def get_api_spec():
|
|
|
|
| 215 |
"message": "backend_contract.json not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable.",
|
| 216 |
}
|
| 217 |
|
| 218 |
+
|
| 219 |
def _write_reinvent_sampling_config(job_id, num_molecules):
|
| 220 |
if BUNDLE_ROOT is None:
|
| 221 |
raise RuntimeError("Bundle root is not loaded. Check HF_TOKEN and BUNDLE_REPO.")
|
|
|
|
| 308 |
else:
|
| 309 |
row["NLL"] = None
|
| 310 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
rows.append(row)
|
| 312 |
|
| 313 |
+
rows = add_ligand_ids_to_rows(rows)
|
| 314 |
df = pd.DataFrame(rows)
|
| 315 |
|
| 316 |
preferred_cols = [
|
| 317 |
+
"ligand_id",
|
| 318 |
"SMILES",
|
| 319 |
"SMILES_state",
|
| 320 |
"NLL",
|
|
|
|
| 332 |
"docking_score",
|
| 333 |
"docking_status",
|
| 334 |
"docking_pose_file",
|
| 335 |
+
"ligand_3d_file",
|
| 336 |
"note",
|
| 337 |
]
|
| 338 |
|
|
|
|
| 350 |
"""
|
| 351 |
Generation endpoint for the Space.
|
| 352 |
|
| 353 |
+
demo_rows: uses packaged example rows for stable demo.
|
| 354 |
+
live_reinvent_cpu: tries to run real REINVENT CPU sampling if available.
|
| 355 |
"""
|
| 356 |
job_id = f"gen_{uuid.uuid4().hex[:8]}"
|
| 357 |
created_at = datetime.utcnow().isoformat() + "Z"
|
|
|
|
| 362 |
|
| 363 |
try:
|
| 364 |
if generation_backend == "live_reinvent_cpu":
|
| 365 |
+
raw_csv, _log_file = _run_reinvent_sampling(job_id, num_molecules)
|
| 366 |
df = _enrich_reinvent_csv(raw_csv, return_top_k)
|
| 367 |
mode = "live_reinvent_cpu_sampling"
|
| 368 |
status = "completed"
|
|
|
|
| 386 |
clean["docking_score"] = None
|
| 387 |
clean["docking_status"] = "not_run"
|
| 388 |
clean["docking_pose_file"] = None
|
| 389 |
+
clean["ligand_3d_file"] = clean.get("ligand_3d_file", None)
|
| 390 |
if "pred_pAff_mean" not in clean:
|
| 391 |
clean["pred_pAff_mean"] = None
|
| 392 |
clean_rows.append(clean)
|
| 393 |
|
| 394 |
+
clean_rows = add_ligand_ids_to_rows(clean_rows)
|
| 395 |
df = pd.DataFrame(clean_rows)
|
| 396 |
mode = "generate_job_cpu_demo"
|
| 397 |
status = "completed"
|
| 398 |
warnings.extend([
|
| 399 |
"This is a CPU demo generation response.",
|
| 400 |
+
"Live REINVENT generation was not executed because demo_rows was selected.",
|
| 401 |
"The response shape matches the planned backend contract.",
|
| 402 |
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 403 |
])
|
|
|
|
| 408 |
status = "failed"
|
| 409 |
warnings.append(str(e))
|
| 410 |
|
| 411 |
+
df = add_ligand_ids_to_df(df)
|
| 412 |
+
|
| 413 |
output_dir = Path("outputs")
|
| 414 |
output_dir.mkdir(exist_ok=True)
|
| 415 |
csv_path = output_dir / f"{job_id}_results.csv"
|
|
|
|
| 440 |
"num_returned": int(len(df)),
|
| 441 |
},
|
| 442 |
"columns": list(df.columns),
|
| 443 |
+
"results": clean_records_from_df(df),
|
| 444 |
"download_csv_file": str(csv_path),
|
| 445 |
"warnings": warnings,
|
| 446 |
}
|
|
|
|
| 448 |
return job_summary, df, response, str(csv_path)
|
| 449 |
|
| 450 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
def smiles_to_3d_file(smiles: str, file_format: str = "sdf"):
|
| 452 |
smiles = str(smiles).strip()
|
| 453 |
file_format = str(file_format).lower().strip()
|
|
|
|
| 515 |
|
| 516 |
return str(out_path), meta
|
| 517 |
|
| 518 |
+
|
| 519 |
loaded_ok, loaded_msg = download_bundle_light()
|
| 520 |
|
| 521 |
|
|
|
|
| 526 |
|
| 527 |
Private CPU demo for the EGFR AI backend workflow.
|
| 528 |
|
| 529 |
+
This demo focuses on the 3 things needed by filters, backend, and frontend:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
|
| 531 |
+
1. Generated ligands returned as a list of objects.
|
| 532 |
+
2. CSV download for generated/scored ligands.
|
| 533 |
+
3. SMILES to downloadable 3D ligand file.
|
| 534 |
|
| 535 |
+
Real AutoDock-GPU docking is disabled in this CPU Space, but docking fields are kept in the response schema.
|
|
|
|
|
|
|
|
|
|
| 536 |
"""
|
| 537 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
|
| 539 |
+
gr.Markdown(f"**Bundle load status:** {loaded_msg}")
|
| 540 |
|
| 541 |
+
with gr.Tab("Generate Ligands"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
gr.Markdown(
|
| 543 |
"""
|
| 544 |
+
## Generate Ligands
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
|
| 546 |
+
This tab returns generated ligands as `results`, a list of ligand objects.
|
| 547 |
|
| 548 |
+
Each ligand object contains properties, affinity/docking placeholders, and stable IDs for filters/frontend.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
"""
|
| 550 |
)
|
| 551 |
|
|
|
|
| 553 |
label="generation_backend",
|
| 554 |
choices=["demo_rows", "live_reinvent_cpu"],
|
| 555 |
value="demo_rows",
|
| 556 |
+
info="Use demo_rows for stable demo. live_reinvent_cpu tries REINVENT CPU sampling if available.",
|
| 557 |
)
|
| 558 |
|
| 559 |
gen_num_molecules = gr.Number(
|
|
|
|
| 572 |
label="docking_mode",
|
| 573 |
choices=["off", "top_k", "all"],
|
| 574 |
value="off",
|
| 575 |
+
info="CPU Space does not run real docking. This field is included for backend/frontend contract.",
|
| 576 |
)
|
| 577 |
|
| 578 |
+
gen_btn = gr.Button("Run Generate Job")
|
| 579 |
|
| 580 |
gen_summary = gr.JSON(label="Job summary")
|
| 581 |
+
gen_table = gr.Dataframe(label="Generated ligand objects table")
|
| 582 |
+
gen_json = gr.JSON(label="Backend-style JSON response")
|
| 583 |
gen_file = gr.File(label="Download CSV")
|
| 584 |
|
| 585 |
gen_btn.click(
|
|
|
|
| 588 |
outputs=[gen_summary, gen_table, gen_json, gen_file],
|
| 589 |
)
|
| 590 |
|
| 591 |
+
with gr.Tab("Score SMILES & 3D File"):
|
|
|
|
| 592 |
gr.Markdown(
|
| 593 |
"""
|
| 594 |
+
## Score SMILES
|
|
|
|
|
|
|
| 595 |
|
| 596 |
+
Paste one or more SMILES. The response returns a list of ligand objects and a downloadable CSV file.
|
| 597 |
"""
|
| 598 |
)
|
| 599 |
|
| 600 |
+
smiles_input = gr.Textbox(
|
| 601 |
+
label="SMILES input",
|
| 602 |
+
value="CCO\nc1ccccc1",
|
| 603 |
+
lines=5,
|
| 604 |
+
)
|
| 605 |
+
|
| 606 |
+
score_btn = gr.Button("Score SMILES")
|
| 607 |
+
score_table = gr.Dataframe(label="Ligand objects table")
|
| 608 |
+
score_json = gr.JSON(label="Backend-style JSON response")
|
| 609 |
+
score_csv = gr.File(label="Download CSV")
|
| 610 |
|
| 611 |
+
score_btn.click(
|
| 612 |
+
score_smiles,
|
| 613 |
+
inputs=smiles_input,
|
| 614 |
+
outputs=[score_table, score_json, score_csv],
|
| 615 |
)
|
| 616 |
|
| 617 |
+
gr.Markdown(
|
|
|
|
| 618 |
"""
|
| 619 |
+
---
|
| 620 |
|
| 621 |
+
## SMILES to 3D Ligand File
|
| 622 |
|
| 623 |
+
Send one SMILES and download a generated 3D ligand file.
|
| 624 |
+
|
| 625 |
+
Supported formats:
|
| 626 |
- SDF
|
| 627 |
- MOL
|
| 628 |
- PDB
|
|
|
|
| 632 |
)
|
| 633 |
|
| 634 |
smiles_3d_input = gr.Textbox(
|
| 635 |
+
label="SMILES for 3D file",
|
| 636 |
+
value="CNCCC(=O)Nc1ccc2nncc(-c3ccc4cncnc4c3)c2c1",
|
| 637 |
lines=2,
|
| 638 |
)
|
| 639 |
|
|
|
|
| 652 |
inputs=[smiles_3d_input, format_input],
|
| 653 |
outputs=[ligand_file, ligand_meta],
|
| 654 |
)
|
| 655 |
+
|
| 656 |
+
with gr.Tab("Contract & Notes"):
|
| 657 |
gr.Markdown(
|
| 658 |
"""
|
| 659 |
+
## Backend / Filters Contract
|
| 660 |
+
|
| 661 |
+
### Main response shape
|
| 662 |
+
|
| 663 |
+
The important part is:
|
| 664 |
+
|
| 665 |
+
```json
|
| 666 |
+
{
|
| 667 |
+
"results": [
|
| 668 |
+
{
|
| 669 |
+
"ligand_id": "lig_0001",
|
| 670 |
+
"SMILES": "...",
|
| 671 |
+
"canonical_smiles": "...",
|
| 672 |
+
"mw": 358.405,
|
| 673 |
+
"logp": 2.788,
|
| 674 |
+
"tpsa": 92.69,
|
| 675 |
+
"hbd": 2,
|
| 676 |
+
"hba": 6,
|
| 677 |
+
"rot_bonds": 5,
|
| 678 |
+
"qed": 0.5698,
|
| 679 |
+
"sa_score": 2.5204,
|
| 680 |
+
"pred_pAff_mean": null,
|
| 681 |
+
"docking_score": null,
|
| 682 |
+
"docking_status": "not_run",
|
| 683 |
+
"docking_pose_file": null,
|
| 684 |
+
"ligand_3d_file": null
|
| 685 |
+
}
|
| 686 |
+
]
|
| 687 |
+
}
|
| 688 |
+
```
|
| 689 |
|
| 690 |
+
### CSV
|
| 691 |
|
| 692 |
+
Each Generate or Score action returns a downloadable CSV file.
|
|
|
|
|
|
|
| 693 |
|
| 694 |
+
### 3D ligand file
|
| 695 |
|
| 696 |
+
The SMILES to 3D tool returns a downloadable SDF/MOL/PDB file.
|
| 697 |
|
| 698 |
+
### Docking modes in the full backend
|
|
|
|
|
|
|
| 699 |
|
| 700 |
+
- `off`
|
| 701 |
+
- `top_k`
|
| 702 |
+
- `all`
|
| 703 |
|
| 704 |
+
In this CPU Space:
|
| 705 |
+
- `docking_score = null`
|
| 706 |
+
- `docking_status = not_run`
|
| 707 |
+
- `docking_pose_file = null`
|
|
|
|
| 708 |
|
| 709 |
+
### Recommended DB columns
|
| 710 |
|
| 711 |
+
- `docking_score FLOAT NULL`
|
| 712 |
+
- `docking_status VARCHAR(30) DEFAULT 'not_run'`
|
| 713 |
+
- `docking_pose_file TEXT NULL`
|
| 714 |
+
- `docking_mode VARCHAR(20) DEFAULT 'off'`
|
| 715 |
+
- `docking_error TEXT NULL`
|
| 716 |
"""
|
| 717 |
)
|
| 718 |
|
| 719 |
+
contract_btn = gr.Button("Load backend_contract.json")
|
| 720 |
contract_json = gr.JSON(label="backend_contract.json")
|
| 721 |
|
| 722 |
contract_btn.click(
|
|
|
|
| 724 |
outputs=contract_json,
|
| 725 |
)
|
| 726 |
|
| 727 |
+
spec_btn = gr.Button("Load API_RESPONSE_SPEC.md")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
spec_output = gr.Markdown(label="API_RESPONSE_SPEC.md")
|
| 729 |
|
| 730 |
spec_btn.click(
|
app.py.backup_before_final_ui
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import subprocess
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import uuid
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
import gradio as gr
|
| 8 |
+
import pandas as pd
|
| 9 |
+
from huggingface_hub import snapshot_download
|
| 10 |
+
from rdkit import Chem
|
| 11 |
+
from rdkit.Chem import Descriptors, Crippen, Lipinski, rdMolDescriptors, QED, AllChem
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
BUNDLE_REPO = os.getenv("BUNDLE_REPO", "abdullahsamra/egfr-ai-bundle")
|
| 15 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 16 |
+
|
| 17 |
+
CACHE_DIR = Path("bundle_cache")
|
| 18 |
+
EXAMPLE_JSON = None
|
| 19 |
+
API_SPEC_TEXT = None
|
| 20 |
+
BACKEND_CONTRACT_TEXT = None
|
| 21 |
+
BUNDLE_ROOT = None
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def download_bundle_light():
|
| 25 |
+
global EXAMPLE_JSON, API_SPEC_TEXT, BACKEND_CONTRACT_TEXT, BUNDLE_ROOT
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
local_dir = snapshot_download(
|
| 29 |
+
repo_id=BUNDLE_REPO,
|
| 30 |
+
repo_type="dataset",
|
| 31 |
+
token=HF_TOKEN,
|
| 32 |
+
allow_patterns=[
|
| 33 |
+
"examples/generate_response.example.json",
|
| 34 |
+
"docs/API_RESPONSE_SPEC.md",
|
| 35 |
+
"configs/backend_contract.json",
|
| 36 |
+
"configs/sampling_model.bundle.toml",
|
| 37 |
+
"models/generator/egfr_generator.chkpt",
|
| 38 |
+
"README.md",
|
| 39 |
+
],
|
| 40 |
+
local_dir=str(CACHE_DIR),
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
root = Path(local_dir)
|
| 44 |
+
BUNDLE_ROOT = root
|
| 45 |
+
|
| 46 |
+
example_path = root / "examples" / "generate_response.example.json"
|
| 47 |
+
spec_path = root / "docs" / "API_RESPONSE_SPEC.md"
|
| 48 |
+
contract_path = root / "configs" / "backend_contract.json"
|
| 49 |
+
|
| 50 |
+
if example_path.exists():
|
| 51 |
+
EXAMPLE_JSON = json.loads(example_path.read_text(encoding="utf-8"))
|
| 52 |
+
|
| 53 |
+
if spec_path.exists():
|
| 54 |
+
API_SPEC_TEXT = spec_path.read_text(encoding="utf-8")
|
| 55 |
+
|
| 56 |
+
if contract_path.exists():
|
| 57 |
+
BACKEND_CONTRACT_TEXT = contract_path.read_text(encoding="utf-8")
|
| 58 |
+
|
| 59 |
+
return True, f"Loaded docs/examples from {BUNDLE_REPO}"
|
| 60 |
+
|
| 61 |
+
except Exception as e:
|
| 62 |
+
return False, f"Could not load private bundle. Check HF_TOKEN secret and BUNDLE_REPO variable. Error: {e}"
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def canonicalize(smiles: str):
|
| 66 |
+
mol = Chem.MolFromSmiles(str(smiles).strip())
|
| 67 |
+
if mol is None:
|
| 68 |
+
return None, None
|
| 69 |
+
return mol, Chem.MolToSmiles(mol, isomericSmiles=True)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def try_sa_score(mol):
|
| 73 |
+
try:
|
| 74 |
+
from rdkit.Contrib.SA_Score import sascorer
|
| 75 |
+
return round(float(sascorer.calculateScore(mol)), 4)
|
| 76 |
+
except Exception:
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def compute_row(smiles: str):
|
| 81 |
+
mol, can = canonicalize(smiles)
|
| 82 |
+
|
| 83 |
+
if mol is None:
|
| 84 |
+
return {
|
| 85 |
+
"SMILES": smiles,
|
| 86 |
+
"valid": False,
|
| 87 |
+
"canonical_smiles": None,
|
| 88 |
+
"mw": None,
|
| 89 |
+
"logp": None,
|
| 90 |
+
"tpsa": None,
|
| 91 |
+
"hbd": None,
|
| 92 |
+
"hba": None,
|
| 93 |
+
"rot_bonds": None,
|
| 94 |
+
"qed": None,
|
| 95 |
+
"sa_score": None,
|
| 96 |
+
"pred_pAff_mean": None,
|
| 97 |
+
"docking_score": None,
|
| 98 |
+
"docking_status": "not_run",
|
| 99 |
+
"docking_pose_file": None,
|
| 100 |
+
"note": "Invalid SMILES",
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
return {
|
| 104 |
+
"SMILES": smiles,
|
| 105 |
+
"valid": True,
|
| 106 |
+
"canonical_smiles": can,
|
| 107 |
+
"mw": round(float(Descriptors.MolWt(mol)), 4),
|
| 108 |
+
"logp": round(float(Crippen.MolLogP(mol)), 4),
|
| 109 |
+
"tpsa": round(float(rdMolDescriptors.CalcTPSA(mol)), 4),
|
| 110 |
+
"hbd": int(Lipinski.NumHDonors(mol)),
|
| 111 |
+
"hba": int(Lipinski.NumHAcceptors(mol)),
|
| 112 |
+
"rot_bonds": int(Lipinski.NumRotatableBonds(mol)),
|
| 113 |
+
"qed": round(float(QED.qed(mol)), 4),
|
| 114 |
+
"sa_score": try_sa_score(mol),
|
| 115 |
+
"pred_pAff_mean": None,
|
| 116 |
+
"docking_score": None,
|
| 117 |
+
"docking_status": "not_run",
|
| 118 |
+
"docking_pose_file": None,
|
| 119 |
+
"note": "CPU demo: DeepPurpose and real docking are disabled in this Space.",
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def add_ligand_ids_to_rows(rows, prefix="lig"):
|
| 125 |
+
output = []
|
| 126 |
+
for i, row in enumerate(rows, start=1):
|
| 127 |
+
clean = dict(row)
|
| 128 |
+
if not clean.get("ligand_id"):
|
| 129 |
+
clean["ligand_id"] = f"{prefix}_{i:04d}"
|
| 130 |
+
output.append(clean)
|
| 131 |
+
return output
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def add_ligand_ids_to_df(df, prefix="lig"):
|
| 135 |
+
df = df.copy()
|
| 136 |
+
if "ligand_id" not in df.columns:
|
| 137 |
+
df.insert(0, "ligand_id", [f"{prefix}_{i:04d}" for i in range(1, len(df) + 1)])
|
| 138 |
+
return df
|
| 139 |
+
|
| 140 |
+
def score_smiles(smiles_text):
|
| 141 |
+
smiles_list = []
|
| 142 |
+
for part in str(smiles_text).replace(",", "\n").splitlines():
|
| 143 |
+
part = part.strip()
|
| 144 |
+
if part:
|
| 145 |
+
smiles_list.append(part)
|
| 146 |
+
|
| 147 |
+
if not smiles_list:
|
| 148 |
+
return pd.DataFrame(), {
|
| 149 |
+
"status": "error",
|
| 150 |
+
"message": "Please enter at least one SMILES.",
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
rows = add_ligand_ids_to_rows([compute_row(s) for s in smiles_list])
|
| 154 |
+
df = pd.DataFrame(rows)
|
| 155 |
+
|
| 156 |
+
response = {
|
| 157 |
+
"status": "completed",
|
| 158 |
+
"mode": "score_smiles_cpu_demo",
|
| 159 |
+
"docking_mode": "off",
|
| 160 |
+
"summary": {
|
| 161 |
+
"num_input": len(smiles_list),
|
| 162 |
+
"num_valid": int(df["valid"].sum()),
|
| 163 |
+
"num_returned": len(df),
|
| 164 |
+
},
|
| 165 |
+
"columns": list(df.columns),
|
| 166 |
+
"results": rows,
|
| 167 |
+
"warnings": [
|
| 168 |
+
"This Space is a CPU demo.",
|
| 169 |
+
"DeepPurpose pred_pAff_mean is not computed in this Space version.",
|
| 170 |
+
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 171 |
+
"Outputs are computational predictions only.",
|
| 172 |
+
],
|
| 173 |
+
}
|
| 174 |
+
csv_path = write_results_csv(df)
|
| 175 |
+
response["download_csv_file"] = csv_path
|
| 176 |
+
return df, response, csv_path
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def get_example_table():
|
| 180 |
+
if EXAMPLE_JSON and "results" in EXAMPLE_JSON:
|
| 181 |
+
return pd.DataFrame(EXAMPLE_JSON["results"])
|
| 182 |
+
return pd.DataFrame([
|
| 183 |
+
{"message": "Example response not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable."}
|
| 184 |
+
])
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def get_example_json():
|
| 188 |
+
if EXAMPLE_JSON:
|
| 189 |
+
return EXAMPLE_JSON
|
| 190 |
+
return {
|
| 191 |
+
"status": "not_loaded",
|
| 192 |
+
"message": "Example response not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable.",
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def get_api_spec():
|
| 197 |
+
return API_SPEC_TEXT or "API_RESPONSE_SPEC.md not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable."
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def get_contract():
|
| 201 |
+
if BACKEND_CONTRACT_TEXT:
|
| 202 |
+
try:
|
| 203 |
+
return json.loads(BACKEND_CONTRACT_TEXT)
|
| 204 |
+
except Exception:
|
| 205 |
+
return {"raw": BACKEND_CONTRACT_TEXT}
|
| 206 |
+
|
| 207 |
+
return {
|
| 208 |
+
"status": "not_loaded",
|
| 209 |
+
"message": "backend_contract.json not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable.",
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
def _write_reinvent_sampling_config(job_id, num_molecules):
|
| 213 |
+
if BUNDLE_ROOT is None:
|
| 214 |
+
raise RuntimeError("Bundle root is not loaded. Check HF_TOKEN and BUNDLE_REPO.")
|
| 215 |
+
|
| 216 |
+
checkpoint = Path(BUNDLE_ROOT) / "models" / "generator" / "egfr_generator.chkpt"
|
| 217 |
+
if not checkpoint.exists():
|
| 218 |
+
raise FileNotFoundError(f"Generator checkpoint not found: {checkpoint}")
|
| 219 |
+
|
| 220 |
+
output_dir = Path("outputs")
|
| 221 |
+
output_dir.mkdir(exist_ok=True)
|
| 222 |
+
|
| 223 |
+
raw_csv = output_dir / f"{job_id}_raw_sampling.csv"
|
| 224 |
+
log_file = output_dir / f"{job_id}_reinvent.log"
|
| 225 |
+
resolved_json = output_dir / f"{job_id}_sampling_resolved.json"
|
| 226 |
+
config_path = output_dir / f"{job_id}_sampling.toml"
|
| 227 |
+
|
| 228 |
+
config_text = f"""
|
| 229 |
+
run_type = "sampling"
|
| 230 |
+
device = "cpu"
|
| 231 |
+
|
| 232 |
+
json_out_config = "{resolved_json.resolve()}"
|
| 233 |
+
|
| 234 |
+
[parameters]
|
| 235 |
+
model_file = "{checkpoint.resolve()}"
|
| 236 |
+
output_file = "{raw_csv.resolve()}"
|
| 237 |
+
|
| 238 |
+
num_smiles = {int(num_molecules)}
|
| 239 |
+
unique_molecules = true
|
| 240 |
+
randomize_smiles = true
|
| 241 |
+
"""
|
| 242 |
+
|
| 243 |
+
config_path.write_text(config_text.strip() + "\n", encoding="utf-8")
|
| 244 |
+
return config_path, raw_csv, log_file
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def _run_reinvent_sampling(job_id, num_molecules):
|
| 248 |
+
config_path, raw_csv, log_file = _write_reinvent_sampling_config(job_id, num_molecules)
|
| 249 |
+
|
| 250 |
+
cmd = [
|
| 251 |
+
"reinvent",
|
| 252 |
+
"-l",
|
| 253 |
+
str(log_file.resolve()),
|
| 254 |
+
str(config_path.resolve()),
|
| 255 |
+
]
|
| 256 |
+
|
| 257 |
+
proc = subprocess.run(
|
| 258 |
+
cmd,
|
| 259 |
+
text=True,
|
| 260 |
+
stdout=subprocess.PIPE,
|
| 261 |
+
stderr=subprocess.STDOUT,
|
| 262 |
+
timeout=300,
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
if proc.returncode != 0:
|
| 266 |
+
raise RuntimeError(
|
| 267 |
+
"REINVENT sampling failed.\n"
|
| 268 |
+
f"Command: {' '.join(cmd)}\n"
|
| 269 |
+
f"Output:\n{proc.stdout[-4000:]}"
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
if not raw_csv.exists():
|
| 273 |
+
raise RuntimeError(f"REINVENT finished but output CSV was not found: {raw_csv}")
|
| 274 |
+
|
| 275 |
+
return raw_csv, log_file
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def _enrich_reinvent_csv(raw_csv, return_top_k):
|
| 279 |
+
raw_df = pd.read_csv(raw_csv)
|
| 280 |
+
|
| 281 |
+
if "SMILES" not in raw_df.columns:
|
| 282 |
+
raise ValueError(f"REINVENT output missing SMILES column. Columns: {list(raw_df.columns)}")
|
| 283 |
+
|
| 284 |
+
rows = []
|
| 285 |
+
for _, r in raw_df.iterrows():
|
| 286 |
+
row = compute_row(str(r["SMILES"]))
|
| 287 |
+
|
| 288 |
+
if "SMILES_state" in raw_df.columns:
|
| 289 |
+
try:
|
| 290 |
+
row["SMILES_state"] = int(r["SMILES_state"])
|
| 291 |
+
except Exception:
|
| 292 |
+
row["SMILES_state"] = None
|
| 293 |
+
else:
|
| 294 |
+
row["SMILES_state"] = None
|
| 295 |
+
|
| 296 |
+
if "NLL" in raw_df.columns:
|
| 297 |
+
try:
|
| 298 |
+
row["NLL"] = float(r["NLL"])
|
| 299 |
+
except Exception:
|
| 300 |
+
row["NLL"] = None
|
| 301 |
+
else:
|
| 302 |
+
row["NLL"] = None
|
| 303 |
+
|
| 304 |
+
# Keep these stable for frontend/backend filters.
|
| 305 |
+
row["pred_pAff_mean"] = None
|
| 306 |
+
row["docking_score"] = None
|
| 307 |
+
row["docking_status"] = "not_run"
|
| 308 |
+
row["docking_pose_file"] = None
|
| 309 |
+
|
| 310 |
+
rows.append(row)
|
| 311 |
+
|
| 312 |
+
df = pd.DataFrame(rows)
|
| 313 |
+
|
| 314 |
+
preferred_cols = [
|
| 315 |
+
"SMILES",
|
| 316 |
+
"SMILES_state",
|
| 317 |
+
"NLL",
|
| 318 |
+
"valid",
|
| 319 |
+
"canonical_smiles",
|
| 320 |
+
"mw",
|
| 321 |
+
"logp",
|
| 322 |
+
"tpsa",
|
| 323 |
+
"hbd",
|
| 324 |
+
"hba",
|
| 325 |
+
"rot_bonds",
|
| 326 |
+
"qed",
|
| 327 |
+
"sa_score",
|
| 328 |
+
"pred_pAff_mean",
|
| 329 |
+
"docking_score",
|
| 330 |
+
"docking_status",
|
| 331 |
+
"docking_pose_file",
|
| 332 |
+
"note",
|
| 333 |
+
]
|
| 334 |
+
|
| 335 |
+
cols = [c for c in preferred_cols if c in df.columns] + [c for c in df.columns if c not in preferred_cols]
|
| 336 |
+
df = df[cols]
|
| 337 |
+
|
| 338 |
+
return_top_k = int(return_top_k)
|
| 339 |
+
if return_top_k > 0:
|
| 340 |
+
df = df.head(return_top_k)
|
| 341 |
+
|
| 342 |
+
return df
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def generate_job_demo(num_molecules, return_top_k, docking_mode, generation_backend):
|
| 346 |
+
"""
|
| 347 |
+
Generation endpoint for the Space.
|
| 348 |
+
|
| 349 |
+
Demo mode uses packaged example rows.
|
| 350 |
+
Live mode tries to run REINVENT CPU sampling from the packaged checkpoint.
|
| 351 |
+
"""
|
| 352 |
+
job_id = f"gen_{uuid.uuid4().hex[:8]}"
|
| 353 |
+
created_at = datetime.utcnow().isoformat() + "Z"
|
| 354 |
+
num_molecules = int(num_molecules)
|
| 355 |
+
return_top_k = int(return_top_k)
|
| 356 |
+
|
| 357 |
+
warnings = []
|
| 358 |
+
|
| 359 |
+
try:
|
| 360 |
+
if generation_backend == "live_reinvent_cpu":
|
| 361 |
+
raw_csv, log_file = _run_reinvent_sampling(job_id, num_molecules)
|
| 362 |
+
df = _enrich_reinvent_csv(raw_csv, return_top_k)
|
| 363 |
+
mode = "live_reinvent_cpu_sampling"
|
| 364 |
+
status = "completed"
|
| 365 |
+
warnings.extend([
|
| 366 |
+
"Live REINVENT CPU sampling was executed in this Space.",
|
| 367 |
+
"DeepPurpose scoring is not enabled in this Space version; pred_pAff_mean is null.",
|
| 368 |
+
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 369 |
+
])
|
| 370 |
+
|
| 371 |
+
else:
|
| 372 |
+
if EXAMPLE_JSON and "results" in EXAMPLE_JSON:
|
| 373 |
+
rows = EXAMPLE_JSON["results"]
|
| 374 |
+
else:
|
| 375 |
+
rows = [compute_row("CCO"), compute_row("c1ccccc1")]
|
| 376 |
+
|
| 377 |
+
selected = rows[: max(1, min(return_top_k, len(rows)))]
|
| 378 |
+
|
| 379 |
+
clean_rows = []
|
| 380 |
+
for row in selected:
|
| 381 |
+
clean = dict(row)
|
| 382 |
+
clean["docking_score"] = None
|
| 383 |
+
clean["docking_status"] = "not_run"
|
| 384 |
+
clean["docking_pose_file"] = None
|
| 385 |
+
if "pred_pAff_mean" not in clean:
|
| 386 |
+
clean["pred_pAff_mean"] = None
|
| 387 |
+
clean_rows.append(clean)
|
| 388 |
+
|
| 389 |
+
df = pd.DataFrame(clean_rows)
|
| 390 |
+
mode = "generate_job_cpu_demo"
|
| 391 |
+
status = "completed"
|
| 392 |
+
warnings.extend([
|
| 393 |
+
"This is a CPU demo generation response.",
|
| 394 |
+
"Live REINVENT generation was not executed because demo mode was selected.",
|
| 395 |
+
"The response shape matches the planned backend contract.",
|
| 396 |
+
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 397 |
+
])
|
| 398 |
+
|
| 399 |
+
except Exception as e:
|
| 400 |
+
df = pd.DataFrame()
|
| 401 |
+
mode = "live_reinvent_cpu_sampling"
|
| 402 |
+
status = "failed"
|
| 403 |
+
warnings.append(str(e))
|
| 404 |
+
|
| 405 |
+
df = add_ligand_ids_to_df(df)
|
| 406 |
+
|
| 407 |
+
output_dir = Path("outputs")
|
| 408 |
+
output_dir.mkdir(exist_ok=True)
|
| 409 |
+
csv_path = output_dir / f"{job_id}_results.csv"
|
| 410 |
+
df.to_csv(csv_path, index=False)
|
| 411 |
+
|
| 412 |
+
job_summary = {
|
| 413 |
+
"job_id": job_id,
|
| 414 |
+
"status": status,
|
| 415 |
+
"mode": mode,
|
| 416 |
+
"created_at": created_at,
|
| 417 |
+
"preset": "egfr_generator",
|
| 418 |
+
"docking_mode": docking_mode,
|
| 419 |
+
"num_requested": num_molecules,
|
| 420 |
+
"num_returned": int(len(df)),
|
| 421 |
+
"download_csv_file": str(csv_path),
|
| 422 |
+
"live_generation": generation_backend == "live_reinvent_cpu",
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
response = {
|
| 426 |
+
"job_id": job_id,
|
| 427 |
+
"status": status,
|
| 428 |
+
"mode": mode,
|
| 429 |
+
"created_at": created_at,
|
| 430 |
+
"preset": "egfr_generator",
|
| 431 |
+
"docking_mode": docking_mode,
|
| 432 |
+
"summary": {
|
| 433 |
+
"num_requested": num_molecules,
|
| 434 |
+
"num_returned": int(len(df)),
|
| 435 |
+
},
|
| 436 |
+
"columns": list(df.columns),
|
| 437 |
+
"results": df.to_dict(orient="records"),
|
| 438 |
+
"download_csv_file": str(csv_path),
|
| 439 |
+
"warnings": warnings,
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
return job_summary, df, response, str(csv_path)
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
def write_results_csv(df: pd.DataFrame, filename: str = "score_smiles_results.csv"):
|
| 446 |
+
out_dir = Path("outputs")
|
| 447 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 448 |
+
out_path = out_dir / filename
|
| 449 |
+
df.to_csv(out_path, index=False)
|
| 450 |
+
return str(out_path)
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def smiles_to_3d_file(smiles: str, file_format: str = "sdf"):
|
| 454 |
+
smiles = str(smiles).strip()
|
| 455 |
+
file_format = str(file_format).lower().strip()
|
| 456 |
+
|
| 457 |
+
if file_format not in ["sdf", "mol", "pdb"]:
|
| 458 |
+
return None, {
|
| 459 |
+
"status": "error",
|
| 460 |
+
"message": "Unsupported format. Choose sdf, mol, or pdb.",
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
mol = Chem.MolFromSmiles(smiles)
|
| 464 |
+
if mol is None:
|
| 465 |
+
return None, {
|
| 466 |
+
"status": "error",
|
| 467 |
+
"input_smiles": smiles,
|
| 468 |
+
"message": "Invalid SMILES.",
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
canonical_smiles = Chem.MolToSmiles(mol, isomericSmiles=True)
|
| 472 |
+
|
| 473 |
+
mol = Chem.AddHs(mol)
|
| 474 |
+
|
| 475 |
+
params = AllChem.ETKDGv3()
|
| 476 |
+
params.randomSeed = 42
|
| 477 |
+
|
| 478 |
+
embed_status = AllChem.EmbedMolecule(mol, params)
|
| 479 |
+
if embed_status != 0:
|
| 480 |
+
return None, {
|
| 481 |
+
"status": "error",
|
| 482 |
+
"input_smiles": smiles,
|
| 483 |
+
"canonical_smiles": canonical_smiles,
|
| 484 |
+
"message": "3D embedding failed.",
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
try:
|
| 488 |
+
AllChem.UFFOptimizeMolecule(mol, maxIters=500)
|
| 489 |
+
optimization_status = "UFF optimized"
|
| 490 |
+
except Exception as e:
|
| 491 |
+
optimization_status = f"UFF optimization failed, using embedded geometry only: {e}"
|
| 492 |
+
|
| 493 |
+
out_dir = Path("outputs/ligands")
|
| 494 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 495 |
+
|
| 496 |
+
safe_name = "ligand_3d"
|
| 497 |
+
out_path = out_dir / f"{safe_name}.{file_format}"
|
| 498 |
+
|
| 499 |
+
if file_format == "sdf":
|
| 500 |
+
writer = Chem.SDWriter(str(out_path))
|
| 501 |
+
writer.write(mol)
|
| 502 |
+
writer.close()
|
| 503 |
+
elif file_format == "mol":
|
| 504 |
+
Chem.MolToMolFile(mol, str(out_path))
|
| 505 |
+
elif file_format == "pdb":
|
| 506 |
+
Chem.MolToPDBFile(mol, str(out_path))
|
| 507 |
+
|
| 508 |
+
meta = {
|
| 509 |
+
"status": "completed",
|
| 510 |
+
"input_smiles": smiles,
|
| 511 |
+
"canonical_smiles": canonical_smiles,
|
| 512 |
+
"file_format": file_format,
|
| 513 |
+
"file_path": str(out_path),
|
| 514 |
+
"optimization_status": optimization_status,
|
| 515 |
+
"note": "This is a generated 3D conformer from SMILES, not a docked protein-ligand pose.",
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
return str(out_path), meta
|
| 519 |
+
|
| 520 |
+
loaded_ok, loaded_msg = download_bundle_light()
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
with gr.Blocks(title="EGFR AI Demo") as demo:
|
| 524 |
+
gr.Markdown(
|
| 525 |
+
"""
|
| 526 |
+
# EGFR AI Demo
|
| 527 |
+
|
| 528 |
+
Private CPU demo for the EGFR AI backend workflow.
|
| 529 |
+
|
| 530 |
+
This demo shows the backend response shape for generation, scoring, filtering, and docking integration.
|
| 531 |
+
|
| 532 |
+
Real AutoDock-GPU docking is disabled in this CPU Space, but docking fields are present so backend/frontend teams can implement database columns and filters.
|
| 533 |
+
"""
|
| 534 |
+
)
|
| 535 |
+
|
| 536 |
+
gr.Markdown(f"Bundle load status: {loaded_msg}")
|
| 537 |
+
|
| 538 |
+
with gr.Tab("Score SMILES"):
|
| 539 |
+
gr.Markdown(
|
| 540 |
+
"""
|
| 541 |
+
Enter one or more SMILES.
|
| 542 |
+
|
| 543 |
+
This CPU demo calculates molecular properties and returns the same response columns expected by the backend.
|
| 544 |
+
|
| 545 |
+
Important:
|
| 546 |
+
- pred_pAff_mean is an integration field in this Space version.
|
| 547 |
+
- real docking is disabled in this CPU Space.
|
| 548 |
+
- docking_status is returned as not_run.
|
| 549 |
+
"""
|
| 550 |
+
)
|
| 551 |
+
|
| 552 |
+
smiles_input = gr.Textbox(
|
| 553 |
+
label="SMILES input",
|
| 554 |
+
value="CCO\nc1ccccc1",
|
| 555 |
+
lines=5,
|
| 556 |
+
)
|
| 557 |
+
|
| 558 |
+
score_btn = gr.Button("Score SMILES")
|
| 559 |
+
score_table = gr.Dataframe(label="Results table")
|
| 560 |
+
score_json = gr.JSON(label="Backend-style JSON response")
|
| 561 |
+
score_csv = gr.File(label="Download CSV")
|
| 562 |
+
|
| 563 |
+
|
| 564 |
+
score_btn.click(
|
| 565 |
+
score_smiles,
|
| 566 |
+
inputs=smiles_input,
|
| 567 |
+
outputs=[score_table, score_json, score_csv],
|
| 568 |
+
)
|
| 569 |
+
with gr.Tab("Generate Job Demo"):
|
| 570 |
+
gr.Markdown(
|
| 571 |
+
"""
|
| 572 |
+
This tab demonstrates generation job behavior.
|
| 573 |
+
|
| 574 |
+
Generation backend options:
|
| 575 |
+
|
| 576 |
+
- demo_rows: uses packaged example rows. Fast and stable.
|
| 577 |
+
- live_reinvent_cpu: tries to run real REINVENT sampling from the packaged checkpoint on CPU.
|
| 578 |
+
|
| 579 |
+
The output is split into:
|
| 580 |
+
|
| 581 |
+
1. Job summary: job_id, status, preset, mode, and metadata.
|
| 582 |
+
2. Generated molecule results table.
|
| 583 |
+
3. Full backend-style generation JSON.
|
| 584 |
+
4. Download CSV.
|
| 585 |
+
|
| 586 |
+
Notes:
|
| 587 |
+
- DeepPurpose live scoring is not enabled yet in this Space version.
|
| 588 |
+
- Real docking is disabled in this CPU Space, so docking_status is returned as not_run.
|
| 589 |
+
"""
|
| 590 |
+
)
|
| 591 |
+
|
| 592 |
+
generation_backend = gr.Dropdown(
|
| 593 |
+
label="generation_backend",
|
| 594 |
+
choices=["demo_rows", "live_reinvent_cpu"],
|
| 595 |
+
value="demo_rows",
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
gen_num_molecules = gr.Number(
|
| 599 |
+
label="num_molecules",
|
| 600 |
+
value=20,
|
| 601 |
+
precision=0,
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
gen_return_top_k = gr.Number(
|
| 605 |
+
label="return_top_k",
|
| 606 |
+
value=20,
|
| 607 |
+
precision=0,
|
| 608 |
+
)
|
| 609 |
+
|
| 610 |
+
gen_docking_mode = gr.Dropdown(
|
| 611 |
+
label="docking_mode",
|
| 612 |
+
choices=["off", "top_k", "all"],
|
| 613 |
+
value="off",
|
| 614 |
+
)
|
| 615 |
+
|
| 616 |
+
gen_btn = gr.Button("Run Generation Job")
|
| 617 |
+
|
| 618 |
+
gen_summary = gr.JSON(label="Job summary")
|
| 619 |
+
gen_table = gr.Dataframe(label="Generated molecule results table")
|
| 620 |
+
gen_json = gr.JSON(label="Full backend-style generation JSON")
|
| 621 |
+
gen_file = gr.File(label="Download CSV")
|
| 622 |
+
|
| 623 |
+
gen_btn.click(
|
| 624 |
+
generate_job_demo,
|
| 625 |
+
inputs=[gen_num_molecules, gen_return_top_k, gen_docking_mode, generation_backend],
|
| 626 |
+
outputs=[gen_summary, gen_table, gen_json, gen_file],
|
| 627 |
+
)
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
with gr.Tab("Example Generate Response"):
|
| 631 |
+
gr.Markdown(
|
| 632 |
+
"""
|
| 633 |
+
This example is loaded from the private Hugging Face Dataset bundle.
|
| 634 |
+
|
| 635 |
+
It shows the expected response shape after:
|
| 636 |
+
|
| 637 |
+
Generate -> Enrich -> Optional Docking
|
| 638 |
+
"""
|
| 639 |
+
)
|
| 640 |
+
|
| 641 |
+
example_btn = gr.Button("Load example response")
|
| 642 |
+
example_table = gr.Dataframe(label="Example results table")
|
| 643 |
+
example_json = gr.JSON(label="Example JSON response")
|
| 644 |
+
|
| 645 |
+
example_btn.click(
|
| 646 |
+
lambda: (get_example_table(), get_example_json()),
|
| 647 |
+
outputs=[example_table, example_json],
|
| 648 |
+
)
|
| 649 |
+
|
| 650 |
+
with gr.Tab("SMILES to 3D File"):
|
| 651 |
+
gr.Markdown(
|
| 652 |
+
"""
|
| 653 |
+
Convert a SMILES string into a downloadable 3D ligand file.
|
| 654 |
+
|
| 655 |
+
Supported formats:
|
| 656 |
+
|
| 657 |
+
- SDF
|
| 658 |
+
- MOL
|
| 659 |
+
- PDB
|
| 660 |
+
|
| 661 |
+
Important: this creates a 3D conformer from SMILES. It is not a docked pose inside the EGFR binding pocket.
|
| 662 |
+
"""
|
| 663 |
+
)
|
| 664 |
+
|
| 665 |
+
smiles_3d_input = gr.Textbox(
|
| 666 |
+
label="SMILES",
|
| 667 |
+
value="CCO",
|
| 668 |
+
lines=2,
|
| 669 |
+
)
|
| 670 |
+
|
| 671 |
+
format_input = gr.Radio(
|
| 672 |
+
choices=["sdf", "mol", "pdb"],
|
| 673 |
+
value="sdf",
|
| 674 |
+
label="Output file format",
|
| 675 |
+
)
|
| 676 |
+
|
| 677 |
+
make_3d_btn = gr.Button("Generate 3D ligand file")
|
| 678 |
+
ligand_file = gr.File(label="Download 3D ligand file")
|
| 679 |
+
ligand_meta = gr.JSON(label="3D file metadata")
|
| 680 |
+
|
| 681 |
+
make_3d_btn.click(
|
| 682 |
+
smiles_to_3d_file,
|
| 683 |
+
inputs=[smiles_3d_input, format_input],
|
| 684 |
+
outputs=[ligand_file, ligand_meta],
|
| 685 |
+
)
|
| 686 |
+
|
| 687 |
+
with gr.Tab("Docking Contract"):
|
| 688 |
+
gr.Markdown(
|
| 689 |
+
"""
|
| 690 |
+
## Docking modes
|
| 691 |
+
|
| 692 |
+
The full backend supports:
|
| 693 |
+
|
| 694 |
+
- off
|
| 695 |
+
- top_k
|
| 696 |
+
- all
|
| 697 |
+
|
| 698 |
+
In this CPU Space, real AutoDock-GPU docking is disabled.
|
| 699 |
+
|
| 700 |
+
Returned docking fields in CPU demo:
|
| 701 |
+
|
| 702 |
+
- docking_score: null
|
| 703 |
+
- docking_status: not_run
|
| 704 |
+
- docking_pose_file: null
|
| 705 |
+
|
| 706 |
+
Recommended database columns:
|
| 707 |
+
|
| 708 |
+
- docking_score FLOAT NULL
|
| 709 |
+
- docking_status VARCHAR(30) DEFAULT 'not_run'
|
| 710 |
+
- docking_pose_file TEXT NULL
|
| 711 |
+
- docking_mode VARCHAR(20) DEFAULT 'off'
|
| 712 |
+
- docking_error TEXT NULL
|
| 713 |
+
|
| 714 |
+
Recommended frontend and filter behavior:
|
| 715 |
+
|
| 716 |
+
- Show all valid molecules: valid == true
|
| 717 |
+
- Show docked only: docking_status == completed
|
| 718 |
+
- Show not-yet-docked molecules: docking_status == not_run
|
| 719 |
+
- Sort docking results ascending, because more negative docking scores are usually better
|
| 720 |
+
"""
|
| 721 |
+
)
|
| 722 |
+
|
| 723 |
+
contract_btn = gr.Button("Load backend contract")
|
| 724 |
+
contract_json = gr.JSON(label="backend_contract.json")
|
| 725 |
+
|
| 726 |
+
contract_btn.click(
|
| 727 |
+
get_contract,
|
| 728 |
+
outputs=contract_json,
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
with gr.Tab("API Spec"):
|
| 732 |
+
gr.Markdown(
|
| 733 |
+
"""
|
| 734 |
+
This tab loads the API response specification from the private artifact bundle.
|
| 735 |
+
|
| 736 |
+
It explains the expected backend/frontend response format for:
|
| 737 |
+
|
| 738 |
+
- generated molecules
|
| 739 |
+
- scored SMILES
|
| 740 |
+
- docking fields
|
| 741 |
+
- frontend filters
|
| 742 |
+
"""
|
| 743 |
+
)
|
| 744 |
+
|
| 745 |
+
spec_btn = gr.Button("Load API response spec")
|
| 746 |
+
spec_output = gr.Markdown(label="API_RESPONSE_SPEC.md")
|
| 747 |
+
|
| 748 |
+
spec_btn.click(
|
| 749 |
+
get_api_spec,
|
| 750 |
+
outputs=spec_output,
|
| 751 |
+
)
|
| 752 |
+
|
| 753 |
+
|
| 754 |
+
demo.launch()
|
app.py.backup_old_tabs
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import subprocess
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import uuid
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
import gradio as gr
|
| 8 |
+
import pandas as pd
|
| 9 |
+
from huggingface_hub import snapshot_download
|
| 10 |
+
from rdkit import Chem
|
| 11 |
+
from rdkit.Chem import Descriptors, Crippen, Lipinski, rdMolDescriptors, QED, AllChem
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
BUNDLE_REPO = os.getenv("BUNDLE_REPO", "abdullahsamra/egfr-ai-bundle")
|
| 15 |
+
HF_TOKEN = os.getenv("HF_TOKEN")
|
| 16 |
+
|
| 17 |
+
CACHE_DIR = Path("bundle_cache")
|
| 18 |
+
EXAMPLE_JSON = None
|
| 19 |
+
API_SPEC_TEXT = None
|
| 20 |
+
BACKEND_CONTRACT_TEXT = None
|
| 21 |
+
BUNDLE_ROOT = None
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def download_bundle_light():
|
| 25 |
+
global EXAMPLE_JSON, API_SPEC_TEXT, BACKEND_CONTRACT_TEXT, BUNDLE_ROOT
|
| 26 |
+
|
| 27 |
+
try:
|
| 28 |
+
local_dir = snapshot_download(
|
| 29 |
+
repo_id=BUNDLE_REPO,
|
| 30 |
+
repo_type="dataset",
|
| 31 |
+
token=HF_TOKEN,
|
| 32 |
+
allow_patterns=[
|
| 33 |
+
"examples/generate_response.example.json",
|
| 34 |
+
"docs/API_RESPONSE_SPEC.md",
|
| 35 |
+
"configs/backend_contract.json",
|
| 36 |
+
"configs/sampling_model.bundle.toml",
|
| 37 |
+
"models/generator/egfr_generator.chkpt",
|
| 38 |
+
"README.md",
|
| 39 |
+
],
|
| 40 |
+
local_dir=str(CACHE_DIR),
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
root = Path(local_dir)
|
| 44 |
+
BUNDLE_ROOT = root
|
| 45 |
+
|
| 46 |
+
example_path = root / "examples" / "generate_response.example.json"
|
| 47 |
+
spec_path = root / "docs" / "API_RESPONSE_SPEC.md"
|
| 48 |
+
contract_path = root / "configs" / "backend_contract.json"
|
| 49 |
+
|
| 50 |
+
if example_path.exists():
|
| 51 |
+
EXAMPLE_JSON = json.loads(example_path.read_text(encoding="utf-8"))
|
| 52 |
+
|
| 53 |
+
if spec_path.exists():
|
| 54 |
+
API_SPEC_TEXT = spec_path.read_text(encoding="utf-8")
|
| 55 |
+
|
| 56 |
+
if contract_path.exists():
|
| 57 |
+
BACKEND_CONTRACT_TEXT = contract_path.read_text(encoding="utf-8")
|
| 58 |
+
|
| 59 |
+
return True, f"Loaded docs/examples from {BUNDLE_REPO}"
|
| 60 |
+
|
| 61 |
+
except Exception as e:
|
| 62 |
+
return False, f"Could not load private bundle. Check HF_TOKEN secret and BUNDLE_REPO variable. Error: {e}"
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def canonicalize(smiles: str):
|
| 66 |
+
mol = Chem.MolFromSmiles(str(smiles).strip())
|
| 67 |
+
if mol is None:
|
| 68 |
+
return None, None
|
| 69 |
+
return mol, Chem.MolToSmiles(mol, isomericSmiles=True)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def try_sa_score(mol):
|
| 73 |
+
try:
|
| 74 |
+
from rdkit.Contrib.SA_Score import sascorer
|
| 75 |
+
return round(float(sascorer.calculateScore(mol)), 4)
|
| 76 |
+
except Exception:
|
| 77 |
+
return None
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def compute_row(smiles: str):
|
| 81 |
+
mol, can = canonicalize(smiles)
|
| 82 |
+
|
| 83 |
+
if mol is None:
|
| 84 |
+
return {
|
| 85 |
+
"SMILES": smiles,
|
| 86 |
+
"valid": False,
|
| 87 |
+
"canonical_smiles": None,
|
| 88 |
+
"mw": None,
|
| 89 |
+
"logp": None,
|
| 90 |
+
"tpsa": None,
|
| 91 |
+
"hbd": None,
|
| 92 |
+
"hba": None,
|
| 93 |
+
"rot_bonds": None,
|
| 94 |
+
"qed": None,
|
| 95 |
+
"sa_score": None,
|
| 96 |
+
"pred_pAff_mean": None,
|
| 97 |
+
"docking_score": None,
|
| 98 |
+
"docking_status": "not_run",
|
| 99 |
+
"docking_pose_file": None,
|
| 100 |
+
"note": "Invalid SMILES",
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
return {
|
| 104 |
+
"SMILES": smiles,
|
| 105 |
+
"valid": True,
|
| 106 |
+
"canonical_smiles": can,
|
| 107 |
+
"mw": round(float(Descriptors.MolWt(mol)), 4),
|
| 108 |
+
"logp": round(float(Crippen.MolLogP(mol)), 4),
|
| 109 |
+
"tpsa": round(float(rdMolDescriptors.CalcTPSA(mol)), 4),
|
| 110 |
+
"hbd": int(Lipinski.NumHDonors(mol)),
|
| 111 |
+
"hba": int(Lipinski.NumHAcceptors(mol)),
|
| 112 |
+
"rot_bonds": int(Lipinski.NumRotatableBonds(mol)),
|
| 113 |
+
"qed": round(float(QED.qed(mol)), 4),
|
| 114 |
+
"sa_score": try_sa_score(mol),
|
| 115 |
+
"pred_pAff_mean": None,
|
| 116 |
+
"docking_score": None,
|
| 117 |
+
"docking_status": "not_run",
|
| 118 |
+
"docking_pose_file": None,
|
| 119 |
+
"note": "CPU demo: DeepPurpose and real docking are disabled in this Space.",
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def add_ligand_ids_to_rows(rows, prefix="lig"):
|
| 125 |
+
output = []
|
| 126 |
+
for i, row in enumerate(rows, start=1):
|
| 127 |
+
clean = dict(row)
|
| 128 |
+
if not clean.get("ligand_id"):
|
| 129 |
+
clean["ligand_id"] = f"{prefix}_{i:04d}"
|
| 130 |
+
output.append(clean)
|
| 131 |
+
return output
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
def add_ligand_ids_to_df(df, prefix="lig"):
|
| 135 |
+
df = df.copy()
|
| 136 |
+
if "ligand_id" not in df.columns:
|
| 137 |
+
df.insert(0, "ligand_id", [f"{prefix}_{i:04d}" for i in range(1, len(df) + 1)])
|
| 138 |
+
return df
|
| 139 |
+
|
| 140 |
+
def score_smiles(smiles_text):
|
| 141 |
+
smiles_list = []
|
| 142 |
+
for part in str(smiles_text).replace(",", "\n").splitlines():
|
| 143 |
+
part = part.strip()
|
| 144 |
+
if part:
|
| 145 |
+
smiles_list.append(part)
|
| 146 |
+
|
| 147 |
+
if not smiles_list:
|
| 148 |
+
return pd.DataFrame(), {
|
| 149 |
+
"status": "error",
|
| 150 |
+
"message": "Please enter at least one SMILES.",
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
rows = add_ligand_ids_to_rows([compute_row(s) for s in smiles_list])
|
| 154 |
+
df = pd.DataFrame(rows)
|
| 155 |
+
|
| 156 |
+
response = {
|
| 157 |
+
"status": "completed",
|
| 158 |
+
"mode": "score_smiles_cpu_demo",
|
| 159 |
+
"docking_mode": "off",
|
| 160 |
+
"summary": {
|
| 161 |
+
"num_input": len(smiles_list),
|
| 162 |
+
"num_valid": int(df["valid"].sum()),
|
| 163 |
+
"num_returned": len(df),
|
| 164 |
+
},
|
| 165 |
+
"columns": list(df.columns),
|
| 166 |
+
"results": rows,
|
| 167 |
+
"warnings": [
|
| 168 |
+
"This Space is a CPU demo.",
|
| 169 |
+
"DeepPurpose pred_pAff_mean is not computed in this Space version.",
|
| 170 |
+
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 171 |
+
"Outputs are computational predictions only.",
|
| 172 |
+
],
|
| 173 |
+
}
|
| 174 |
+
csv_path = write_results_csv(df)
|
| 175 |
+
response["download_csv_file"] = csv_path
|
| 176 |
+
return df, response, csv_path
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def get_example_table():
|
| 180 |
+
if EXAMPLE_JSON and "results" in EXAMPLE_JSON:
|
| 181 |
+
return pd.DataFrame(EXAMPLE_JSON["results"])
|
| 182 |
+
return pd.DataFrame([
|
| 183 |
+
{"message": "Example response not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable."}
|
| 184 |
+
])
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def get_example_json():
|
| 188 |
+
if EXAMPLE_JSON:
|
| 189 |
+
return EXAMPLE_JSON
|
| 190 |
+
return {
|
| 191 |
+
"status": "not_loaded",
|
| 192 |
+
"message": "Example response not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable.",
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def get_api_spec():
|
| 197 |
+
return API_SPEC_TEXT or "API_RESPONSE_SPEC.md not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable."
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def get_contract():
|
| 201 |
+
if BACKEND_CONTRACT_TEXT:
|
| 202 |
+
try:
|
| 203 |
+
return json.loads(BACKEND_CONTRACT_TEXT)
|
| 204 |
+
except Exception:
|
| 205 |
+
return {"raw": BACKEND_CONTRACT_TEXT}
|
| 206 |
+
|
| 207 |
+
return {
|
| 208 |
+
"status": "not_loaded",
|
| 209 |
+
"message": "backend_contract.json not loaded. Check HF_TOKEN secret or BUNDLE_REPO variable.",
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
def _write_reinvent_sampling_config(job_id, num_molecules):
|
| 213 |
+
if BUNDLE_ROOT is None:
|
| 214 |
+
raise RuntimeError("Bundle root is not loaded. Check HF_TOKEN and BUNDLE_REPO.")
|
| 215 |
+
|
| 216 |
+
checkpoint = Path(BUNDLE_ROOT) / "models" / "generator" / "egfr_generator.chkpt"
|
| 217 |
+
if not checkpoint.exists():
|
| 218 |
+
raise FileNotFoundError(f"Generator checkpoint not found: {checkpoint}")
|
| 219 |
+
|
| 220 |
+
output_dir = Path("outputs")
|
| 221 |
+
output_dir.mkdir(exist_ok=True)
|
| 222 |
+
|
| 223 |
+
raw_csv = output_dir / f"{job_id}_raw_sampling.csv"
|
| 224 |
+
log_file = output_dir / f"{job_id}_reinvent.log"
|
| 225 |
+
resolved_json = output_dir / f"{job_id}_sampling_resolved.json"
|
| 226 |
+
config_path = output_dir / f"{job_id}_sampling.toml"
|
| 227 |
+
|
| 228 |
+
config_text = f"""
|
| 229 |
+
run_type = "sampling"
|
| 230 |
+
device = "cpu"
|
| 231 |
+
|
| 232 |
+
json_out_config = "{resolved_json.resolve()}"
|
| 233 |
+
|
| 234 |
+
[parameters]
|
| 235 |
+
model_file = "{checkpoint.resolve()}"
|
| 236 |
+
output_file = "{raw_csv.resolve()}"
|
| 237 |
+
|
| 238 |
+
num_smiles = {int(num_molecules)}
|
| 239 |
+
unique_molecules = true
|
| 240 |
+
randomize_smiles = true
|
| 241 |
+
"""
|
| 242 |
+
|
| 243 |
+
config_path.write_text(config_text.strip() + "\n", encoding="utf-8")
|
| 244 |
+
return config_path, raw_csv, log_file
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def _run_reinvent_sampling(job_id, num_molecules):
|
| 248 |
+
config_path, raw_csv, log_file = _write_reinvent_sampling_config(job_id, num_molecules)
|
| 249 |
+
|
| 250 |
+
cmd = [
|
| 251 |
+
"reinvent",
|
| 252 |
+
"-l",
|
| 253 |
+
str(log_file.resolve()),
|
| 254 |
+
str(config_path.resolve()),
|
| 255 |
+
]
|
| 256 |
+
|
| 257 |
+
proc = subprocess.run(
|
| 258 |
+
cmd,
|
| 259 |
+
text=True,
|
| 260 |
+
stdout=subprocess.PIPE,
|
| 261 |
+
stderr=subprocess.STDOUT,
|
| 262 |
+
timeout=300,
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
if proc.returncode != 0:
|
| 266 |
+
raise RuntimeError(
|
| 267 |
+
"REINVENT sampling failed.\n"
|
| 268 |
+
f"Command: {' '.join(cmd)}\n"
|
| 269 |
+
f"Output:\n{proc.stdout[-4000:]}"
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
if not raw_csv.exists():
|
| 273 |
+
raise RuntimeError(f"REINVENT finished but output CSV was not found: {raw_csv}")
|
| 274 |
+
|
| 275 |
+
return raw_csv, log_file
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
def _enrich_reinvent_csv(raw_csv, return_top_k):
|
| 279 |
+
raw_df = pd.read_csv(raw_csv)
|
| 280 |
+
|
| 281 |
+
if "SMILES" not in raw_df.columns:
|
| 282 |
+
raise ValueError(f"REINVENT output missing SMILES column. Columns: {list(raw_df.columns)}")
|
| 283 |
+
|
| 284 |
+
rows = []
|
| 285 |
+
for _, r in raw_df.iterrows():
|
| 286 |
+
row = compute_row(str(r["SMILES"]))
|
| 287 |
+
|
| 288 |
+
if "SMILES_state" in raw_df.columns:
|
| 289 |
+
try:
|
| 290 |
+
row["SMILES_state"] = int(r["SMILES_state"])
|
| 291 |
+
except Exception:
|
| 292 |
+
row["SMILES_state"] = None
|
| 293 |
+
else:
|
| 294 |
+
row["SMILES_state"] = None
|
| 295 |
+
|
| 296 |
+
if "NLL" in raw_df.columns:
|
| 297 |
+
try:
|
| 298 |
+
row["NLL"] = float(r["NLL"])
|
| 299 |
+
except Exception:
|
| 300 |
+
row["NLL"] = None
|
| 301 |
+
else:
|
| 302 |
+
row["NLL"] = None
|
| 303 |
+
|
| 304 |
+
# Keep these stable for frontend/backend filters.
|
| 305 |
+
row["pred_pAff_mean"] = None
|
| 306 |
+
row["docking_score"] = None
|
| 307 |
+
row["docking_status"] = "not_run"
|
| 308 |
+
row["docking_pose_file"] = None
|
| 309 |
+
|
| 310 |
+
rows.append(row)
|
| 311 |
+
|
| 312 |
+
df = pd.DataFrame(rows)
|
| 313 |
+
|
| 314 |
+
preferred_cols = [
|
| 315 |
+
"SMILES",
|
| 316 |
+
"SMILES_state",
|
| 317 |
+
"NLL",
|
| 318 |
+
"valid",
|
| 319 |
+
"canonical_smiles",
|
| 320 |
+
"mw",
|
| 321 |
+
"logp",
|
| 322 |
+
"tpsa",
|
| 323 |
+
"hbd",
|
| 324 |
+
"hba",
|
| 325 |
+
"rot_bonds",
|
| 326 |
+
"qed",
|
| 327 |
+
"sa_score",
|
| 328 |
+
"pred_pAff_mean",
|
| 329 |
+
"docking_score",
|
| 330 |
+
"docking_status",
|
| 331 |
+
"docking_pose_file",
|
| 332 |
+
"note",
|
| 333 |
+
]
|
| 334 |
+
|
| 335 |
+
cols = [c for c in preferred_cols if c in df.columns] + [c for c in df.columns if c not in preferred_cols]
|
| 336 |
+
df = df[cols]
|
| 337 |
+
|
| 338 |
+
return_top_k = int(return_top_k)
|
| 339 |
+
if return_top_k > 0:
|
| 340 |
+
df = df.head(return_top_k)
|
| 341 |
+
|
| 342 |
+
return df
|
| 343 |
+
|
| 344 |
+
|
| 345 |
+
def generate_job_demo(num_molecules, return_top_k, docking_mode, generation_backend):
|
| 346 |
+
"""
|
| 347 |
+
Generation endpoint for the Space.
|
| 348 |
+
|
| 349 |
+
Demo mode uses packaged example rows.
|
| 350 |
+
Live mode tries to run REINVENT CPU sampling from the packaged checkpoint.
|
| 351 |
+
"""
|
| 352 |
+
job_id = f"gen_{uuid.uuid4().hex[:8]}"
|
| 353 |
+
created_at = datetime.utcnow().isoformat() + "Z"
|
| 354 |
+
num_molecules = int(num_molecules)
|
| 355 |
+
return_top_k = int(return_top_k)
|
| 356 |
+
|
| 357 |
+
warnings = []
|
| 358 |
+
|
| 359 |
+
try:
|
| 360 |
+
if generation_backend == "live_reinvent_cpu":
|
| 361 |
+
raw_csv, log_file = _run_reinvent_sampling(job_id, num_molecules)
|
| 362 |
+
df = _enrich_reinvent_csv(raw_csv, return_top_k)
|
| 363 |
+
mode = "live_reinvent_cpu_sampling"
|
| 364 |
+
status = "completed"
|
| 365 |
+
warnings.extend([
|
| 366 |
+
"Live REINVENT CPU sampling was executed in this Space.",
|
| 367 |
+
"DeepPurpose scoring is not enabled in this Space version; pred_pAff_mean is null.",
|
| 368 |
+
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 369 |
+
])
|
| 370 |
+
|
| 371 |
+
else:
|
| 372 |
+
if EXAMPLE_JSON and "results" in EXAMPLE_JSON:
|
| 373 |
+
rows = EXAMPLE_JSON["results"]
|
| 374 |
+
else:
|
| 375 |
+
rows = [compute_row("CCO"), compute_row("c1ccccc1")]
|
| 376 |
+
|
| 377 |
+
selected = rows[: max(1, min(return_top_k, len(rows)))]
|
| 378 |
+
|
| 379 |
+
clean_rows = []
|
| 380 |
+
for row in selected:
|
| 381 |
+
clean = dict(row)
|
| 382 |
+
clean["docking_score"] = None
|
| 383 |
+
clean["docking_status"] = "not_run"
|
| 384 |
+
clean["docking_pose_file"] = None
|
| 385 |
+
if "pred_pAff_mean" not in clean:
|
| 386 |
+
clean["pred_pAff_mean"] = None
|
| 387 |
+
clean_rows.append(clean)
|
| 388 |
+
|
| 389 |
+
df = pd.DataFrame(clean_rows)
|
| 390 |
+
mode = "generate_job_cpu_demo"
|
| 391 |
+
status = "completed"
|
| 392 |
+
warnings.extend([
|
| 393 |
+
"This is a CPU demo generation response.",
|
| 394 |
+
"Live REINVENT generation was not executed because demo mode was selected.",
|
| 395 |
+
"The response shape matches the planned backend contract.",
|
| 396 |
+
"Real docking is not run in this CPU Space; docking_status is not_run.",
|
| 397 |
+
])
|
| 398 |
+
|
| 399 |
+
except Exception as e:
|
| 400 |
+
df = pd.DataFrame()
|
| 401 |
+
mode = "live_reinvent_cpu_sampling"
|
| 402 |
+
status = "failed"
|
| 403 |
+
warnings.append(str(e))
|
| 404 |
+
|
| 405 |
+
df = add_ligand_ids_to_df(df)
|
| 406 |
+
|
| 407 |
+
output_dir = Path("outputs")
|
| 408 |
+
output_dir.mkdir(exist_ok=True)
|
| 409 |
+
csv_path = output_dir / f"{job_id}_results.csv"
|
| 410 |
+
df.to_csv(csv_path, index=False)
|
| 411 |
+
|
| 412 |
+
job_summary = {
|
| 413 |
+
"job_id": job_id,
|
| 414 |
+
"status": status,
|
| 415 |
+
"mode": mode,
|
| 416 |
+
"created_at": created_at,
|
| 417 |
+
"preset": "egfr_generator",
|
| 418 |
+
"docking_mode": docking_mode,
|
| 419 |
+
"num_requested": num_molecules,
|
| 420 |
+
"num_returned": int(len(df)),
|
| 421 |
+
"download_csv_file": str(csv_path),
|
| 422 |
+
"live_generation": generation_backend == "live_reinvent_cpu",
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
response = {
|
| 426 |
+
"job_id": job_id,
|
| 427 |
+
"status": status,
|
| 428 |
+
"mode": mode,
|
| 429 |
+
"created_at": created_at,
|
| 430 |
+
"preset": "egfr_generator",
|
| 431 |
+
"docking_mode": docking_mode,
|
| 432 |
+
"summary": {
|
| 433 |
+
"num_requested": num_molecules,
|
| 434 |
+
"num_returned": int(len(df)),
|
| 435 |
+
},
|
| 436 |
+
"columns": list(df.columns),
|
| 437 |
+
"results": df.to_dict(orient="records"),
|
| 438 |
+
"download_csv_file": str(csv_path),
|
| 439 |
+
"warnings": warnings,
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
return job_summary, df, response, str(csv_path)
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
def write_results_csv(df: pd.DataFrame, filename: str = "score_smiles_results.csv"):
|
| 446 |
+
out_dir = Path("outputs")
|
| 447 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 448 |
+
out_path = out_dir / filename
|
| 449 |
+
df.to_csv(out_path, index=False)
|
| 450 |
+
return str(out_path)
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def smiles_to_3d_file(smiles: str, file_format: str = "sdf"):
|
| 454 |
+
smiles = str(smiles).strip()
|
| 455 |
+
file_format = str(file_format).lower().strip()
|
| 456 |
+
|
| 457 |
+
if file_format not in ["sdf", "mol", "pdb"]:
|
| 458 |
+
return None, {
|
| 459 |
+
"status": "error",
|
| 460 |
+
"message": "Unsupported format. Choose sdf, mol, or pdb.",
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
mol = Chem.MolFromSmiles(smiles)
|
| 464 |
+
if mol is None:
|
| 465 |
+
return None, {
|
| 466 |
+
"status": "error",
|
| 467 |
+
"input_smiles": smiles,
|
| 468 |
+
"message": "Invalid SMILES.",
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
canonical_smiles = Chem.MolToSmiles(mol, isomericSmiles=True)
|
| 472 |
+
|
| 473 |
+
mol = Chem.AddHs(mol)
|
| 474 |
+
|
| 475 |
+
params = AllChem.ETKDGv3()
|
| 476 |
+
params.randomSeed = 42
|
| 477 |
+
|
| 478 |
+
embed_status = AllChem.EmbedMolecule(mol, params)
|
| 479 |
+
if embed_status != 0:
|
| 480 |
+
return None, {
|
| 481 |
+
"status": "error",
|
| 482 |
+
"input_smiles": smiles,
|
| 483 |
+
"canonical_smiles": canonical_smiles,
|
| 484 |
+
"message": "3D embedding failed.",
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
try:
|
| 488 |
+
AllChem.UFFOptimizeMolecule(mol, maxIters=500)
|
| 489 |
+
optimization_status = "UFF optimized"
|
| 490 |
+
except Exception as e:
|
| 491 |
+
optimization_status = f"UFF optimization failed, using embedded geometry only: {e}"
|
| 492 |
+
|
| 493 |
+
out_dir = Path("outputs/ligands")
|
| 494 |
+
out_dir.mkdir(parents=True, exist_ok=True)
|
| 495 |
+
|
| 496 |
+
safe_name = "ligand_3d"
|
| 497 |
+
out_path = out_dir / f"{safe_name}.{file_format}"
|
| 498 |
+
|
| 499 |
+
if file_format == "sdf":
|
| 500 |
+
writer = Chem.SDWriter(str(out_path))
|
| 501 |
+
writer.write(mol)
|
| 502 |
+
writer.close()
|
| 503 |
+
elif file_format == "mol":
|
| 504 |
+
Chem.MolToMolFile(mol, str(out_path))
|
| 505 |
+
elif file_format == "pdb":
|
| 506 |
+
Chem.MolToPDBFile(mol, str(out_path))
|
| 507 |
+
|
| 508 |
+
meta = {
|
| 509 |
+
"status": "completed",
|
| 510 |
+
"input_smiles": smiles,
|
| 511 |
+
"canonical_smiles": canonical_smiles,
|
| 512 |
+
"file_format": file_format,
|
| 513 |
+
"file_path": str(out_path),
|
| 514 |
+
"optimization_status": optimization_status,
|
| 515 |
+
"note": "This is a generated 3D conformer from SMILES, not a docked protein-ligand pose.",
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
return str(out_path), meta
|
| 519 |
+
|
| 520 |
+
loaded_ok, loaded_msg = download_bundle_light()
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
with gr.Blocks(title="EGFR AI Demo") as demo:
|
| 524 |
+
gr.Markdown(
|
| 525 |
+
"""
|
| 526 |
+
# EGFR AI Demo
|
| 527 |
+
|
| 528 |
+
Private CPU demo for the EGFR AI backend workflow.
|
| 529 |
+
|
| 530 |
+
This demo shows the backend response shape for generation, scoring, filtering, and docking integration.
|
| 531 |
+
|
| 532 |
+
Real AutoDock-GPU docking is disabled in this CPU Space, but docking fields are present so backend/frontend teams can implement database columns and filters.
|
| 533 |
+
"""
|
| 534 |
+
)
|
| 535 |
+
|
| 536 |
+
gr.Markdown(f"Bundle load status: {loaded_msg}")
|
| 537 |
+
|
| 538 |
+
with gr.Tab("Score SMILES"):
|
| 539 |
+
gr.Markdown(
|
| 540 |
+
"""
|
| 541 |
+
Enter one or more SMILES.
|
| 542 |
+
|
| 543 |
+
This CPU demo calculates molecular properties and returns the same response columns expected by the backend.
|
| 544 |
+
|
| 545 |
+
Important:
|
| 546 |
+
- pred_pAff_mean is an integration field in this Space version.
|
| 547 |
+
- real docking is disabled in this CPU Space.
|
| 548 |
+
- docking_status is returned as not_run.
|
| 549 |
+
"""
|
| 550 |
+
)
|
| 551 |
+
|
| 552 |
+
smiles_input = gr.Textbox(
|
| 553 |
+
label="SMILES input",
|
| 554 |
+
value="CCO\nc1ccccc1",
|
| 555 |
+
lines=5,
|
| 556 |
+
)
|
| 557 |
+
|
| 558 |
+
score_btn = gr.Button("Score SMILES")
|
| 559 |
+
score_table = gr.Dataframe(label="Results table")
|
| 560 |
+
score_json = gr.JSON(label="Backend-style JSON response")
|
| 561 |
+
score_csv = gr.File(label="Download CSV")
|
| 562 |
+
|
| 563 |
+
|
| 564 |
+
score_btn.click(
|
| 565 |
+
score_smiles,
|
| 566 |
+
inputs=smiles_input,
|
| 567 |
+
outputs=[score_table, score_json, score_csv],
|
| 568 |
+
)
|
| 569 |
+
with gr.Tab("Generate Job Demo"):
|
| 570 |
+
gr.Markdown(
|
| 571 |
+
"""
|
| 572 |
+
This tab demonstrates generation job behavior.
|
| 573 |
+
|
| 574 |
+
Generation backend options:
|
| 575 |
+
|
| 576 |
+
- demo_rows: uses packaged example rows. Fast and stable.
|
| 577 |
+
- live_reinvent_cpu: tries to run real REINVENT sampling from the packaged checkpoint on CPU.
|
| 578 |
+
|
| 579 |
+
The output is split into:
|
| 580 |
+
|
| 581 |
+
1. Job summary: job_id, status, preset, mode, and metadata.
|
| 582 |
+
2. Generated molecule results table.
|
| 583 |
+
3. Full backend-style generation JSON.
|
| 584 |
+
4. Download CSV.
|
| 585 |
+
|
| 586 |
+
Notes:
|
| 587 |
+
- DeepPurpose live scoring is not enabled yet in this Space version.
|
| 588 |
+
- Real docking is disabled in this CPU Space, so docking_status is returned as not_run.
|
| 589 |
+
"""
|
| 590 |
+
)
|
| 591 |
+
|
| 592 |
+
generation_backend = gr.Dropdown(
|
| 593 |
+
label="generation_backend",
|
| 594 |
+
choices=["demo_rows", "live_reinvent_cpu"],
|
| 595 |
+
value="demo_rows",
|
| 596 |
+
)
|
| 597 |
+
|
| 598 |
+
gen_num_molecules = gr.Number(
|
| 599 |
+
label="num_molecules",
|
| 600 |
+
value=20,
|
| 601 |
+
precision=0,
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
+
gen_return_top_k = gr.Number(
|
| 605 |
+
label="return_top_k",
|
| 606 |
+
value=20,
|
| 607 |
+
precision=0,
|
| 608 |
+
)
|
| 609 |
+
|
| 610 |
+
gen_docking_mode = gr.Dropdown(
|
| 611 |
+
label="docking_mode",
|
| 612 |
+
choices=["off", "top_k", "all"],
|
| 613 |
+
value="off",
|
| 614 |
+
)
|
| 615 |
+
|
| 616 |
+
gen_btn = gr.Button("Run Generation Job")
|
| 617 |
+
|
| 618 |
+
gen_summary = gr.JSON(label="Job summary")
|
| 619 |
+
gen_table = gr.Dataframe(label="Generated molecule results table")
|
| 620 |
+
gen_json = gr.JSON(label="Full backend-style generation JSON")
|
| 621 |
+
gen_file = gr.File(label="Download CSV")
|
| 622 |
+
|
| 623 |
+
gen_btn.click(
|
| 624 |
+
generate_job_demo,
|
| 625 |
+
inputs=[gen_num_molecules, gen_return_top_k, gen_docking_mode, generation_backend],
|
| 626 |
+
outputs=[gen_summary, gen_table, gen_json, gen_file],
|
| 627 |
+
)
|
| 628 |
+
|
| 629 |
+
|
| 630 |
+
with gr.Tab("Example Generate Response"):
|
| 631 |
+
gr.Markdown(
|
| 632 |
+
"""
|
| 633 |
+
This example is loaded from the private Hugging Face Dataset bundle.
|
| 634 |
+
|
| 635 |
+
It shows the expected response shape after:
|
| 636 |
+
|
| 637 |
+
Generate -> Enrich -> Optional Docking
|
| 638 |
+
"""
|
| 639 |
+
)
|
| 640 |
+
|
| 641 |
+
example_btn = gr.Button("Load example response")
|
| 642 |
+
example_table = gr.Dataframe(label="Example results table")
|
| 643 |
+
example_json = gr.JSON(label="Example JSON response")
|
| 644 |
+
|
| 645 |
+
example_btn.click(
|
| 646 |
+
lambda: (get_example_table(), get_example_json()),
|
| 647 |
+
outputs=[example_table, example_json],
|
| 648 |
+
)
|
| 649 |
+
|
| 650 |
+
with gr.Tab("SMILES to 3D File"):
|
| 651 |
+
gr.Markdown(
|
| 652 |
+
"""
|
| 653 |
+
Convert a SMILES string into a downloadable 3D ligand file.
|
| 654 |
+
|
| 655 |
+
Supported formats:
|
| 656 |
+
|
| 657 |
+
- SDF
|
| 658 |
+
- MOL
|
| 659 |
+
- PDB
|
| 660 |
+
|
| 661 |
+
Important: this creates a 3D conformer from SMILES. It is not a docked pose inside the EGFR binding pocket.
|
| 662 |
+
"""
|
| 663 |
+
)
|
| 664 |
+
|
| 665 |
+
smiles_3d_input = gr.Textbox(
|
| 666 |
+
label="SMILES",
|
| 667 |
+
value="CCO",
|
| 668 |
+
lines=2,
|
| 669 |
+
)
|
| 670 |
+
|
| 671 |
+
format_input = gr.Radio(
|
| 672 |
+
choices=["sdf", "mol", "pdb"],
|
| 673 |
+
value="sdf",
|
| 674 |
+
label="Output file format",
|
| 675 |
+
)
|
| 676 |
+
|
| 677 |
+
make_3d_btn = gr.Button("Generate 3D ligand file")
|
| 678 |
+
ligand_file = gr.File(label="Download 3D ligand file")
|
| 679 |
+
ligand_meta = gr.JSON(label="3D file metadata")
|
| 680 |
+
|
| 681 |
+
make_3d_btn.click(
|
| 682 |
+
smiles_to_3d_file,
|
| 683 |
+
inputs=[smiles_3d_input, format_input],
|
| 684 |
+
outputs=[ligand_file, ligand_meta],
|
| 685 |
+
)
|
| 686 |
+
|
| 687 |
+
with gr.Tab("Docking Contract"):
|
| 688 |
+
gr.Markdown(
|
| 689 |
+
"""
|
| 690 |
+
## Docking modes
|
| 691 |
+
|
| 692 |
+
The full backend supports:
|
| 693 |
+
|
| 694 |
+
- off
|
| 695 |
+
- top_k
|
| 696 |
+
- all
|
| 697 |
+
|
| 698 |
+
In this CPU Space, real AutoDock-GPU docking is disabled.
|
| 699 |
+
|
| 700 |
+
Returned docking fields in CPU demo:
|
| 701 |
+
|
| 702 |
+
- docking_score: null
|
| 703 |
+
- docking_status: not_run
|
| 704 |
+
- docking_pose_file: null
|
| 705 |
+
|
| 706 |
+
Recommended database columns:
|
| 707 |
+
|
| 708 |
+
- docking_score FLOAT NULL
|
| 709 |
+
- docking_status VARCHAR(30) DEFAULT 'not_run'
|
| 710 |
+
- docking_pose_file TEXT NULL
|
| 711 |
+
- docking_mode VARCHAR(20) DEFAULT 'off'
|
| 712 |
+
- docking_error TEXT NULL
|
| 713 |
+
|
| 714 |
+
Recommended frontend and filter behavior:
|
| 715 |
+
|
| 716 |
+
- Show all valid molecules: valid == true
|
| 717 |
+
- Show docked only: docking_status == completed
|
| 718 |
+
- Show not-yet-docked molecules: docking_status == not_run
|
| 719 |
+
- Sort docking results ascending, because more negative docking scores are usually better
|
| 720 |
+
"""
|
| 721 |
+
)
|
| 722 |
+
|
| 723 |
+
contract_btn = gr.Button("Load backend contract")
|
| 724 |
+
contract_json = gr.JSON(label="backend_contract.json")
|
| 725 |
+
|
| 726 |
+
contract_btn.click(
|
| 727 |
+
get_contract,
|
| 728 |
+
outputs=contract_json,
|
| 729 |
+
)
|
| 730 |
+
|
| 731 |
+
with gr.Tab("API Spec"):
|
| 732 |
+
gr.Markdown(
|
| 733 |
+
"""
|
| 734 |
+
This tab loads the API response specification from the private artifact bundle.
|
| 735 |
+
|
| 736 |
+
It explains the expected backend/frontend response format for:
|
| 737 |
+
|
| 738 |
+
- generated molecules
|
| 739 |
+
- scored SMILES
|
| 740 |
+
- docking fields
|
| 741 |
+
- frontend filters
|
| 742 |
+
"""
|
| 743 |
+
)
|
| 744 |
+
|
| 745 |
+
spec_btn = gr.Button("Load API response spec")
|
| 746 |
+
spec_output = gr.Markdown(label="API_RESPONSE_SPEC.md")
|
| 747 |
+
|
| 748 |
+
spec_btn.click(
|
| 749 |
+
get_api_spec,
|
| 750 |
+
outputs=spec_output,
|
| 751 |
+
)
|
| 752 |
+
|
| 753 |
+
|
| 754 |
+
demo.launch()
|