TEZv commited on
Commit
eb25c85
·
verified ·
1 Parent(s): 56e818a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +50 -66
app.py CHANGED
@@ -25,7 +25,7 @@ from PIL import Image
25
  from functools import wraps
26
 
27
  # ─────────────────────────────────────────────
28
- # CACHE SYSTEM (TTL = 24 h)
29
  # ─────────────────────────────────────────────
30
  CACHE_DIR = "./cache"
31
  os.makedirs(CACHE_DIR, exist_ok=True)
@@ -55,7 +55,6 @@ def cache_set(endpoint: str, query: str, data):
55
  # RETRY DECORATOR
56
  # ─────────────────────────────────────────────
57
  def retry(max_attempts=3, delay=1):
58
- """Decorator to retry a function on exception."""
59
  def decorator(func):
60
  @wraps(func)
61
  def wrapper(*args, **kwargs):
@@ -72,24 +71,23 @@ def retry(max_attempts=3, delay=1):
72
  return decorator
73
 
74
  # ─────────────────────────────────────────────
75
- # LAB JOURNAL
76
  # ─────────────────────────────────────────────
77
  JOURNAL_FILE = "./lab_journal.csv"
78
- # Уніфікована система кодування (як у Learning Playground)
79
  JOURNAL_CATEGORIES = [
80
- # Real Data Tools (S2-A)
81
- "S2-A·R1a", # Gray Zones Explorer
82
- "S2-A·R1b", # Understudied Target Finder
83
- "S2-A·R1c", # Real Variant Lookup
84
- "S2-A·R1d", # Literature Gap Finder
85
- "S2-A·R1e", # Druggable Orphans
86
- "S2-A·R1f", # Research Assistant (Chatbot)
87
- # Learning Sandbox (S2-B)
88
- "S2-B·R1a", # miRNA Explorer
89
- "S2-B·R1b", # siRNA Targets
90
- "S2-B·R1c", # LNP Corona
91
- "S2-B·R1d", # Flow Corona
92
- "S2-B·R1e", # Variant Concepts
93
  "Manual"
94
  ]
95
 
@@ -176,12 +174,11 @@ CT_BASE = "https://clinicaltrials.gov/api/v2"
176
 
177
  @retry(max_attempts=3, delay=1)
178
  def pubmed_count(query: str) -> int:
179
- """Return paper count for a PubMed query (cached)."""
180
  cached = cache_get("pubmed_count", query)
181
  if cached is not None:
182
  return cached
183
  try:
184
- time.sleep(0.34) # Rate limit compliance
185
  r = requests.get(
186
  f"{PUBMED_BASE}/esearch.fcgi",
187
  params={"db": "pubmed", "term": query, "rettype": "count", "retmode": "json"},
@@ -192,19 +189,11 @@ def pubmed_count(query: str) -> int:
192
  count = int(data.get("esearchresult", {}).get("count", 0))
193
  cache_set("pubmed_count", query, count)
194
  return count
195
- except requests.exceptions.Timeout:
196
- print(f"Timeout for query: {query}")
197
- return -1
198
- except requests.exceptions.RequestException as e:
199
- print(f"Request error for {query}: {e}")
200
- return -1
201
- except (KeyError, ValueError) as e:
202
- print(f"Parsing error for {query}: {e}")
203
  return -1
204
 
205
  @retry(max_attempts=3, delay=1)
206
  def pubmed_search(query: str, retmax: int = 10) -> list:
207
- """Return list of PMIDs (cached)."""
208
  cached = cache_get("pubmed_search", f"{query}_{retmax}")
209
  if cached is not None:
210
  return cached
@@ -225,7 +214,6 @@ def pubmed_search(query: str, retmax: int = 10) -> list:
225
 
226
  @retry(max_attempts=3, delay=1)
227
  def pubmed_summary(pmids: list) -> list:
228
- """Fetch summaries for a list of PMIDs."""
229
  if not pmids:
230
  return []
231
  cached = cache_get("pubmed_summary", ",".join(pmids))
@@ -248,7 +236,6 @@ def pubmed_summary(pmids: list) -> list:
248
 
249
  @retry(max_attempts=3, delay=1)
250
  def ot_query(gql: str, variables: dict = None) -> dict:
251
- """Run an OpenTargets GraphQL query (cached)."""
252
  key = json.dumps({"q": gql, "v": variables}, sort_keys=True)
253
  cached = cache_get("ot_gql", key)
254
  if cached is not None:
@@ -262,16 +249,13 @@ def ot_query(gql: str, variables: dict = None) -> dict:
262
  r.raise_for_status()
263
  data = r.json()
264
  if "errors" in data:
265
- print(f"GraphQL errors: {data['errors']}")
266
  return {"error": data["errors"]}
267
  cache_set("ot_gql", key, data)
268
  return data
269
  except Exception as e:
270
- print(f"OT query error: {e}")
271
  return {"error": str(e)}
272
-
273
  # ─────────────────────────────────────────────
274
- # TAB A1 — GRAY ZONES EXPLORER
275
  # ─────────────────────────────────────────────
276
 
277
  def a1_run(cancer_type: str):
@@ -324,13 +308,13 @@ def a1_run(cancer_type: str):
324
  )
