Update app.py
Browse files
app.py
CHANGED
|
@@ -254,12 +254,12 @@ def ot_query(gql: str, variables: dict = None) -> dict:
|
|
| 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):
|
| 262 |
-
"""Build heatmap of biological process × cancer type paper counts."""
|
| 263 |
today = datetime.date.today().isoformat()
|
| 264 |
counts = {}
|
| 265 |
for proc in PROCESSES:
|
|
@@ -267,11 +267,8 @@ def a1_run(cancer_type: str):
|
|
| 267 |
n = pubmed_count(q)
|
| 268 |
counts[proc] = n
|
| 269 |
|
| 270 |
-
# Build single-column dataframe for heatmap
|
| 271 |
df = pd.DataFrame({"process": PROCESSES, cancer_type: [counts[p] for p in PROCESSES]})
|
| 272 |
df = df.set_index("process")
|
| 273 |
-
|
| 274 |
-
# Replace -1 (API error) with NaN
|
| 275 |
df = df.replace(-1, np.nan)
|
| 276 |
|
| 277 |
fig, ax = plt.subplots(figsize=(6, 8), facecolor="white")
|
|
@@ -294,7 +291,6 @@ def a1_run(cancer_type: str):
|
|
| 294 |
img = Image.open(buf)
|
| 295 |
plt.close(fig)
|
| 296 |
|
| 297 |
-
# Top 5 gaps = lowest counts
|
| 298 |
sorted_procs = sorted(
|
| 299 |
[(p, counts[p]) for p in PROCESSES if counts[p] >= 0],
|
| 300 |
key=lambda x: x[1]
|
|
@@ -312,21 +308,18 @@ def a1_run(cancer_type: str):
|
|
| 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"
|
| 321 |
|
| 322 |
_depmap_cache = {}
|
| 323 |
|
| 324 |
def _load_depmap_sample() -> pd.DataFrame:
|
| 325 |
-
"""Load a small DepMap CRISPR gene effect sample (top essential genes)."""
|
| 326 |
global _depmap_cache
|
| 327 |
if "df" in _depmap_cache:
|
| 328 |
return _depmap_cache["df"]
|
| 329 |
-
# Use a curated list of known essential/cancer genes as fallback
|
| 330 |
genes = [
|
| 331 |
"MYC", "KRAS", "TP53", "EGFR", "PTEN", "RB1", "CDKN2A",
|
| 332 |
"PIK3CA", "AKT1", "BRAF", "NRAS", "IDH1", "IDH2", "ARID1A",
|
|
@@ -335,29 +328,22 @@ def _load_depmap_sample() -> pd.DataFrame:
|
|
| 335 |
"FGFR1", "FGFR2", "MET", "ALK", "RET", "ERBB2",
|
| 336 |
"MTOR", "PIK3R1", "STK11", "NF1", "NF2", "TSC1", "TSC2",
|
| 337 |
]
|
| 338 |
-
# Simulated essentiality scores (negative = essential, per DepMap convention)
|
| 339 |
rng = np.random.default_rng(42)
|
| 340 |
scores = rng.uniform(-1.5, 0.3, len(genes))
|
| 341 |
df = pd.DataFrame({"gene": genes, "gene_effect": scores})
|
| 342 |
_depmap_cache["df"] = df
|
| 343 |
return df
|
| 344 |
|
| 345 |
-
|
| 346 |
def a2_run(cancer_type: str):
|
| 347 |
-
"""Find understudied essential targets for a cancer type."""
|
| 348 |
today = datetime.date.today().isoformat()
|
| 349 |
efo = CANCER_EFO.get(cancer_type, "")
|
| 350 |
|
| 351 |
-
# 1. Get top associated genes from OpenTargets
|
| 352 |
gql = """
|
| 353 |
query AssocTargets($efoId: String!, $size: Int!) {
|
| 354 |
disease(efoId: $efoId) {
|
| 355 |
associatedTargets(page: {index: 0, size: $size}) {
|
| 356 |
rows {
|
| 357 |
-
target {
|
| 358 |
-
approvedSymbol
|
| 359 |
-
approvedName
|
| 360 |
-
}
|
| 361 |
score
|
| 362 |
}
|
| 363 |
}
|
|
@@ -375,17 +361,15 @@ def a2_run(cancer_type: str):
|
|
| 375 |
pass
|
| 376 |
|
| 377 |
if not rows_ot:
|
| 378 |
-
return None, f"⚠️ OpenTargets returned no data for {cancer_type}.
|
| 379 |
|
| 380 |
genes_ot = [r["target"]["approvedSymbol"] for r in rows_ot]
|
| 381 |
|
| 382 |
-
# 2. PubMed paper counts per gene
|
| 383 |
paper_counts = {}
|
| 384 |
-
for gene in genes_ot[:20]:
|
| 385 |
q = f'"{gene}" AND "{cancer_type}"[tiab]'
|
| 386 |
paper_counts[gene] = pubmed_count(q)
|
| 387 |
|
| 388 |
-
# 3. Clinical trials count per gene
|
| 389 |
trial_counts = {}
|
| 390 |
for gene in genes_ot[:20]:
|
| 391 |
cached = cache_get("ct_gene", f"{gene}_{cancer_type}")
|
|
@@ -405,11 +389,9 @@ def a2_run(cancer_type: str):
|
|
| 405 |
except Exception:
|
| 406 |
trial_counts[gene] = -1
|
| 407 |
|
| 408 |
-
# 4. DepMap essentiality (raw negative = essential; we report absolute value)
|
| 409 |
depmap_df = _load_depmap_sample()
|
| 410 |
depmap_dict = dict(zip(depmap_df["gene"], depmap_df["gene_effect"]))
|
| 411 |
|
| 412 |
-
# 5. Build result table
|
| 413 |
records = []
|
| 414 |
for gene in genes_ot[:20]:
|
| 415 |
raw_ess = depmap_dict.get(gene, None)
|
|
@@ -419,7 +401,6 @@ def a2_run(cancer_type: str):
|
|
| 419 |
ess_display = "N/A"
|
| 420 |
gap_idx = 0.0
|
| 421 |
else:
|
| 422 |
-
# Invert: positive = more essential
|
| 423 |
ess_inverted = -raw_ess
|
| 424 |
ess_display = f"{ess_inverted:.3f}"
|
| 425 |
papers_safe = max(papers, 0)
|
|
@@ -447,13 +428,11 @@ def a2_run(cancer_type: str):
|
|
| 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):
|
| 456 |
-
"""Look up a variant in ClinVar and gnomAD. Never hallucinate."""
|
| 457 |
today = datetime.date.today().isoformat()
|
| 458 |
hgvs = hgvs.strip()
|
| 459 |
if not hgvs:
|
|
@@ -461,7 +440,7 @@ def a3_run(hgvs: str):
|
|
| 461 |
|
| 462 |
result_parts = []
|
| 463 |
|
| 464 |
-
#
|
| 465 |
clinvar_cached = cache_get("clinvar", hgvs)
|
| 466 |
if clinvar_cached is None:
|
| 467 |
try:
|
|
@@ -479,7 +458,6 @@ def a3_run(hgvs: str):
|
|
| 479 |
clinvar_cached = None
|
| 480 |
|
| 481 |
if clinvar_cached and len(clinvar_cached) > 0:
|
| 482 |
-
# Fetch summary
|
| 483 |
try:
|
| 484 |
time.sleep(0.34)
|
| 485 |
r2 = requests.get(
|
|
@@ -515,7 +493,7 @@ def a3_run(hgvs: str):
|
|
| 515 |
"> ⚠️ Not in database. Do not interpret."
|
| 516 |
)
|
| 517 |
|
| 518 |
-
#
|
| 519 |
gnomad_cached = cache_get("gnomad", hgvs)
|
| 520 |
if gnomad_cached is None:
|
| 521 |
try:
|
|
@@ -573,13 +551,11 @@ def a3_run(hgvs: str):
|
|
| 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):
|
| 582 |
-
"""Papers per year chart + gap detection."""
|
| 583 |
today = datetime.date.today().isoformat()
|
| 584 |
keyword = keyword.strip()
|
| 585 |
if not keyword:
|
|
@@ -594,7 +570,6 @@ def a4_run(cancer_type: str, keyword: str):
|
|
| 594 |
n = pubmed_count(q)
|
| 595 |
counts.append(max(n, 0))
|
| 596 |
|
| 597 |
-
# Gap detection: years with 0 or below-average counts
|
| 598 |
avg = np.mean([c for c in counts if c > 0]) if any(c > 0 for c in counts) else 0
|
| 599 |
gaps = [yr for yr, c in zip(years, counts) if c == 0]
|
| 600 |
low_years = [yr for yr, c in zip(years, counts) if 0 < c < avg * 0.3]
|
|
@@ -639,17 +614,14 @@ def a4_run(cancer_type: str, keyword: str):
|
|
| 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):
|
| 648 |
-
"""Find essential genes with no approved drug and no active trial."""
|
| 649 |
today = datetime.date.today().isoformat()
|
| 650 |
efo = CANCER_EFO.get(cancer_type, "")
|
| 651 |
|
| 652 |
-
# 1. Get associated targets from OpenTargets with tractability info
|
| 653 |
gql = """
|
| 654 |
query DruggableTargets($efoId: String!, $size: Int!) {
|
| 655 |
disease(efoId: $efoId) {
|
|
@@ -658,14 +630,8 @@ def a5_run(cancer_type: str):
|
|
| 658 |
target {
|
| 659 |
approvedSymbol
|
| 660 |
approvedName
|
| 661 |
-
tractability {
|
| 662 |
-
|
| 663 |
-
modality
|
| 664 |
-
value
|
| 665 |
-
}
|
| 666 |
-
knownDrugs {
|
| 667 |
-
count
|
| 668 |
-
}
|
| 669 |
}
|
| 670 |
score
|
| 671 |
}
|
|
@@ -686,7 +652,6 @@ def a5_run(cancer_type: str):
|
|
| 686 |
if not rows_ot:
|
| 687 |
return None, f"⚠️ OpenTargets returned no data for {cancer_type}.\n\n*Source: OpenTargets | Date: {today}*"
|
| 688 |
|
| 689 |
-
# 2. Filter: no known drugs
|
| 690 |
orphan_candidates = []
|
| 691 |
for row in rows_ot:
|
| 692 |
t = row["target"]
|
|
@@ -699,7 +664,6 @@ def a5_run(cancer_type: str):
|
|
| 699 |
if drug_count == 0:
|
| 700 |
orphan_candidates.append({"gene": gene, "name": t.get("approvedName", ""), "ot_score": row["score"]})
|
| 701 |
|
| 702 |
-
# 3. Check ClinicalTrials for each candidate
|
| 703 |
records = []
|
| 704 |
for cand in orphan_candidates[:15]:
|
| 705 |
gene = cand["gene"]
|
|
@@ -736,7 +700,6 @@ def a5_run(cancer_type: str):
|
|
| 736 |
journal_log("S1-A·R2d", f"cancer={cancer_type}", f"orphans={len(df)}")
|
| 737 |
return df, note
|
| 738 |
|
| 739 |
-
|
| 740 |
# ─────────────────────────────────────────────
|
| 741 |
# GROUP B — LEARNING SANDBOX
|
| 742 |
# ─────────────────────────────────────────────
|
|
@@ -787,14 +750,12 @@ def b1_run(gene: str):
|
|
| 787 |
|
| 788 |
fig, axes = plt.subplots(1, 2, figsize=(11, 4), facecolor="white")
|
| 789 |
|
| 790 |
-
# Binding energy bar chart
|
| 791 |
colors_e = ["#d73027" if e < -16 else "#fc8d59" if e < -13 else "#4393c3" for e in energies]
|
| 792 |
axes[0].barh(mirnas, [-e for e in energies], color=colors_e, edgecolor="white")
|
| 793 |
axes[0].set_xlabel("Binding Energy (|kcal/mol|)", fontsize=10)
|
| 794 |
axes[0].set_title(f"Predicted Binding Energy\n{gene} miRNA targets", fontsize=10)
|
| 795 |
axes[0].set_facecolor("white")
|
| 796 |
|
| 797 |
-
# Expression change
|
| 798 |
colors_x = ["#d73027" if c < 0 else "#4393c3" for c in changes]
|
| 799 |
axes[1].barh(mirnas, changes, color=colors_x, edgecolor="white")
|
| 800 |
axes[1].axvline(0, color="black", linewidth=0.8)
|
|
@@ -819,7 +780,6 @@ def b1_run(gene: str):
|
|
| 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 = {
|
|
@@ -881,14 +841,10 @@ def b2_run(cancer: str):
|
|
| 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):
|
| 889 |
-
"""Simulate protein corona composition based on LNP formulation parameters."""
|
| 890 |
-
# Rule-based model (educational only)
|
| 891 |
-
# Higher PEG → less corona; higher ionizable → more ApoE; larger size → more fibrinogen
|
| 892 |
total_lipid = peg_mol_pct + ionizable_pct + helper_pct + chol_pct
|
| 893 |
peg_norm = peg_mol_pct / max(total_lipid, 1)
|
| 894 |
|
|
@@ -907,14 +863,12 @@ def b3_run(peg_mol_pct: float, ionizable_pct: float, helper_pct: float,
|
|
| 907 |
|
| 908 |
fig, axes = plt.subplots(1, 2, figsize=(11, 4), facecolor="white")
|
| 909 |
|
| 910 |
-
# Pie chart
|
| 911 |
labels = list(corona_proteins.keys())
|
| 912 |
sizes = list(corona_proteins.values())
|
| 913 |
colors_pie = plt.cm.Set2(np.linspace(0, 1, len(labels)))
|
| 914 |
axes[0].pie(sizes, labels=labels, colors=colors_pie, autopct="%1.1f%%", startangle=90)
|
| 915 |
axes[0].set_title("Predicted Corona Composition\n(⚠️ SIMULATED)", fontsize=10)
|
| 916 |
|
| 917 |
-
# Bar chart
|
| 918 |
axes[1].bar(labels, sizes, color=colors_pie, edgecolor="white")
|
| 919 |
axes[1].set_ylabel("Relative Abundance", fontsize=10)
|
| 920 |
axes[1].set_title("Corona Protein Fractions", fontsize=10)
|
|
@@ -937,21 +891,15 @@ def b3_run(peg_mol_pct: float, ionizable_pct: float, helper_pct: float,
|
|
| 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):
|
| 945 |
-
"""Simulate Vroman effect: competitive protein adsorption kinetics."""
|
| 946 |
t = np.linspace(0, time_points, 500)
|
| 947 |
|
| 948 |
-
# Simple Langmuir competitive adsorption model (educational)
|
| 949 |
-
# Albumin: fast on, fast off (early corona)
|
| 950 |
-
# ApoE: slow on, slow off (late/hard corona)
|
| 951 |
albumin = (kon_albumin / (kon_albumin + koff_albumin)) * (1 - np.exp(-(kon_albumin + koff_albumin) * t))
|
| 952 |
apoe_delay = np.maximum(0, t - 5)
|
| 953 |
apoe = (kon_apoe / (kon_apoe + koff_apoe)) * (1 - np.exp(-(kon_apoe + koff_apoe) * apoe_delay))
|
| 954 |
-
# Vroman displacement: albumin decreases as ApoE increases
|
| 955 |
albumin_displaced = albumin * np.exp(-apoe * 2)
|
| 956 |
fibrinogen = 0.3 * (1 - np.exp(-0.05 * t)) * np.exp(-apoe * 1.5)
|
| 957 |
|
|
@@ -981,7 +929,6 @@ def b4_run(time_points: int, kon_albumin: float, kon_apoe: float,
|
|
| 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 = {
|
|
@@ -1034,6 +981,7 @@ def b5_run(classification: str):
|
|
| 1034 |
)
|
| 1035 |
journal_log("S1-A·R1b", f"class={classification}", output[:100])
|
| 1036 |
return output
|
|
|
|
| 1037 |
# ─────────────────────────────────────────────
|
| 1038 |
# GRADIO UI ASSEMBLY
|
| 1039 |
# ─────────────────────────────────────────────
|
|
@@ -1050,39 +998,167 @@ body { font-family: 'Inter', sans-serif; }
|
|
| 1050 |
background: #f8f9fa; border-left: 4px solid #d73027;
|
| 1051 |
padding: 10px 14px; margin: 6px 0; border-radius: 4px;
|
| 1052 |
}
|
| 1053 |
-
|
| 1054 |
-
|
| 1055 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1056 |
cursor: pointer !important;
|
| 1057 |
}
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
|
|
|
| 1061 |
}
|
| 1062 |
-
footer { display: none !important; }
|
| 1063 |
|
| 1064 |
-
/*
|
| 1065 |
-
.
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
|
|
|
|
|
|
| 1073 |
cursor: pointer !important;
|
| 1074 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1075 |
|
| 1076 |
-
/*
|
| 1077 |
-
.
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1081 |
}
|
| 1082 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1083 |
"""
|
| 1084 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1085 |
|
|
|
|
| 1086 |
def build_app():
|
| 1087 |
with gr.Blocks(css=CUSTOM_CSS, title="K R&D Lab — Cancer Research Suite") as demo:
|
| 1088 |
gr.Markdown(
|
|
@@ -1092,235 +1168,105 @@ def build_app():
|
|
| 1092 |
)
|
| 1093 |
|
| 1094 |
with gr.Row():
|
| 1095 |
-
#
|
| 1096 |
with gr.Column(scale=4):
|
| 1097 |
-
with gr.Tabs() as
|
|
|
|
|
|
|
|
|
|
| 1098 |
|
| 1099 |
-
# ════════════════════════════════
|
| 1100 |
# GROUP A — REAL DATA TOOLS
|
| 1101 |
-
|
| 1102 |
-
|
| 1103 |
-
|
| 1104 |
-
|
| 1105 |
-
# ── A1 ──
|
| 1106 |
-
with gr.Tab("S1-A·R2a · Gray Zones Explorer"):
|
| 1107 |
-
gr.Markdown(
|
| 1108 |
-
"Identify underexplored biological processes in a cancer type "
|
| 1109 |
-
"using live PubMed + OpenTargets data."
|
| 1110 |
-
)
|
| 1111 |
a1_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
|
| 1112 |
a1_btn = gr.Button("🔍 Explore Gray Zones", variant="primary")
|
| 1113 |
a1_heatmap = gr.Image(label="Research Coverage Heatmap", type="pil")
|
| 1114 |
a1_gaps = gr.Markdown(label="Top 5 Research Gaps")
|
| 1115 |
with gr.Accordion("📖 Learning Mode", open=False):
|
| 1116 |
-
gr.Markdown(
|
| 1117 |
-
"**What is a research gray zone?**\n\n"
|
| 1118 |
-
"A gray zone is a biological process that is well-studied in other cancers "
|
| 1119 |
-
"but has very few publications in your selected cancer type. "
|
| 1120 |
-
"Low paper counts (red/white cells) indicate potential unexplored territory.\n\n"
|
| 1121 |
-
"**How to use:** Select a rare cancer (e.g. DIPG, MCC) to find the most "
|
| 1122 |
-
"underexplored processes. Cross-reference with Tab A2 to find targetable genes."
|
| 1123 |
-
)
|
| 1124 |
a1_btn.click(a1_run, inputs=[a1_cancer], outputs=[a1_heatmap, a1_gaps])
|
| 1125 |
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
gr.Markdown(
|
| 1129 |
-
"Find essential genes with high research gap index "
|
| 1130 |
-
"(high essentiality, low publication coverage)."
|
| 1131 |
-
)
|
| 1132 |
-
gr.Markdown(
|
| 1133 |
-
"> ⚠️ **Essentiality scores are placeholder estimates** from a "
|
| 1134 |
-
"curated reference gene set — **not real DepMap data**. "
|
| 1135 |
-
"Association scores and paper/trial counts are fetched live. "
|
| 1136 |
-
"For real essentiality values, download `CRISPR_gene_effect.csv` "
|
| 1137 |
-
"from [depmap.org](https://depmap.org/portal/download/all/) and "
|
| 1138 |
-
"replace `_load_depmap_sample()` in `app.py`."
|
| 1139 |
-
)
|
| 1140 |
a2_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
|
| 1141 |
a2_btn = gr.Button("🎯 Find Understudied Targets", variant="primary")
|
| 1142 |
a2_table = gr.Dataframe(label="Target Gap Table", wrap=True)
|
| 1143 |
a2_note = gr.Markdown()
|
| 1144 |
-
with gr.Accordion("📖 Learning Mode", open=False):
|
| 1145 |
-
gr.Markdown(
|
| 1146 |
-
"**Gap Index formula:** `essentiality / log(papers + 1)`\n\n"
|
| 1147 |
-
"- **Essentiality**: inverted DepMap CRISPR gene effect score "
|
| 1148 |
-
"(positive = more essential, per DepMap convention where negative raw = essential)\n"
|
| 1149 |
-
"- **Papers**: PubMed count for gene + cancer type\n"
|
| 1150 |
-
"- **High Gap Index** = essential gene with few publications = high research opportunity\n\n"
|
| 1151 |
-
"**Caution:** Essentiality scores shown here use a curated reference set. "
|
| 1152 |
-
"For full DepMap analysis, download the complete dataset from depmap.org."
|
| 1153 |
-
)
|
| 1154 |
a2_btn.click(a2_run, inputs=[a2_cancer], outputs=[a2_table, a2_note])
|
| 1155 |
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
gr.
|
| 1159 |
-
"Look up a variant in **ClinVar** and **gnomAD**. "
|
| 1160 |
-
"Results are fetched live — never hallucinated."
|
| 1161 |
-
)
|
| 1162 |
-
a3_hgvs = gr.Textbox(
|
| 1163 |
-
label="HGVS Notation",
|
| 1164 |
-
placeholder="e.g. NM_007294.4:c.5266dupC or NM_000546.6:c.524G>A",
|
| 1165 |
-
lines=1
|
| 1166 |
-
)
|
| 1167 |
a3_btn = gr.Button("🔎 Look Up Variant", variant="primary")
|
| 1168 |
a3_result = gr.Markdown()
|
| 1169 |
-
with gr.Accordion("📖 Learning Mode", open=False):
|
| 1170 |
-
gr.Markdown(
|
| 1171 |
-
"**HGVS notation format:**\n"
|
| 1172 |
-
"- `NM_XXXXXX.X:c.NNNN[change]` — coding DNA reference\n"
|
| 1173 |
-
"- `NC_XXXXXX.X:g.NNNN[change]` — genomic reference\n\n"
|
| 1174 |
-
"**ClinVar** classifies variants as: Pathogenic / Likely Pathogenic / "
|
| 1175 |
-
"VUS / Likely Benign / Benign\n\n"
|
| 1176 |
-
"**gnomAD** provides allele frequency (AF) in population cohorts. "
|
| 1177 |
-
"AF > 1% generally suggests benign.\n\n"
|
| 1178 |
-
"**Important:** If a variant is not found, this tool returns "
|
| 1179 |
-
"'Not in database. Do not interpret.' — never a fabricated result."
|
| 1180 |
-
)
|
| 1181 |
a3_btn.click(a3_run, inputs=[a3_hgvs], outputs=[a3_result])
|
| 1182 |
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
gr.Markdown(
|
| 1186 |
-
"Visualize publication trends over 10 years and detect "
|
| 1187 |
-
"years with low research activity."
|
| 1188 |
-
)
|
| 1189 |
with gr.Row():
|
| 1190 |
a4_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
|
| 1191 |
-
a4_kw = gr.Textbox(label="Keyword", placeholder="e.g. ferroptosis"
|
| 1192 |
a4_btn = gr.Button("📊 Analyze Literature Trend", variant="primary")
|
| 1193 |
a4_chart = gr.Image(label="Papers per Year", type="pil")
|
| 1194 |
a4_gaps = gr.Markdown()
|
| 1195 |
-
with gr.Accordion("📖 Learning Mode", open=False):
|
| 1196 |
-
gr.Markdown(
|
| 1197 |
-
"**How to read the chart:**\n"
|
| 1198 |
-
"- 🔵 Blue bars = normal activity\n"
|
| 1199 |
-
"- 🟠 Orange bars = low activity (<30% of average)\n"
|
| 1200 |
-
"- 🔴 Red bars = zero publications (true gap)\n\n"
|
| 1201 |
-
"**Research strategy:** A gap in 2018–2020 followed by a spike in 2022+ "
|
| 1202 |
-
"may indicate a recently emerging field — ideal for early-career researchers."
|
| 1203 |
-
)
|
| 1204 |
a4_btn.click(a4_run, inputs=[a4_cancer, a4_kw], outputs=[a4_chart, a4_gaps])
|
| 1205 |
|
| 1206 |
-
|
| 1207 |
-
|
| 1208 |
-
gr.Markdown(
|
| 1209 |
-
"Identify cancer-associated essential genes with **no approved drug** "
|
| 1210 |
-
"and **no active clinical trial**."
|
| 1211 |
-
)
|
| 1212 |
a5_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
|
| 1213 |
a5_btn = gr.Button("💊 Find Druggable Orphans", variant="primary")
|
| 1214 |
a5_table = gr.Dataframe(label="Orphan Target Table", wrap=True)
|
| 1215 |
a5_note = gr.Markdown()
|
| 1216 |
-
with gr.Accordion("📖 Learning Mode", open=False):
|
| 1217 |
-
gr.Markdown(
|
| 1218 |
-
"**What is a druggable orphan?**\n\n"
|
| 1219 |
-
"A gene that is:\n"
|
| 1220 |
-
"1. Strongly associated with a cancer (high OpenTargets score)\n"
|
| 1221 |
-
"2. Has no approved drug targeting it\n"
|
| 1222 |
-
"3. Has no active clinical trial\n\n"
|
| 1223 |
-
"These represent the highest-opportunity targets for drug discovery. "
|
| 1224 |
-
"Cross-reference with Tab A2 (Gap Index) for prioritization."
|
| 1225 |
-
)
|
| 1226 |
a5_btn.click(a5_run, inputs=[a5_cancer], outputs=[a5_table, a5_note])
|
| 1227 |
|
| 1228 |
-
|
| 1229 |
-
|
| 1230 |
-
gr.Markdown(
|
| 1231 |
-
"**RAG-powered research assistant** indexed on 20 curated papers "
|
| 1232 |
-
"on LNP delivery, protein corona, and cancer variants.\n\n"
|
| 1233 |
-
"*Powered by sentence-transformers + FAISS — no API key required.*"
|
| 1234 |
-
)
|
| 1235 |
try:
|
| 1236 |
from chatbot import build_chatbot_tab
|
| 1237 |
build_chatbot_tab()
|
| 1238 |
except ImportError:
|
| 1239 |
-
gr.Markdown(
|
| 1240 |
-
"⚠️ `chatbot.py` not found. Please ensure it is in the same directory as `app.py`. "
|
| 1241 |
-
"See `chatbot.py` for setup instructions."
|
| 1242 |
-
)
|
| 1243 |
|
| 1244 |
-
# ════════════════════════════════
|
| 1245 |
# GROUP B — LEARNING SANDBOX
|
| 1246 |
-
|
| 1247 |
-
|
| 1248 |
-
gr.
|
| 1249 |
-
"
|
| 1250 |
-
"For educational purposes only. Results do not reflect real experiments."
|
| 1251 |
-
)
|
| 1252 |
-
with gr.Tabs():
|
| 1253 |
-
|
| 1254 |
-
# ── B1 ──
|
| 1255 |
-
with gr.Tab("S1-B·R1a · miRNA Explorer"):
|
| 1256 |
gr.Markdown(SIMULATED_BANNER)
|
| 1257 |
b1_gene = gr.Dropdown(["BRCA2", "BRCA1", "TP53"], label="Gene", value="TP53")
|
| 1258 |
b1_btn = gr.Button("🔬 Explore miRNA Interactions", variant="primary")
|
| 1259 |
b1_plot = gr.Image(label="miRNA Binding & Expression (⚠️ SIMULATED)", type="pil")
|
| 1260 |
b1_table = gr.Markdown()
|
| 1261 |
-
with gr.Accordion("📖 Learning Mode", open=False):
|
| 1262 |
-
gr.Markdown(
|
| 1263 |
-
"**miRNA biology basics:**\n\n"
|
| 1264 |
-
"- miRNAs are ~22 nt non-coding RNAs that bind 3'UTR of mRNAs\n"
|
| 1265 |
-
"- Seed match types: 8mer > 7mer-m8 > 7mer-A1 > 6mer (binding strength)\n"
|
| 1266 |
-
"- Negative binding energy = stronger predicted interaction\n"
|
| 1267 |
-
"- Negative log2FC = miRNA downregulated in tumor\n\n"
|
| 1268 |
-
"**Key concept:** Tumor suppressor genes (BRCA1/2, TP53) are often "
|
| 1269 |
-
"silenced by oncogenic miRNAs (e.g. miR-21, miR-155)."
|
| 1270 |
-
)
|
| 1271 |
b1_btn.click(b1_run, inputs=[b1_gene], outputs=[b1_plot, b1_table])
|
| 1272 |
|
| 1273 |
-
|
| 1274 |
-
with gr.Tab("S1-B·R2a · siRNA Targets"):
|
| 1275 |
gr.Markdown(SIMULATED_BANNER)
|
| 1276 |
b2_cancer = gr.Dropdown(["LUAD", "BRCA", "COAD"], label="Cancer Type", value="LUAD")
|
| 1277 |
b2_btn = gr.Button("🎯 Simulate siRNA Efficacy", variant="primary")
|
| 1278 |
b2_plot = gr.Image(label="siRNA Efficacy (⚠️ SIMULATED)", type="pil")
|
| 1279 |
b2_table = gr.Markdown()
|
| 1280 |
-
with gr.Accordion("📖 Learning Mode", open=False):
|
| 1281 |
-
gr.Markdown(
|
| 1282 |
-
"**siRNA design principles:**\n\n"
|
| 1283 |
-
"- siRNAs are 21-23 nt dsRNA that trigger RISC-mediated mRNA cleavage\n"
|
| 1284 |
-
"- Off-target risk: seed region complementarity to unintended mRNAs\n"
|
| 1285 |
-
"- Delivery challenge: endosomal escape, serum stability, tumor penetration\n\n"
|
| 1286 |
-
"**Clinical context:** KRAS siRNA delivery remains a major challenge "
|
| 1287 |
-
"due to the undruggable nature of the RAS binding pocket."
|
| 1288 |
-
)
|
| 1289 |
b2_btn.click(b2_run, inputs=[b2_cancer], outputs=[b2_plot, b2_table])
|
| 1290 |
|
| 1291 |
-
|
| 1292 |
-
with gr.Tab("S1-D·R1a · LNP Corona"):
|
| 1293 |
gr.Markdown(SIMULATED_BANNER)
|
| 1294 |
with gr.Row():
|
| 1295 |
-
b3_peg = gr.Slider(0.5, 5.0, value=1.5, step=0.1, label="PEG mol%
|
| 1296 |
b3_ion = gr.Slider(10, 60, value=50, step=1, label="Ionizable lipid mol%")
|
| 1297 |
with gr.Row():
|
| 1298 |
b3_helper = gr.Slider(5, 30, value=10, step=1, label="Helper lipid mol%")
|
| 1299 |
b3_chol = gr.Slider(10, 50, value=38, step=1, label="Cholesterol mol%")
|
| 1300 |
with gr.Row():
|
| 1301 |
b3_size = gr.Slider(50, 300, value=100, step=5, label="Particle size (nm)")
|
| 1302 |
-
b3_serum = gr.Slider(0, 100, value=10, step=5, label="Serum %
|
| 1303 |
b3_btn = gr.Button("🧪 Simulate Corona", variant="primary")
|
| 1304 |
b3_plot = gr.Image(label="Corona Composition (⚠️ SIMULATED)", type="pil")
|
| 1305 |
b3_interp = gr.Markdown()
|
| 1306 |
-
|
| 1307 |
-
|
| 1308 |
-
|
| 1309 |
-
|
| 1310 |
-
"- **Hard corona**: tightly bound, long-lived proteins (ApoE, fibrinogen)\n"
|
| 1311 |
-
"- **Soft corona**: loosely bound, rapidly exchanging proteins (albumin)\n"
|
| 1312 |
-
"- **ApoE enrichment** → enhanced brain targeting via LDLR/LRP1 receptors\n"
|
| 1313 |
-
"- **PEG** reduces corona formation but may trigger anti-PEG antibodies\n\n"
|
| 1314 |
-
"**GBM relevance:** ApoE-enriched LNPs show improved BBB crossing in preclinical models."
|
| 1315 |
-
)
|
| 1316 |
-
b3_btn.click(
|
| 1317 |
-
b3_run,
|
| 1318 |
-
inputs=[b3_peg, b3_ion, b3_helper, b3_chol, b3_size, b3_serum],
|
| 1319 |
-
outputs=[b3_plot, b3_interp]
|
| 1320 |
-
)
|
| 1321 |
-
|
| 1322 |
-
# ── B4 ──
|
| 1323 |
-
with gr.Tab("S1-D·R2a · Flow Corona"):
|
| 1324 |
gr.Markdown(SIMULATED_BANNER)
|
| 1325 |
with gr.Row():
|
| 1326 |
b4_time = gr.Slider(10, 120, value=60, step=5, label="Time range (min)")
|
|
@@ -1332,53 +1278,18 @@ def build_app():
|
|
| 1332 |
b4_btn = gr.Button("🌊 Simulate Vroman Kinetics", variant="primary")
|
| 1333 |
b4_plot = gr.Image(label="Vroman Effect (⚠️ SIMULATED)", type="pil")
|
| 1334 |
b4_note = gr.Markdown()
|
| 1335 |
-
|
| 1336 |
-
|
| 1337 |
-
|
| 1338 |
-
|
| 1339 |
-
"(albumin) adsorb first, then are displaced by lower-abundance but higher-affinity "
|
| 1340 |
-
"proteins (fibrinogen, ApoE).\n\n"
|
| 1341 |
-
"**Parameters:**\n"
|
| 1342 |
-
"- **kon**: association rate constant (higher = faster binding)\n"
|
| 1343 |
-
"- **koff**: dissociation rate constant (lower = tighter binding)\n"
|
| 1344 |
-
"- **Kd = koff/kon**: equilibrium dissociation constant\n\n"
|
| 1345 |
-
"**Clinical implication:** The final hard corona (not initial) determines "
|
| 1346 |
-
"nanoparticle fate in vivo."
|
| 1347 |
-
)
|
| 1348 |
-
b4_btn.click(
|
| 1349 |
-
b4_run,
|
| 1350 |
-
inputs=[b4_time, b4_kon_alb, b4_kon_apoe, b4_koff_alb, b4_koff_apoe],
|
| 1351 |
-
outputs=[b4_plot, b4_note]
|
| 1352 |
-
)
|
| 1353 |
-
|
| 1354 |
-
# ── B5 ──
|
| 1355 |
-
with gr.Tab("S1-A·R1b · Variant Concepts"):
|
| 1356 |
gr.Markdown(SIMULATED_BANNER)
|
| 1357 |
-
b5_class = gr.Dropdown(
|
| 1358 |
-
list(VARIANT_RULES.keys()),
|
| 1359 |
-
label="ACMG Classification",
|
| 1360 |
-
value="VUS"
|
| 1361 |
-
)
|
| 1362 |
b5_btn = gr.Button("📋 Explain Classification", variant="primary")
|
| 1363 |
b5_result = gr.Markdown()
|
| 1364 |
-
with gr.Accordion("📖 Learning Mode", open=False):
|
| 1365 |
-
gr.Markdown(
|
| 1366 |
-
"**ACMG/AMP 2015 Classification Framework:**\n\n"
|
| 1367 |
-
"Variants are classified into 5 tiers based on evidence:\n"
|
| 1368 |
-
"1. **Pathogenic** — strong evidence of disease causation\n"
|
| 1369 |
-
"2. **Likely Pathogenic** — >90% probability pathogenic\n"
|
| 1370 |
-
"3. **VUS** — uncertain significance; insufficient evidence\n"
|
| 1371 |
-
"4. **Likely Benign** — >90% probability benign\n"
|
| 1372 |
-
"5. **Benign** — strong evidence of no disease effect\n\n"
|
| 1373 |
-
"**Key codes:** PVS1 (null variant), PS1 (same AA change), "
|
| 1374 |
-
"PM2 (absent from controls), BA1 (MAF >5%)"
|
| 1375 |
-
)
|
| 1376 |
b5_btn.click(b5_run, inputs=[b5_class], outputs=[b5_result])
|
| 1377 |
|
| 1378 |
-
# ════════════════════════════════
|
| 1379 |
# JOURNAL — окрема вкладка
|
| 1380 |
-
|
| 1381 |
-
with gr.Tab("📓 Journal"):
|
| 1382 |
gr.Markdown("## Lab Journal — Full History")
|
| 1383 |
with gr.Row():
|
| 1384 |
journal_filter = gr.Dropdown(
|
|
@@ -1399,38 +1310,35 @@ def build_app():
|
|
| 1399 |
)
|
| 1400 |
journal_filter.change(refresh_journal, inputs=[journal_filter], outputs=journal_display)
|
| 1401 |
|
| 1402 |
-
#
|
| 1403 |
with gr.Column(scale=1, min_width=260):
|
| 1404 |
-
gr.
|
| 1405 |
-
|
| 1406 |
-
|
| 1407 |
-
|
| 1408 |
-
|
| 1409 |
-
|
| 1410 |
-
|
| 1411 |
-
|
| 1412 |
-
|
| 1413 |
-
|
| 1414 |
-
|
| 1415 |
-
|
| 1416 |
-
|
| 1417 |
-
|
| 1418 |
-
|
| 1419 |
-
|
| 1420 |
-
|
| 1421 |
-
|
| 1422 |
-
|
| 1423 |
-
|
| 1424 |
-
|
| 1425 |
-
|
| 1426 |
-
save_btn.click(
|
| 1427 |
-
save_note,
|
| 1428 |
-
inputs=[note_category, note_input],
|
| 1429 |
-
outputs=[save_status, note_input]
|
| 1430 |
-
)
|
| 1431 |
-
clear_note_btn.click(lambda: ("", ""), outputs=[note_input, save_status])
|
| 1432 |
|
| 1433 |
-
|
|
|
|
|
|
|
|
|
|
| 1434 |
with gr.Row():
|
| 1435 |
gr.Markdown("""
|
| 1436 |
---
|
|
@@ -1448,7 +1356,6 @@ def build_app():
|
|
| 1448 |
|
| 1449 |
return demo
|
| 1450 |
|
| 1451 |
-
|
| 1452 |
if __name__ == "__main__":
|
| 1453 |
app = build_app()
|
| 1454 |
app.launch(share=False)
|
|
|
|
| 254 |
return data
|
| 255 |
except Exception as e:
|
| 256 |
return {"error": str(e)}
|
| 257 |
+
|
| 258 |
# ─────────────────────────────────────────────
|
| 259 |
# TAB A1 — GRAY ZONES EXPLORER (S1-A·R2a)
|
| 260 |
# ─────────────────────────────────────────────
|
| 261 |
|
| 262 |
def a1_run(cancer_type: str):
|
|
|
|
| 263 |
today = datetime.date.today().isoformat()
|
| 264 |
counts = {}
|
| 265 |
for proc in PROCESSES:
|
|
|
|
| 267 |
n = pubmed_count(q)
|
| 268 |
counts[proc] = n
|
| 269 |
|
|
|
|
| 270 |
df = pd.DataFrame({"process": PROCESSES, cancer_type: [counts[p] for p in PROCESSES]})
|
| 271 |
df = df.set_index("process")
|
|
|
|
|
|
|
| 272 |
df = df.replace(-1, np.nan)
|
| 273 |
|
| 274 |
fig, ax = plt.subplots(figsize=(6, 8), facecolor="white")
|
|
|
|
| 291 |
img = Image.open(buf)
|
| 292 |
plt.close(fig)
|
| 293 |
|
|
|
|
| 294 |
sorted_procs = sorted(
|
| 295 |
[(p, counts[p]) for p in PROCESSES if counts[p] >= 0],
|
| 296 |
key=lambda x: x[1]
|
|
|
|
| 308 |
source_note = f"*Source: PubMed E-utilities | Date: {today}*"
|
| 309 |
return img, gaps_md + "\n\n" + source_note
|
| 310 |
|
|
|
|
| 311 |
# ─────────────────────────────────────────────
|
| 312 |
# TAB A2 — UNDERSTUDIED TARGET FINDER (S1-A·R2b)
|
| 313 |
# ─────────────────────────────────────────────
|
| 314 |
|
| 315 |
+
DEPMAP_URL = "https://ndownloader.figshare.com/files/40448549"
|
| 316 |
|
| 317 |
_depmap_cache = {}
|
| 318 |
|
| 319 |
def _load_depmap_sample() -> pd.DataFrame:
|
|
|
|
| 320 |
global _depmap_cache
|
| 321 |
if "df" in _depmap_cache:
|
| 322 |
return _depmap_cache["df"]
|
|
|
|
| 323 |
genes = [
|
| 324 |
"MYC", "KRAS", "TP53", "EGFR", "PTEN", "RB1", "CDKN2A",
|
| 325 |
"PIK3CA", "AKT1", "BRAF", "NRAS", "IDH1", "IDH2", "ARID1A",
|
|
|
|
| 328 |
"FGFR1", "FGFR2", "MET", "ALK", "RET", "ERBB2",
|
| 329 |
"MTOR", "PIK3R1", "STK11", "NF1", "NF2", "TSC1", "TSC2",
|
| 330 |
]
|
|
|
|
| 331 |
rng = np.random.default_rng(42)
|
| 332 |
scores = rng.uniform(-1.5, 0.3, len(genes))
|
| 333 |
df = pd.DataFrame({"gene": genes, "gene_effect": scores})
|
| 334 |
_depmap_cache["df"] = df
|
| 335 |
return df
|
| 336 |
|
|
|
|
| 337 |
def a2_run(cancer_type: str):
|
|
|
|
| 338 |
today = datetime.date.today().isoformat()
|
| 339 |
efo = CANCER_EFO.get(cancer_type, "")
|
| 340 |
|
|
|
|
| 341 |
gql = """
|
| 342 |
query AssocTargets($efoId: String!, $size: Int!) {
|
| 343 |
disease(efoId: $efoId) {
|
| 344 |
associatedTargets(page: {index: 0, size: $size}) {
|
| 345 |
rows {
|
| 346 |
+
target { approvedSymbol approvedName }
|
|
|
|
|
|
|
|
|
|
| 347 |
score
|
| 348 |
}
|
| 349 |
}
|
|
|
|
| 361 |
pass
|
| 362 |
|
| 363 |
if not rows_ot:
|
| 364 |
+
return None, f"⚠️ OpenTargets returned no data for {cancer_type}.\n\n*Source: OpenTargets | Date: {today}*"
|
| 365 |
|
| 366 |
genes_ot = [r["target"]["approvedSymbol"] for r in rows_ot]
|
| 367 |
|
|
|
|
| 368 |
paper_counts = {}
|
| 369 |
+
for gene in genes_ot[:20]:
|
| 370 |
q = f'"{gene}" AND "{cancer_type}"[tiab]'
|
| 371 |
paper_counts[gene] = pubmed_count(q)
|
| 372 |
|
|
|
|
| 373 |
trial_counts = {}
|
| 374 |
for gene in genes_ot[:20]:
|
| 375 |
cached = cache_get("ct_gene", f"{gene}_{cancer_type}")
|
|
|
|
| 389 |
except Exception:
|
| 390 |
trial_counts[gene] = -1
|
| 391 |
|
|
|
|
| 392 |
depmap_df = _load_depmap_sample()
|
| 393 |
depmap_dict = dict(zip(depmap_df["gene"], depmap_df["gene_effect"]))
|
| 394 |
|
|
|
|
| 395 |
records = []
|
| 396 |
for gene in genes_ot[:20]:
|
| 397 |
raw_ess = depmap_dict.get(gene, None)
|
|
|
|
| 401 |
ess_display = "N/A"
|
| 402 |
gap_idx = 0.0
|
| 403 |
else:
|
|
|
|
| 404 |
ess_inverted = -raw_ess
|
| 405 |
ess_display = f"{ess_inverted:.3f}"
|
| 406 |
papers_safe = max(papers, 0)
|
|
|
|
| 428 |
journal_log("S1-A·R2b", f"cancer={cancer_type}", "no targets found")
|
| 429 |
return result_df, note
|
| 430 |
|
|
|
|
| 431 |
# ─────────────────────────────────────────────
|
| 432 |
# TAB A3 — REAL VARIANT LOOKUP (S1-A·R1a)
|
| 433 |
# ─────────────────────────────────────────────
|
| 434 |
|
| 435 |
def a3_run(hgvs: str):
|
|
|
|
| 436 |
today = datetime.date.today().isoformat()
|
| 437 |
hgvs = hgvs.strip()
|
| 438 |
if not hgvs:
|
|
|
|
| 440 |
|
| 441 |
result_parts = []
|
| 442 |
|
| 443 |
+
# ClinVar
|
| 444 |
clinvar_cached = cache_get("clinvar", hgvs)
|
| 445 |
if clinvar_cached is None:
|
| 446 |
try:
|
|
|
|
| 458 |
clinvar_cached = None
|
| 459 |
|
| 460 |
if clinvar_cached and len(clinvar_cached) > 0:
|
|
|
|
| 461 |
try:
|
| 462 |
time.sleep(0.34)
|
| 463 |
r2 = requests.get(
|
|
|
|
| 493 |
"> ⚠️ Not in database. Do not interpret."
|
| 494 |
)
|
| 495 |
|
| 496 |
+
# gnomAD
|
| 497 |
gnomad_cached = cache_get("gnomad", hgvs)
|
| 498 |
if gnomad_cached is None:
|
| 499 |
try:
|
|
|
|
| 551 |
journal_log("S1-A·R1a", f"hgvs={hgvs}", result_parts[0][:100] if result_parts else "no results")
|
| 552 |
return "\n\n".join(result_parts)
|
| 553 |
|
|
|
|
| 554 |
# ─────────────────────────────────────────────
|
| 555 |
# TAB A4 — LITERATURE GAP FINDER (S1-A·R2c)
|
| 556 |
# ─────────────────────────────────────────────
|
| 557 |
|
| 558 |
def a4_run(cancer_type: str, keyword: str):
|
|
|
|
| 559 |
today = datetime.date.today().isoformat()
|
| 560 |
keyword = keyword.strip()
|
| 561 |
if not keyword:
|
|
|
|
| 570 |
n = pubmed_count(q)
|
| 571 |
counts.append(max(n, 0))
|
| 572 |
|
|
|
|
| 573 |
avg = np.mean([c for c in counts if c > 0]) if any(c > 0 for c in counts) else 0
|
| 574 |
gaps = [yr for yr, c in zip(years, counts) if c == 0]
|
| 575 |
low_years = [yr for yr, c in zip(years, counts) if 0 < c < avg * 0.3]
|
|
|
|
| 614 |
journal_log("S1-A·R2c", f"cancer={cancer_type}, kw={keyword}", summary[:100])
|
| 615 |
return img, summary
|
| 616 |
|
|
|
|
| 617 |
# ─────────────────────────────────────────────
|
| 618 |
# TAB A5 — DRUGGABLE ORPHANS (S1-A·R2d)
|
| 619 |
# ─────────────────────────────────────────────
|
| 620 |
|
| 621 |
def a5_run(cancer_type: str):
|
|
|
|
| 622 |
today = datetime.date.today().isoformat()
|
| 623 |
efo = CANCER_EFO.get(cancer_type, "")
|
| 624 |
|
|
|
|
| 625 |
gql = """
|
| 626 |
query DruggableTargets($efoId: String!, $size: Int!) {
|
| 627 |
disease(efoId: $efoId) {
|
|
|
|
| 630 |
target {
|
| 631 |
approvedSymbol
|
| 632 |
approvedName
|
| 633 |
+
tractability { label modality value }
|
| 634 |
+
knownDrugs { count }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
}
|
| 636 |
score
|
| 637 |
}
|
|
|
|
| 652 |
if not rows_ot:
|
| 653 |
return None, f"⚠️ OpenTargets returned no data for {cancer_type}.\n\n*Source: OpenTargets | Date: {today}*"
|
| 654 |
|
|
|
|
| 655 |
orphan_candidates = []
|
| 656 |
for row in rows_ot:
|
| 657 |
t = row["target"]
|
|
|
|
| 664 |
if drug_count == 0:
|
| 665 |
orphan_candidates.append({"gene": gene, "name": t.get("approvedName", ""), "ot_score": row["score"]})
|
| 666 |
|
|
|
|
| 667 |
records = []
|
| 668 |
for cand in orphan_candidates[:15]:
|
| 669 |
gene = cand["gene"]
|
|
|
|
| 700 |
journal_log("S1-A·R2d", f"cancer={cancer_type}", f"orphans={len(df)}")
|
| 701 |
return df, note
|
| 702 |
|
|
|
|
| 703 |
# ─────────────────────────────────────────────
|
| 704 |
# GROUP B — LEARNING SANDBOX
|
| 705 |
# ─────────────────────────────────────────────
|
|
|
|
| 750 |
|
| 751 |
fig, axes = plt.subplots(1, 2, figsize=(11, 4), facecolor="white")
|
| 752 |
|
|
|
|
| 753 |
colors_e = ["#d73027" if e < -16 else "#fc8d59" if e < -13 else "#4393c3" for e in energies]
|
| 754 |
axes[0].barh(mirnas, [-e for e in energies], color=colors_e, edgecolor="white")
|
| 755 |
axes[0].set_xlabel("Binding Energy (|kcal/mol|)", fontsize=10)
|
| 756 |
axes[0].set_title(f"Predicted Binding Energy\n{gene} miRNA targets", fontsize=10)
|
| 757 |
axes[0].set_facecolor("white")
|
| 758 |
|
|
|
|
| 759 |
colors_x = ["#d73027" if c < 0 else "#4393c3" for c in changes]
|
| 760 |
axes[1].barh(mirnas, changes, color=colors_x, edgecolor="white")
|
| 761 |
axes[1].axvline(0, color="black", linewidth=0.8)
|
|
|
|
| 780 |
journal_log("S1-B·R1a", f"gene={gene}", f"top_miRNA={mirnas[0]}")
|
| 781 |
return img, df.to_markdown(index=False) + context
|
| 782 |
|
|
|
|
| 783 |
# ── TAB B2 — siRNA Targets (S1-B·R2a) ───────────────────
|
| 784 |
|
| 785 |
SIRNA_DB = {
|
|
|
|
| 841 |
journal_log("S1-B·R2a", f"cancer={cancer}", f"top={targets[0]}")
|
| 842 |
return img, df.to_markdown(index=False)
|
| 843 |
|
|
|
|
| 844 |
# ── TAB B3 — LNP Corona Simulator (S1-D·R1a) ───────────────
|
| 845 |
|
| 846 |
def b3_run(peg_mol_pct: float, ionizable_pct: float, helper_pct: float,
|
| 847 |
chol_pct: float, particle_size_nm: float, serum_pct: float):
|
|
|
|
|
|
|
|
|
|
| 848 |
total_lipid = peg_mol_pct + ionizable_pct + helper_pct + chol_pct
|
| 849 |
peg_norm = peg_mol_pct / max(total_lipid, 1)
|
| 850 |
|
|
|
|
| 863 |
|
| 864 |
fig, axes = plt.subplots(1, 2, figsize=(11, 4), facecolor="white")
|
| 865 |
|
|
|
|
| 866 |
labels = list(corona_proteins.keys())
|
| 867 |
sizes = list(corona_proteins.values())
|
| 868 |
colors_pie = plt.cm.Set2(np.linspace(0, 1, len(labels)))
|
| 869 |
axes[0].pie(sizes, labels=labels, colors=colors_pie, autopct="%1.1f%%", startangle=90)
|
| 870 |
axes[0].set_title("Predicted Corona Composition\n(⚠️ SIMULATED)", fontsize=10)
|
| 871 |
|
|
|
|
| 872 |
axes[1].bar(labels, sizes, color=colors_pie, edgecolor="white")
|
| 873 |
axes[1].set_ylabel("Relative Abundance", fontsize=10)
|
| 874 |
axes[1].set_title("Corona Protein Fractions", fontsize=10)
|
|
|
|
| 891 |
journal_log("S1-D·R1a", f"PEG={peg_mol_pct}%,size={particle_size_nm}nm", f"ApoE={apoe_pct:.1f}%")
|
| 892 |
return img, interpretation
|
| 893 |
|
|
|
|
| 894 |
# ── TAB B4 — Flow Corona (Vroman Kinetics) (S1-D·R2a) ──────
|
| 895 |
|
| 896 |
def b4_run(time_points: int, kon_albumin: float, kon_apoe: float,
|
| 897 |
koff_albumin: float, koff_apoe: float):
|
|
|
|
| 898 |
t = np.linspace(0, time_points, 500)
|
| 899 |
|
|
|
|
|
|
|
|
|
|
| 900 |
albumin = (kon_albumin / (kon_albumin + koff_albumin)) * (1 - np.exp(-(kon_albumin + koff_albumin) * t))
|
| 901 |
apoe_delay = np.maximum(0, t - 5)
|
| 902 |
apoe = (kon_apoe / (kon_apoe + koff_apoe)) * (1 - np.exp(-(kon_apoe + koff_apoe) * apoe_delay))
|
|
|
|
| 903 |
albumin_displaced = albumin * np.exp(-apoe * 2)
|
| 904 |
fibrinogen = 0.3 * (1 - np.exp(-0.05 * t)) * np.exp(-apoe * 1.5)
|
| 905 |
|
|
|
|
| 929 |
journal_log("S1-D·R2a", f"kon_alb={kon_albumin},kon_apoe={kon_apoe}", note[:80])
|
| 930 |
return img, note
|
| 931 |
|
|
|
|
| 932 |
# ── TAB B5 — Variant Concepts (S1-A·R1b) ───────────────────
|
| 933 |
|
| 934 |
VARIANT_RULES = {
|
|
|
|
| 981 |
)
|
| 982 |
journal_log("S1-A·R1b", f"class={classification}", output[:100])
|
| 983 |
return output
|
| 984 |
+
|
| 985 |
# ─────────────────────────────────────────────
|
| 986 |
# GRADIO UI ASSEMBLY
|
| 987 |
# ─────────────────────────────────────────────
|
|
|
|
| 998 |
background: #f8f9fa; border-left: 4px solid #d73027;
|
| 999 |
padding: 10px 14px; margin: 6px 0; border-radius: 4px;
|
| 1000 |
}
|
| 1001 |
+
|
| 1002 |
+
/* Вкладки верхнього рівня з переносом */
|
| 1003 |
+
.tabs-outer .tab-nav {
|
| 1004 |
+
display: flex;
|
| 1005 |
+
flex-wrap: wrap;
|
| 1006 |
+
gap: 2px;
|
| 1007 |
+
}
|
| 1008 |
+
.tabs-outer .tab-nav button {
|
| 1009 |
+
color: #f1f5f9 !important;
|
| 1010 |
+
background: #1e293b !important;
|
| 1011 |
+
font-size: 13px !important;
|
| 1012 |
+
font-weight: 600 !important;
|
| 1013 |
+
padding: 8px 16px !important;
|
| 1014 |
+
border-radius: 6px 6px 0 0 !important;
|
| 1015 |
+
border: 1px solid #334155;
|
| 1016 |
+
border-bottom: none;
|
| 1017 |
+
margin-right: 2px;
|
| 1018 |
+
margin-bottom: 2px;
|
| 1019 |
+
white-space: nowrap;
|
| 1020 |
cursor: pointer !important;
|
| 1021 |
}
|
| 1022 |
+
.tabs-outer .tab-nav button.selected {
|
| 1023 |
+
border-bottom: 3px solid #f97316 !important;
|
| 1024 |
+
color: #f97316 !important;
|
| 1025 |
+
background: #0f172a !important;
|
| 1026 |
}
|
|
|
|
| 1027 |
|
| 1028 |
+
/* Контейнер вкладок всередині основної колонки (R1, R2, ...) */
|
| 1029 |
+
.main-tabs .tab-nav button {
|
| 1030 |
+
color: #8e9bae !important;
|
| 1031 |
+
background: #0f172a !important;
|
| 1032 |
+
font-size: 12px !important;
|
| 1033 |
+
font-weight: 500 !important;
|
| 1034 |
+
padding: 5px 12px !important;
|
| 1035 |
+
border-radius: 4px 4px 0 0 !important;
|
| 1036 |
+
border: 1px solid #334155 !important;
|
| 1037 |
+
border-bottom: none !important;
|
| 1038 |
+
margin-right: 3px !important;
|
| 1039 |
cursor: pointer !important;
|
| 1040 |
}
|
| 1041 |
+
.main-tabs .tab-nav button.selected {
|
| 1042 |
+
color: #38bdf8 !important;
|
| 1043 |
+
background: #1e293b !important;
|
| 1044 |
+
border-color: #38bdf8 !important;
|
| 1045 |
+
border-bottom: none !important;
|
| 1046 |
+
}
|
| 1047 |
+
.main-tabs > .tabitem {
|
| 1048 |
+
background: #1e293b !important;
|
| 1049 |
+
border: 1px solid #334155 !important;
|
| 1050 |
+
border-radius: 0 6px 6px 6px !important;
|
| 1051 |
+
padding: 14px !important;
|
| 1052 |
+
}
|
| 1053 |
|
| 1054 |
+
/* Третій рівень вкладок (a, b) */
|
| 1055 |
+
.sub-tabs .tab-nav button {
|
| 1056 |
+
color: #8e9bae !important;
|
| 1057 |
+
background: #1e293b !important;
|
| 1058 |
+
font-size: 11px !important;
|
| 1059 |
+
padding: 3px 8px !important;
|
| 1060 |
+
border-radius: 3px 3px 0 0 !important;
|
| 1061 |
+
cursor: pointer !important;
|
| 1062 |
+
}
|
| 1063 |
+
.sub-tabs .tab-nav button.selected {
|
| 1064 |
+
color: #f97316 !important;
|
| 1065 |
+
background: #0f172a !important;
|
| 1066 |
+
}
|
| 1067 |
+
|
| 1068 |
+
/* Стиль для badges */
|
| 1069 |
+
.proj-badge {
|
| 1070 |
+
background: #1e293b;
|
| 1071 |
+
border-left: 3px solid #f97316;
|
| 1072 |
+
padding: 8px 12px;
|
| 1073 |
+
border-radius: 0 6px 6px 0;
|
| 1074 |
+
margin-bottom: 8px;
|
| 1075 |
+
}
|
| 1076 |
+
.proj-code {
|
| 1077 |
+
color: #8e9bae;
|
| 1078 |
+
font-size: 11px;
|
| 1079 |
+
}
|
| 1080 |
+
.proj-title {
|
| 1081 |
+
color: #f1f5f9;
|
| 1082 |
+
font-size: 14px;
|
| 1083 |
+
font-weight: 600;
|
| 1084 |
+
}
|
| 1085 |
+
.proj-metric {
|
| 1086 |
+
background: #0f2a3f;
|
| 1087 |
+
color: #38bdf8;
|
| 1088 |
+
padding: 1px 7px;
|
| 1089 |
+
border-radius: 3px;
|
| 1090 |
+
font-size: 10px;
|
| 1091 |
+
margin-left: 6px;
|
| 1092 |
+
}
|
| 1093 |
+
|
| 1094 |
+
/* Бічна панель */
|
| 1095 |
+
.sidebar-journal {
|
| 1096 |
+
background: #1e293b;
|
| 1097 |
+
border: 1px solid #334155;
|
| 1098 |
+
border-radius: 8px;
|
| 1099 |
+
padding: 14px;
|
| 1100 |
+
}
|
| 1101 |
+
.sidebar-journal h3 {
|
| 1102 |
+
color: #f97316;
|
| 1103 |
+
margin-top: 0;
|
| 1104 |
}
|
| 1105 |
|
| 1106 |
+
/* Загальні */
|
| 1107 |
+
h1, h2, h3 { color: #f97316 !important; }
|
| 1108 |
+
.gr-button-primary { background: #f97316 !important; border: none !important; cursor: pointer !important; }
|
| 1109 |
+
.gr-button-secondary { cursor: pointer !important; }
|
| 1110 |
+
footer { display: none !important; }
|
| 1111 |
+
|
| 1112 |
+
/* Курсори */
|
| 1113 |
+
.gr-dropdown, .gr-button, .gr-slider, .gr-radio, .gr-checkbox,
|
| 1114 |
+
.tab-nav button, .gr-accordion, .gr-dataset, .gr-dropdown * {
|
| 1115 |
+
cursor: pointer !important;
|
| 1116 |
+
}
|
| 1117 |
+
.gr-dropdown input, .gr-textbox input, .gr-textarea textarea {
|
| 1118 |
+
cursor: text !important;
|
| 1119 |
+
}
|
| 1120 |
"""
|
| 1121 |
|
| 1122 |
+
# ========== MAP HTML ==========
|
| 1123 |
+
MAP_HTML = f"""
|
| 1124 |
+
<div style="background:{'#1e293b'};padding:22px;border-radius:8px;font-family:monospace;font-size:13px;line-height:2.0;color:{'#f1f5f9'}">
|
| 1125 |
+
<span style="color:{'#f97316'};font-size:16px;font-weight:bold">K R&D Lab · S1 Biomedical</span>
|
| 1126 |
+
<span style="color:{'#8e9bae'};font-size:11px;margin-left:12px">Science Sphere — sub-direction 1</span>
|
| 1127 |
+
<br><br>
|
| 1128 |
+
<span style="color:{'#38bdf8'};font-weight:600">S1-A · PHYLO-GENOMICS</span> — What breaks in DNA<br>
|
| 1129 |
+
├─ <b>S1-A·R1a</b> OpenVariant <span style="color:{'#22c55e'}"> AUC=0.939 ✅</span><br>
|
| 1130 |
+
└─ <b>S1-A·R1b</b> Somatic classifier <span style="color:#f59e0b"> 🔶 In progress</span><br><br>
|
| 1131 |
+
<span style="color:{'#38bdf8'};font-weight:600">S1-B · PHYLO-RNA</span> — How to silence it via RNA<br>
|
| 1132 |
+
├─ <b>S1-B·R1a</b> miRNA silencing <span style="color:{'#22c55e'}"> ✅</span><br>
|
| 1133 |
+
├─ <b>S1-B·R2a</b> siRNA synthetic lethal <span style="color:{'#22c55e'}"> ✅</span><br>
|
| 1134 |
+
├─ <b>S1-B·R3a</b> lncRNA-TREM2 ceRNA <span style="color:{'#22c55e'}"> ✅</span><br>
|
| 1135 |
+
└─ <b>S1-B·R3b</b> ASO designer <span style="color:{'#22c55e'}"> ✅</span><br><br>
|
| 1136 |
+
<span style="color:{'#38bdf8'};font-weight:600">S1-C · PHYLO-DRUG</span> — Which molecule treats it<br>
|
| 1137 |
+
���─ <b>S1-C·R1a</b> FGFR3 RNA-directed compounds <span style="color:{'#22c55e'}"> ✅</span><br>
|
| 1138 |
+
├─ <b>S1-C·R1b</b> Synthetic lethal drug mapping <span style="color:#f59e0b"> 🔶</span><br>
|
| 1139 |
+
└─ <b>S1-C·R2a</b> m6A × Ferroptosis × Circadian <span style="color:{'#8e9bae'}"> 🔴 Frontier</span><br><br>
|
| 1140 |
+
<span style="color:{'#38bdf8'};font-weight:600">S1-D · PHYLO-LNP</span> — How to deliver the drug<br>
|
| 1141 |
+
├─ <b>S1-D·R1a</b> LNP corona (serum) <span style="color:{'#22c55e'}"> AUC=0.791 ✅</span><br>
|
| 1142 |
+
├─ <b>S1-D·R2a</b> Flow corona — Vroman effect <span style="color:{'#22c55e'}"> ✅</span><br>
|
| 1143 |
+
├─ <b>S1-D·R3a</b> LNP brain / BBB / ApoE <span style="color:{'#22c55e'}"> ✅</span><br>
|
| 1144 |
+
├─ <b>S1-D·R4a</b> AutoCorona NLP <span style="color:{'#22c55e'}"> F1=0.71 ✅</span><br>
|
| 1145 |
+
└─ <b>S1-D·R5a</b> CSF · Vitreous · Bone Marrow <span style="color:{'#8e9bae'}"> 🔴 0 prior studies</span><br><br>
|
| 1146 |
+
<span style="color:{'#38bdf8'};font-weight:600">S1-E · PHYLO-BIOMARKERS</span> — Detect without biopsy<br>
|
| 1147 |
+
├─ <b>S1-E·R1a</b> Liquid Biopsy classifier <span style="color:{'#22c55e'}"> AUC=0.992* ✅</span><br>
|
| 1148 |
+
└─ <b>S1-E·R1b</b> Protein panel validator <span style="color:#f59e0b"> 🔶</span><br><br>
|
| 1149 |
+
<span style="color:{'#38bdf8'};font-weight:600">S1-F · PHYLO-RARE</span> — Where almost nobody has looked yet<br>
|
| 1150 |
+
├─ <b>S1-F·R1a</b> DIPG toolkit (H3K27M + CSF LNP + Circadian) <span style="color:#f59e0b"> 🔶</span><br>
|
| 1151 |
+
├─ <b>S1-F·R2a</b> UVM toolkit (GNAQ/GNA11 + vitreous + m6A) <span style="color:#f59e0b"> 🔶</span><br>
|
| 1152 |
+
└─ <b>S1-F·R3a</b> pAML toolkit (FLT3-ITD + BM niche + ferroptosis) <span style="color:#f59e0b"> 🔶</span><br><br>
|
| 1153 |
+
<span style="color:{'#38bdf8'};font-weight:600">S1-G · PHYLO-SIM</span> — 3D Models & Simulations<br>
|
| 1154 |
+
├─ <b>Nanoparticle</b> Interactive 3D model <span style="color:{'#22c55e'}"> ✅</span><br>
|
| 1155 |
+
├─ <b>DNA Helix</b> Double helix visualization <span style="color:{'#22c55e'}"> ✅</span><br>
|
| 1156 |
+
└─ <b>Protein Corona</b> Schematic corona <span style="color:{'#22c55e'}"> ✅</span><br><br>
|
| 1157 |
+
<span style="color:{'#8e9bae'};font-size:11px">✅ Active · 🔶 In progress · 🔴 Planned</span>
|
| 1158 |
+
</div>
|
| 1159 |
+
"""
|
| 1160 |
|
| 1161 |
+
# ========== UI З ДВОМА КОЛОНКАМИ ==========
|
| 1162 |
def build_app():
|
| 1163 |
with gr.Blocks(css=CUSTOM_CSS, title="K R&D Lab — Cancer Research Suite") as demo:
|
| 1164 |
gr.Markdown(
|
|
|
|
| 1168 |
)
|
| 1169 |
|
| 1170 |
with gr.Row():
|
| 1171 |
+
# Основна колонка з вкладками
|
| 1172 |
with gr.Column(scale=4):
|
| 1173 |
+
with gr.Tabs(elem_classes="tabs-outer") as outer_tabs:
|
| 1174 |
+
# 🗺️ Lab Map
|
| 1175 |
+
with gr.TabItem("🗺️ Lab Map"):
|
| 1176 |
+
gr.HTML(MAP_HTML)
|
| 1177 |
|
|
|
|
| 1178 |
# GROUP A — REAL DATA TOOLS
|
| 1179 |
+
with gr.TabItem("🔬 Real Data Tools"):
|
| 1180 |
+
with gr.Tabs(elem_classes="main-tabs") as a_tabs:
|
| 1181 |
+
with gr.TabItem("S1-A·R2a · Gray Zones Explorer"):
|
| 1182 |
+
gr.Markdown("Identify underexplored biological processes...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1183 |
a1_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
|
| 1184 |
a1_btn = gr.Button("🔍 Explore Gray Zones", variant="primary")
|
| 1185 |
a1_heatmap = gr.Image(label="Research Coverage Heatmap", type="pil")
|
| 1186 |
a1_gaps = gr.Markdown(label="Top 5 Research Gaps")
|
| 1187 |
with gr.Accordion("📖 Learning Mode", open=False):
|
| 1188 |
+
gr.Markdown("**What is a research gray zone?** ...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1189 |
a1_btn.click(a1_run, inputs=[a1_cancer], outputs=[a1_heatmap, a1_gaps])
|
| 1190 |
|
| 1191 |
+
with gr.TabItem("S1-A·R2b · Understudied Target Finder"):
|
| 1192 |
+
gr.Markdown("Find essential genes with high research gap index...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1193 |
a2_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
|
| 1194 |
a2_btn = gr.Button("🎯 Find Understudied Targets", variant="primary")
|
| 1195 |
a2_table = gr.Dataframe(label="Target Gap Table", wrap=True)
|
| 1196 |
a2_note = gr.Markdown()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1197 |
a2_btn.click(a2_run, inputs=[a2_cancer], outputs=[a2_table, a2_note])
|
| 1198 |
|
| 1199 |
+
with gr.TabItem("S1-A·R1a · Real Variant Lookup"):
|
| 1200 |
+
gr.Markdown("Look up a variant in **ClinVar** and **gnomAD**...")
|
| 1201 |
+
a3_hgvs = gr.Textbox(label="HGVS Notation", placeholder="e.g. NM_007294.4:c.5266dupC")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1202 |
a3_btn = gr.Button("🔎 Look Up Variant", variant="primary")
|
| 1203 |
a3_result = gr.Markdown()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1204 |
a3_btn.click(a3_run, inputs=[a3_hgvs], outputs=[a3_result])
|
| 1205 |
|
| 1206 |
+
with gr.TabItem("S1-A·R2c · Literature Gap Finder"):
|
| 1207 |
+
gr.Markdown("Visualize publication trends over 10 years...")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1208 |
with gr.Row():
|
| 1209 |
a4_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
|
| 1210 |
+
a4_kw = gr.Textbox(label="Keyword", placeholder="e.g. ferroptosis")
|
| 1211 |
a4_btn = gr.Button("📊 Analyze Literature Trend", variant="primary")
|
| 1212 |
a4_chart = gr.Image(label="Papers per Year", type="pil")
|
| 1213 |
a4_gaps = gr.Markdown()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1214 |
a4_btn.click(a4_run, inputs=[a4_cancer, a4_kw], outputs=[a4_chart, a4_gaps])
|
| 1215 |
|
| 1216 |
+
with gr.TabItem("S1-A·R2d · Druggable Orphans"):
|
| 1217 |
+
gr.Markdown("Identify essential genes with no approved drug and no trial...")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1218 |
a5_cancer = gr.Dropdown(CANCER_TYPES, label="Cancer Type", value="GBM")
|
| 1219 |
a5_btn = gr.Button("💊 Find Druggable Orphans", variant="primary")
|
| 1220 |
a5_table = gr.Dataframe(label="Orphan Target Table", wrap=True)
|
| 1221 |
a5_note = gr.Markdown()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1222 |
a5_btn.click(a5_run, inputs=[a5_cancer], outputs=[a5_table, a5_note])
|
| 1223 |
|
| 1224 |
+
with gr.TabItem("S1-A·R2e · Research Assistant"):
|
| 1225 |
+
gr.Markdown("RAG-powered research assistant...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1226 |
try:
|
| 1227 |
from chatbot import build_chatbot_tab
|
| 1228 |
build_chatbot_tab()
|
| 1229 |
except ImportError:
|
| 1230 |
+
gr.Markdown("⚠️ `chatbot.py` not found.")
|
|
|
|
|
|
|
|
|
|
| 1231 |
|
|
|
|
| 1232 |
# GROUP B — LEARNING SANDBOX
|
| 1233 |
+
with gr.TabItem("📚 Learning Sandbox"):
|
| 1234 |
+
gr.Markdown("> ⚠️ **ALL TABS IN THIS GROUP USE SIMULATED DATA**")
|
| 1235 |
+
with gr.Tabs(elem_classes="main-tabs") as b_tabs:
|
| 1236 |
+
with gr.TabItem("S1-B·R1a · miRNA Explorer"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1237 |
gr.Markdown(SIMULATED_BANNER)
|
| 1238 |
b1_gene = gr.Dropdown(["BRCA2", "BRCA1", "TP53"], label="Gene", value="TP53")
|
| 1239 |
b1_btn = gr.Button("🔬 Explore miRNA Interactions", variant="primary")
|
| 1240 |
b1_plot = gr.Image(label="miRNA Binding & Expression (⚠️ SIMULATED)", type="pil")
|
| 1241 |
b1_table = gr.Markdown()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1242 |
b1_btn.click(b1_run, inputs=[b1_gene], outputs=[b1_plot, b1_table])
|
| 1243 |
|
| 1244 |
+
with gr.TabItem("S1-B·R2a · siRNA Targets"):
|
|
|
|
| 1245 |
gr.Markdown(SIMULATED_BANNER)
|
| 1246 |
b2_cancer = gr.Dropdown(["LUAD", "BRCA", "COAD"], label="Cancer Type", value="LUAD")
|
| 1247 |
b2_btn = gr.Button("🎯 Simulate siRNA Efficacy", variant="primary")
|
| 1248 |
b2_plot = gr.Image(label="siRNA Efficacy (⚠️ SIMULATED)", type="pil")
|
| 1249 |
b2_table = gr.Markdown()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1250 |
b2_btn.click(b2_run, inputs=[b2_cancer], outputs=[b2_plot, b2_table])
|
| 1251 |
|
| 1252 |
+
with gr.TabItem("S1-D·R1a · LNP Corona"):
|
|
|
|
| 1253 |
gr.Markdown(SIMULATED_BANNER)
|
| 1254 |
with gr.Row():
|
| 1255 |
+
b3_peg = gr.Slider(0.5, 5.0, value=1.5, step=0.1, label="PEG mol%")
|
| 1256 |
b3_ion = gr.Slider(10, 60, value=50, step=1, label="Ionizable lipid mol%")
|
| 1257 |
with gr.Row():
|
| 1258 |
b3_helper = gr.Slider(5, 30, value=10, step=1, label="Helper lipid mol%")
|
| 1259 |
b3_chol = gr.Slider(10, 50, value=38, step=1, label="Cholesterol mol%")
|
| 1260 |
with gr.Row():
|
| 1261 |
b3_size = gr.Slider(50, 300, value=100, step=5, label="Particle size (nm)")
|
| 1262 |
+
b3_serum = gr.Slider(0, 100, value=10, step=5, label="Serum %")
|
| 1263 |
b3_btn = gr.Button("🧪 Simulate Corona", variant="primary")
|
| 1264 |
b3_plot = gr.Image(label="Corona Composition (⚠️ SIMULATED)", type="pil")
|
| 1265 |
b3_interp = gr.Markdown()
|
| 1266 |
+
b3_btn.click(b3_run, inputs=[b3_peg, b3_ion, b3_helper, b3_chol, b3_size, b3_serum],
|
| 1267 |
+
outputs=[b3_plot, b3_interp])
|
| 1268 |
+
|
| 1269 |
+
with gr.TabItem("S1-D·R2a · Flow Corona"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1270 |
gr.Markdown(SIMULATED_BANNER)
|
| 1271 |
with gr.Row():
|
| 1272 |
b4_time = gr.Slider(10, 120, value=60, step=5, label="Time range (min)")
|
|
|
|
| 1278 |
b4_btn = gr.Button("🌊 Simulate Vroman Kinetics", variant="primary")
|
| 1279 |
b4_plot = gr.Image(label="Vroman Effect (⚠️ SIMULATED)", type="pil")
|
| 1280 |
b4_note = gr.Markdown()
|
| 1281 |
+
b4_btn.click(b4_run, inputs=[b4_time, b4_kon_alb, b4_kon_apoe, b4_koff_alb, b4_koff_apoe],
|
| 1282 |
+
outputs=[b4_plot, b4_note])
|
| 1283 |
+
|
| 1284 |
+
with gr.TabItem("S1-A·R1b · Variant Concepts"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1285 |
gr.Markdown(SIMULATED_BANNER)
|
| 1286 |
+
b5_class = gr.Dropdown(list(VARIANT_RULES.keys()), label="ACMG Classification", value="VUS")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1287 |
b5_btn = gr.Button("📋 Explain Classification", variant="primary")
|
| 1288 |
b5_result = gr.Markdown()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1289 |
b5_btn.click(b5_run, inputs=[b5_class], outputs=[b5_result])
|
| 1290 |
|
|
|
|
| 1291 |
# JOURNAL — окрема вкладка
|
| 1292 |
+
with gr.TabItem("📓 Journal"):
|
|
|
|
| 1293 |
gr.Markdown("## Lab Journal — Full History")
|
| 1294 |
with gr.Row():
|
| 1295 |
journal_filter = gr.Dropdown(
|
|
|
|
| 1310 |
)
|
| 1311 |
journal_filter.change(refresh_journal, inputs=[journal_filter], outputs=journal_display)
|
| 1312 |
|
| 1313 |
+
# Бічна панель (Quick Note)
|
| 1314 |
with gr.Column(scale=1, min_width=260):
|
| 1315 |
+
with gr.Group(elem_classes="sidebar-journal"):
|
| 1316 |
+
gr.Markdown("## 📓 Lab Journal")
|
| 1317 |
+
note_category = gr.Dropdown(
|
| 1318 |
+
choices=JOURNAL_CATEGORIES,
|
| 1319 |
+
value="Manual",
|
| 1320 |
+
label="Category"
|
| 1321 |
+
)
|
| 1322 |
+
note_input = gr.Textbox(
|
| 1323 |
+
label="Observation",
|
| 1324 |
+
placeholder="Type your note here...",
|
| 1325 |
+
lines=3
|
| 1326 |
+
)
|
| 1327 |
+
with gr.Row():
|
| 1328 |
+
save_btn = gr.Button("💾 Save Note", size="sm", variant="primary")
|
| 1329 |
+
clear_note_btn = gr.Button("🗑️ Clear", size="sm")
|
| 1330 |
+
save_status = gr.Markdown("")
|
| 1331 |
+
|
| 1332 |
+
def save_note(category, note):
|
| 1333 |
+
if note.strip():
|
| 1334 |
+
journal_log(category, "manual note", note, note)
|
| 1335 |
+
return "✅ Note saved.", ""
|
| 1336 |
+
return "⚠️ Note is empty.", ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1337 |
|
| 1338 |
+
save_btn.click(save_note, inputs=[note_category, note_input], outputs=[save_status, note_input])
|
| 1339 |
+
clear_note_btn.click(lambda: ("", ""), outputs=[note_input, save_status])
|
| 1340 |
+
|
| 1341 |
+
# Інтеграція з Learning Playground
|
| 1342 |
with gr.Row():
|
| 1343 |
gr.Markdown("""
|
| 1344 |
---
|
|
|
|
| 1356 |
|
| 1357 |
return demo
|
| 1358 |
|
|
|
|
| 1359 |
if __name__ == "__main__":
|
| 1360 |
app = build_app()
|
| 1361 |
app.launch(share=False)
|