abdullahsamra commited on
Commit
c1aa524
·
verified ·
1 Parent(s): d0bff3b

Upload folder using huggingface_hub

Browse files
__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
- BUNDLE_REPO = os.getenv("BUNDLE_REPO", "abdullahsamra/egfr-ai-bundle")
 
 
 
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": rows,
 
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
- def get_example_json():
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
- Demo mode uses packaged example rows.
333
- Live mode tries to run REINVENT CPU sampling from the packaged checkpoint.
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, log_file = _run_reinvent_sampling(job_id, num_molecules)
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 demo mode was selected.",
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": df.to_dict(orient="records"),
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 shows the backend response shape for generation, scoring, filtering, and docking integration.
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
- This CPU demo calculates molecular properties and returns the same response columns expected by the backend.
 
 
525
 
526
- Important:
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
- score_btn.click(
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
- This tab demonstrates generation job behavior.
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
- The output is split into:
561
 
562
- 1. Job summary: job_id, status, preset, mode, and metadata.
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 Generation Job")
598
 
599
  gen_summary = gr.JSON(label="Job summary")
600
- gen_table = gr.Dataframe(label="Generated molecule results table")
601
- gen_json = gr.JSON(label="Full backend-style generation JSON")
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
- This example is loaded from the private Hugging Face Dataset bundle.
615
-
616
- It shows the expected response shape after:
617
 
618
- Generate -> Enrich -> Optional Docking
619
  """
620
  )
621
 
622
- example_btn = gr.Button("Load example response")
623
- example_table = gr.Dataframe(label="Example results table")
624
- example_json = gr.JSON(label="Example JSON response")
 
 
 
 
 
 
 
625
 
626
- example_btn.click(
627
- lambda: (get_example_table(), get_example_json()),
628
- outputs=[example_table, example_json],
 
629
  )
630
 
631
- with gr.Tab("SMILES to 3D File"):
632
- gr.Markdown(
633
  """
634
- Convert a SMILES string into a downloadable 3D ligand file.
635
 
636
- Supported formats:
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="CCO",
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("Docking Contract"):
669
  gr.Markdown(
670
  """
671
- ## Docking modes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
 
673
- The full backend supports:
674
 
675
- - off
676
- - top_k
677
- - all
678
 
679
- In this CPU Space, real AutoDock-GPU docking is disabled.
680
 
681
- Returned docking fields in CPU demo:
682
 
683
- - docking_score: null
684
- - docking_status: not_run
685
- - docking_pose_file: null
686
 
687
- Recommended database columns:
 
 
688
 
689
- - docking_score FLOAT NULL
690
- - docking_status VARCHAR(30) DEFAULT 'not_run'
691
- - docking_pose_file TEXT NULL
692
- - docking_mode VARCHAR(20) DEFAULT 'off'
693
- - docking_error TEXT NULL
694
 
695
- Recommended frontend and filter behavior:
696
 
697
- - Show all valid molecules: valid == true
698
- - Show docked only: docking_status == completed
699
- - Show not-yet-docked molecules: docking_status == not_run
700
- - Sort docking results ascending, because more negative docking scores are usually better
 
701
  """
702
  )
703
 
704
- contract_btn = gr.Button("Load backend contract")
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
- with gr.Tab("API Spec"):
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()