325
 
326
  gaps_md = "\n\n---\n\n".join(gap_cards) if gap_cards else "No data available."
327
- journal_log("S2-A·R1a", f"cancer={cancer_type}", f"gaps={[p for p,_ in sorted_procs[:5]]}")
328
  source_note = f"*Source: PubMed E-utilities | Date: {today}*"
329
  return img, gaps_md + "\n\n" + source_note
330
 
331
 
332
  # ─────────────────────────────────────────────
333
- # TAB A2 — UNDERSTUDIED TARGET FINDER
334
  # ─────────────────────────────────────────────
335
 
336
  DEPMAP_URL = "https://ndownloader.figshare.com/files/40448549" # CRISPR_gene_effect.csv (public)
@@ -458,14 +442,14 @@ def a2_run(cancer_type: str):
458
  f"and replace `_load_depmap_sample()` in `app.py`."
459
  )
460
  if not result_df.empty:
461
- journal_log("S2-A·R1b", f"cancer={cancer_type}", f"top_gap={result_df.iloc[0]['Gene']}")
462
  else:
463
- journal_log("S2-A·R1b", f"cancer={cancer_type}", "no targets found")
464
  return result_df, note
465
 
466
 
467
  # ─────────────────────────────────────────────
468
- # TAB A3 — REAL VARIANT LOOKUP
469
  # ─────────────────────────────────────────────
470
 
471
  def a3_run(hgvs: str):
@@ -586,12 +570,12 @@ def a3_run(hgvs: str):
586
  )
587
 
588
  result_parts.append(f"\n*Source: ClinVar E-utilities + gnomAD GraphQL | Date: {today}*")
589
- journal_log("S2-A·R1c", f"hgvs={hgvs}", result_parts[0][:100] if result_parts else "no results")
590
  return "\n\n".join(result_parts)
591
 
592
 
593
  # ─────────────────────────────────────────────
594
- # TAB A4 — LITERATURE GAP FINDER
595
  # ─────────────────────────────────────────────
596
 
597
  def a4_run(cancer_type: str, keyword: str):
@@ -652,12 +636,12 @@ def a4_run(cancer_type: str, keyword: str):
652
 
653
  summary = "\n\n".join(gap_text)
654
  summary += f"\n\n*Source: PubMed E-utilities | Date: {today}*"
655
- journal_log("S2-A·R1d", f"cancer={cancer_type}, kw={keyword}", summary[:100])
656
  return img, summary
657
 
658
 
659
  # ─────────────────────────────────────────────
660
- # TAB A5 — DRUGGABLE ORPHANS
661
  # ─────────────────────────────────────────────
662
 
663
  def a5_run(cancer_type: str):
@@ -749,7 +733,7 @@ def a5_run(cancer_type: str):
749
  f"*Source: OpenTargets GraphQL + ClinicalTrials.gov v2 | Date: {today}*\n\n"
750
  f"*Orphan = no approved drug (OpenTargets knownDrugs.count = 0)*"
751
  )
752
- journal_log("S2-A·R1e", f"cancer={cancer_type}", f"orphans={len(df)}")
753
  return df, note
754
 
755
 
@@ -762,7 +746,7 @@ SIMULATED_BANNER = (
762
  "for educational purposes only. Results do NOT reflect real experimental outcomes."
763
  )
764
 
765
- # ── TAB B1 — miRNA Explorer ──────────────────
766
 
767
  MIRNA_DB = {
768
  "BRCA2": {
@@ -832,11 +816,11 @@ def b1_run(gene: str):
832
  "Expression log2FC": changes,
833
  })
834
  context = f"\n\n**Cancer Context:** {db['cancer_context']}"
835
- journal_log("S2-B·R1a", f"gene={gene}", f"top_miRNA={mirnas[0]}")
836
  return img, df.to_markdown(index=False) + context
837
 
838
 
839
- # ── TAB B2 — siRNA Targets ───────────────────
840
 
841
  SIRNA_DB = {
842
  "LUAD": {
@@ -894,11 +878,11 @@ def b2_run(cancer: str):
894
  "Off-target Risk": off_risk,
895
  "Delivery Challenge": delivery,
896
  })
897
- journal_log("S2-B·R1b", f"cancer={cancer}", f"top={targets[0]}")
898
  return img, df.to_markdown(index=False)
899
 
900
 
901
- # ── TAB B3 — LNP Corona Simulator ───────────────
902
 
903
  def b3_run(peg_mol_pct: float, ionizable_pct: float, helper_pct: float,
904
  chol_pct: float, particle_size_nm: float, serum_pct: float):
@@ -950,11 +934,11 @@ def b3_run(peg_mol_pct: float, ionizable_pct: float, helper_pct: float,
950
  + ("High ApoE → enhanced brain/liver targeting via LDLR pathway." if apoe_pct > 25
951
  else "Low ApoE → reduced receptor-mediated uptake.")
952
  )
953
- journal_log("S2-B·R1c", f"PEG={peg_mol_pct}%,size={particle_size_nm}nm", f"ApoE={apoe_pct:.1f}%")
954
  return img, interpretation
955
 
956
 
957
- # ── TAB B4 — Flow Corona (Vroman Kinetics) ──────
958
 
959
  def b4_run(time_points: int, kon_albumin: float, kon_apoe: float,
960
  koff_albumin: float, koff_apoe: float):
@@ -994,11 +978,11 @@ def b4_run(time_points: int, kon_albumin: float, kon_apoe: float,
994
  "The Vroman effect describes sequential protein displacement: "
995
  "abundant proteins (albumin) adsorb first, then are displaced by higher-affinity proteins (ApoE, fibrinogen)."
996
  )
997
- journal_log("S2-B·R1d", f"kon_alb={kon_albumin},kon_apoe={kon_apoe}", note[:80])
998
  return img, note
999
 
1000
 
1001
- # ── TAB B5 — Variant Concepts ───────────────────
1002
 
1003
  VARIANT_RULES = {
1004
  "Pathogenic": {
@@ -1048,7 +1032,7 @@ def b5_run(classification: str):
1048
  f"> ⚠️ SIMULATED — This is a rule-based educational model only. "
1049
  f"Real variant classification requires expert review and full ACMG/AMP criteria evaluation."
1050
  )
1051
- journal_log("S2-B·R1e", f"class={classification}", output[:100])
1052
  return output
1053
  # ─────────────────────────────────────────────
1054
  # GRADIO UI ASSEMBLY
@@ -1099,7 +1083,7 @@ def build_app():
1099
  with gr.Tabs():
1100
 
1101
  # ── A1 ──
1102
- with gr.Tab("🔍 Gray Zones Explorer"):
1103
  gr.Markdown(
1104
  "Identify underexplored biological processes in a cancer type "
1105
  "using live PubMed + OpenTargets data."
@@ -1120,7 +1104,7 @@ def build_app():
1120
  a1_btn.click(a1_run, inputs=[a1_cancer], outputs=[a1_heatmap, a1_gaps])
1121
 
1122
  # ── A2 ──
1123
- with gr.Tab("🎯 Understudied Target Finder"):
1124
  gr.Markdown(
1125
  "Find essential genes with high research gap index "
1126
  "(high essentiality, low publication coverage)."
@@ -1150,7 +1134,7 @@ def build_app():
1150
  a2_btn.click(a2_run, inputs=[a2_cancer], outputs=[a2_table, a2_note])
1151
 
1152
  # ── A3 ──
1153
- with gr.Tab("🧬 Real Variant Lookup"):
1154
  gr.Markdown(
1155
  "Look up a variant in **ClinVar** and **gnomAD**. "
1156
  "Results are fetched live — never hallucinated."
@@ -1177,7 +1161,7 @@ def build_app():
1177
  a3_btn.click(a3_run, inputs=[a3_hgvs], outputs=[a3_result])
1178
 
1179
  # ── A4 ──
1180
- with gr.Tab("📰 Literature Gap Finder"):
1181
  gr.Markdown(
1182
  "Visualize publication trends over 10 years and detect "
1183
  "years with low research activity."
@@ -1200,7 +1184,7 @@ def build_app():
1200
  a4_btn.click(a4_run, inputs=[a4_cancer, a4_kw], outputs=[a4_chart, a4_gaps])
1201
 
1202
  # ── A5 ──
1203
- with gr.Tab("💊 Druggable Orphans"):
1204
  gr.Markdown(
1205
  "Identify cancer-associated essential genes with **no approved drug** "
1206
  "and **no active clinical trial**."
@@ -1221,8 +1205,8 @@ def build_app():
1221
  )
1222
  a5_btn.click(a5_run, inputs=[a5_cancer], outputs=[a5_table, a5_note])
1223
 
1224
- # ── A6 (RAG Chatbot — injected from chatbot.py) ──
1225
- with gr.Tab("🤖 Research Assistant"):
1226
  gr.Markdown(
1227
  "**RAG-powered research assistant** indexed on 20 curated papers "
1228
  "on LNP delivery, protein corona, and cancer variants.\n\n"
@@ -1248,7 +1232,7 @@ def build_app():
1248
  with gr.Tabs():
1249
 
1250
  # ── B1 ──
1251
- with gr.Tab("🧬 miRNA Explorer"):
1252
  gr.Markdown(SIMULATED_BANNER)
1253
  b1_gene = gr.Dropdown(["BRCA2", "BRCA1", "TP53"], label="Gene", value="TP53")
1254
  b1_btn = gr.Button("🔬 Explore miRNA Interactions", variant="primary")
@@ -1267,7 +1251,7 @@ def build_app():
1267
  b1_btn.click(b1_run, inputs=[b1_gene], outputs=[b1_plot, b1_table])
1268
 
1269
  # ── B2 ──
1270
- with gr.Tab("🎯 siRNA Targets"):
1271
  gr.Markdown(SIMULATED_BANNER)
1272
  b2_cancer = gr.Dropdown(["LUAD", "BRCA", "COAD"], label="Cancer Type", value="LUAD")
1273
  b2_btn = gr.Button("🎯 Simulate siRNA Efficacy", variant="primary")
@@ -1285,7 +1269,7 @@ def build_app():
1285
  b2_btn.click(b2_run, inputs=[b2_cancer], outputs=[b2_plot, b2_table])
1286
 
1287
  # ── B3 ──
1288
- with gr.Tab("🧪 LNP Corona"):
1289
  gr.Markdown(SIMULATED_BANNER)
1290
  with gr.Row():
1291
  b3_peg = gr.Slider(0.5, 5.0, value=1.5, step=0.1, label="PEG mol% (lipid)")
@@ -1316,7 +1300,7 @@ def build_app():
1316
  )
1317
 
1318
  # ── B4 ──
1319
- with gr.Tab("🌊 Flow Corona"):
1320
  gr.Markdown(SIMULATED_BANNER)
1321
  with gr.Row():
1322
  b4_time = gr.Slider(10, 120, value=60, step=5, label="Time range (min)")
@@ -1348,7 +1332,7 @@ def build_app():
1348
  )
1349
 
1350
  # ── B5 ──
1351
- with gr.Tab("🔬 Variant Concepts"):
1352
  gr.Markdown(SIMULATED_BANNER)
1353
  b5_class = gr.Dropdown(
1354
  list(VARIANT_RULES.keys()),
 
25
  from functools import wraps
26
 
27
  # ─────────────────────────────────────────────
28
+ # CACHE SYSTEM (TTL = 24 h)
29
  # ─────────────────────────────────────────────
30
  CACHE_DIR = "./cache"
31
  os.makedirs(CACHE_DIR, exist_ok=True)
 
55
  # RETRY DECORATOR
56
  # ─────────────────────────────────────────────
57
  def retry(max_attempts=3, delay=1):
 
58
  def decorator(func):
59
  @wraps(func)
60
  def wrapper(*args, **kwargs):
 
71
  return decorator
72
 
73
  # ─────────────────────────────────────────────
74
+ # LAB JOURNAL (уніфікована система кодування S1)
75
  # ─────────────────────────────────────────────
76
  JOURNAL_FILE = "./lab_journal.csv"
 
77
  JOURNAL_CATEGORIES = [
78
+ # Real Data Tools (S1-A)
79
+ "S1-A·R2a", # Gray Zones Explorer
80
+ "S1-A·R2b", # Understudied Target Finder
81
+ "S1-A·R1a", # Real Variant Lookup
82
+ "S1-A·R2c", # Literature Gap Finder
83
+ "S1-A·R2d", # Druggable Orphans
84
+ "S1-A·R2e", # Research Assistant (Chatbot)
85
+ # Learning Sandbox
86
+ "S1-B·R1a", # miRNA Explorer
87
+ "S1-B·R2a", # siRNA Targets
88
+ "S1-D·R1a", # LNP Corona
89
+ "S1-D·R2a", # Flow Corona
90
+ "S1-A·R1b", # Variant Concepts
91
  "Manual"
92
  ]
93
 
 
174
 
175
  @retry(max_attempts=3, delay=1)
176
  def pubmed_count(query: str) -> int:
 
177
  cached = cache_get("pubmed_count", query)
178
  if cached is not None:
179
  return cached
180
  try:
181
+ time.sleep(0.34)
182
  r = requests.get(
183
  f"{PUBMED_BASE}/esearch.fcgi",
184
  params={"db": "pubmed", "term": query, "rettype": "count", "retmode": "json"},
 
189
  count = int(data.get("esearchresult", {}).get("count", 0))
190
  cache_set("pubmed_count", query, count)
191
  return count
192
+ except Exception:
 
 
 
 
 
 
 
193
  return -1
194
 
195
  @retry(max_attempts=3, delay=1)
196
  def pubmed_search(query: str, retmax: int = 10) -> list:
 
197
  cached = cache_get("pubmed_search", f"{query}_{retmax}")
198
  if cached is not None:
199
  return cached
 
214
 
215
  @retry(max_attempts=3, delay=1)
216
  def pubmed_summary(pmids: list) -> list:
 
217
  if not pmids:
218
  return []
219
  cached = cache_get("pubmed_summary", ",".join(pmids))
 
236
 
237
  @retry(max_attempts=3, delay=1)
238
  def ot_query(gql: str, variables: dict = None) -> dict:
 
239
  key = json.dumps({"q": gql, "v": variables}, sort_keys=True)
240
  cached = cache_get("ot_gql", key)
241
  if cached is not None:
 
249
  r.raise_for_status()
250
  data = r.json()
251
  if "errors" in data:
 
252
  return {"error": data["errors"]}
253
  cache_set("ot_gql", key, data)
254
  return data
255
  except Exception as e:
 
256
  return {"error": str(e)}
 
257
  # ─────────────────────────────────────────────
258
+ # TAB A1 — GRAY ZONES EXPLORER (S1-A·R2a)
259
  # ─────────────────────────────────────────────
260
 
261
  def a1_run(cancer_type: str):
 
308
  )
309
 
310
  gaps_md = "\n\n---\n\n".join(gap_cards) if gap_cards else "No data available."
311
+ journal_log("S1-A·R2a", f"cancer={cancer_type}", f"gaps={[p for p,_ in sorted_procs[:5]]}")
312
  source_note = f"*Source: PubMed E-utilities | Date: {today}*"
313
  return img, gaps_md + "\n\n" + source_note
314
 
315
 
316
  # ─────────────────────────────────────────────
317
+ # TAB A2 — UNDERSTUDIED TARGET FINDER (S1-A·R2b)
318
  # ─────────────────────────────────────────────
319
 
320
  DEPMAP_URL = "https://ndownloader.figshare.com/files/40448549" # CRISPR_gene_effect.csv (public)
 
442
  f"and replace `_load_depmap_sample()` in `app.py`."
443
  )
444
  if not result_df.empty:
445
+ journal_log("S1-A·R2b", f"cancer={cancer_type}", f"top_gap={result_df.iloc[0]['Gene']}")
446
  else:
447
+ journal_log("S1-A·R2b", f"cancer={cancer_type}", "no targets found")
448
  return result_df, note
449
 
450
 
451
  # ─────────────────────────────────────────────
452
+ # TAB A3 — REAL VARIANT LOOKUP (S1-A·R1a)
453
  # ─────────────────────────────────────────────
454
 
455
  def a3_run(hgvs: str):
 
570
  )
571
 
572
  result_parts.append(f"\n*Source: ClinVar E-utilities + gnomAD GraphQL | Date: {today}*")
573
+ journal_log("S1-A·R1a", f"hgvs={hgvs}", result_parts[0][:100] if result_parts else "no results")
574
  return "\n\n".join(result_parts)
575
 
576
 
577
  # ─────────────────────────────────────────────
578
+ # TAB A4 — LITERATURE GAP FINDER (S1-A·R2c)
579
  # ─────────────────────────────────────────────
580
 
581
  def a4_run(cancer_type: str, keyword: str):
 
636
 
637
  summary = "\n\n".join(gap_text)
638
  summary += f"\n\n*Source: PubMed E-utilities | Date: {today}*"
639
+ journal_log("S1-A·R2c", f"cancer={cancer_type}, kw={keyword}", summary[:100])
640
  return img, summary
641
 
642
 
643
  # ─────────────────────────────────────────────
644
+ # TAB A5 — DRUGGABLE ORPHANS (S1-A·R2d)
645
  # ─────────────────────────────────────────────
646
 
647
  def a5_run(cancer_type: str):
 
733
  f"*Source: OpenTargets GraphQL + ClinicalTrials.gov v2 | Date: {today}*\n\n"
734
  f"*Orphan = no approved drug (OpenTargets knownDrugs.count = 0)*"
735
  )
736
+ journal_log("S1-A·R2d", f"cancer={cancer_type}", f"orphans={len(df)}")
737
  return df, note
738
 
739
 
 
746
  "for educational purposes only. Results do NOT reflect real experimental outcomes."
747
  )
748
 
749
+ # ── TAB B1 — miRNA Explorer (S1-B·R1a) ──────────────────
750
 
751
  MIRNA_DB = {
752
  "BRCA2": {
 
816
  "Expression log2FC": changes,
817
  })
818
  context = f"\n\n**Cancer Context:** {db['cancer_context']}"
819
+ journal_log("S1-B·R1a", f"gene={gene}", f"top_miRNA={mirnas[0]}")
820
  return img, df.to_markdown(index=False) + context
821
 
822
 
823
+ # ── TAB B2 — siRNA Targets (S1-B·R2a) ───────────────────
824
 
825
  SIRNA_DB = {
826
  "LUAD": {
 
878
  "Off-target Risk": off_risk,
879
  "Delivery Challenge": delivery,
880
  })
881
+ journal_log("S1-B·R2a", f"cancer={cancer}", f"top={targets[0]}")
882
  return img, df.to_markdown(index=False)
883
 
884
 
885
+ # ── TAB B3 — LNP Corona Simulator (S1-D·R1a) ───────────────
886
 
887
  def b3_run(peg_mol_pct: float, ionizable_pct: float, helper_pct: float,
888
  chol_pct: float, particle_size_nm: float, serum_pct: float):
 
934
  + ("High ApoE → enhanced brain/liver targeting via LDLR pathway." if apoe_pct > 25
935
  else "Low ApoE → reduced receptor-mediated uptake.")
936
  )
937
+ journal_log("S1-D·R1a", f"PEG={peg_mol_pct}%,size={particle_size_nm}nm", f"ApoE={apoe_pct:.1f}%")
938
  return img, interpretation
939
 
940
 
941
+ # ── TAB B4 — Flow Corona (Vroman Kinetics) (S1-D·R2a) ──────
942
 
943
  def b4_run(time_points: int, kon_albumin: float, kon_apoe: float,
944
  koff_albumin: float, koff_apoe: float):
 
978
  "The Vroman effect describes sequential protein displacement: "
979
  "abundant proteins (albumin) adsorb first, then are displaced by higher-affinity proteins (ApoE, fibrinogen)."
980
  )
981
+ journal_log("S1-D·R2a", f"kon_alb={kon_albumin},kon_apoe={kon_apoe}", note[:80])
982
  return img, note
983
 
984
 
985
+ # ── TAB B5 — Variant Concepts (S1-A·R1b) ───────────────────
986
 
987
  VARIANT_RULES = {
988
  "Pathogenic": {
 
1032
  f"> ⚠️ SIMULATED — This is a rule-based educational model only. "
1033
  f"Real variant classification requires expert review and full ACMG/AMP criteria evaluation."
1034
  )
1035
+ journal_log("S1-A·R1b", f"class={classification}", output[:100])
1036
  return output
1037
  # ─────────────────────────────────────────────
1038
  # GRADIO UI ASSEMBLY
 
1083
  with gr.Tabs():
1084
 
1085
  # ── A1 ──
1086
+ with gr.Tab("S1-A·R2a · Gray Zones Explorer"):
1087
  gr.Markdown(
1088
  "Identify underexplored biological processes in a cancer type "
1089
  "using live PubMed + OpenTargets data."
 
1104
  a1_btn.click(a1_run, inputs=[a1_cancer], outputs=[a1_heatmap, a1_gaps])
1105
 
1106
  # ── A2 ──
1107
+ with gr.Tab("S1-A·R2b · Understudied Target Finder"):
1108
  gr.Markdown(
1109
  "Find essential genes with high research gap index "
1110
  "(high essentiality, low publication coverage)."
 
1134
  a2_btn.click(a2_run, inputs=[a2_cancer], outputs=[a2_table, a2_note])
1135
 
1136
  # ── A3 ──
1137
+ with gr.Tab("S1-A·R1a · Real Variant Lookup"):
1138
  gr.Markdown(
1139
  "Look up a variant in **ClinVar** and **gnomAD**. "
1140
  "Results are fetched live — never hallucinated."
 
1161
  a3_btn.click(a3_run, inputs=[a3_hgvs], outputs=[a3_result])
1162
 
1163
  # ── A4 ──
1164
+ with gr.Tab("S1-A·R2c · Literature Gap Finder"):
1165
  gr.Markdown(
1166
  "Visualize publication trends over 10 years and detect "
1167
  "years with low research activity."
 
1184
  a4_btn.click(a4_run, inputs=[a4_cancer, a4_kw], outputs=[a4_chart, a4_gaps])
1185
 
1186
  # ── A5 ──
1187
+ with gr.Tab("S1-A·R2d · Druggable Orphans"):
1188
  gr.Markdown(
1189
  "Identify cancer-associated essential genes with **no approved drug** "
1190
  "and **no active clinical trial**."
 
1205
  )
1206
  a5_btn.click(a5_run, inputs=[a5_cancer], outputs=[a5_table, a5_note])
1207
 
1208
+ # ── A6 (RAG Chatbot) ──
1209
+ with gr.Tab("S1-A·R2e · Research Assistant"):
1210
  gr.Markdown(
1211
  "**RAG-powered research assistant** indexed on 20 curated papers "
1212
  "on LNP delivery, protein corona, and cancer variants.\n\n"
 
1232
  with gr.Tabs():
1233
 
1234
  # ── B1 ──
1235
+ with gr.Tab("S1-B·R1a · miRNA Explorer"):
1236
  gr.Markdown(SIMULATED_BANNER)
1237
  b1_gene = gr.Dropdown(["BRCA2", "BRCA1", "TP53"], label="Gene", value="TP53")
1238
  b1_btn = gr.Button("🔬 Explore miRNA Interactions", variant="primary")
 
1251
  b1_btn.click(b1_run, inputs=[b1_gene], outputs=[b1_plot, b1_table])
1252
 
1253
  # ── B2 ──
1254
+ with gr.Tab("S1-B·R2a · siRNA Targets"):
1255
  gr.Markdown(SIMULATED_BANNER)
1256
  b2_cancer = gr.Dropdown(["LUAD", "BRCA", "COAD"], label="Cancer Type", value="LUAD")
1257
  b2_btn = gr.Button("🎯 Simulate siRNA Efficacy", variant="primary")
 
1269
  b2_btn.click(b2_run, inputs=[b2_cancer], outputs=[b2_plot, b2_table])
1270
 
1271
  # ── B3 ──
1272
+ with gr.Tab("S1-D·R1a · LNP Corona"):
1273
  gr.Markdown(SIMULATED_BANNER)
1274
  with gr.Row():
1275
  b3_peg = gr.Slider(0.5, 5.0, value=1.5, step=0.1, label="PEG mol% (lipid)")
 
1300
  )
1301
 
1302
  # ── B4 ──
1303
+ with gr.Tab("S1-D·R2a · Flow Corona"):
1304
  gr.Markdown(SIMULATED_BANNER)
1305
  with gr.Row():
1306
  b4_time = gr.Slider(10, 120, value=60, step=5, label="Time range (min)")
 
1332
  )
1333
 
1334
  # ── B5 ──
1335
+ with gr.Tab("S1-A·R1b · Variant Concepts"):
1336
  gr.Markdown(SIMULATED_BANNER)
1337
  b5_class = gr.Dropdown(
1338
  list(VARIANT_RULES.keys()),