Update app.py
Browse files
app.py
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import pandas as pd
|
| 3 |
import numpy as np
|
| 4 |
-
import json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import matplotlib
|
| 6 |
matplotlib.use("Agg")
|
| 7 |
import matplotlib.pyplot as plt
|
| 8 |
from io import BytesIO
|
| 9 |
from PIL import Image
|
| 10 |
-
from datetime import datetime
|
| 11 |
from pathlib import Path
|
|
|
|
|
|
|
| 12 |
import plotly.graph_objects as go
|
| 13 |
import plotly.express as px
|
| 14 |
import tabulate
|
|
@@ -28,49 +42,127 @@ RED = "#ef4444"
|
|
| 28 |
DIM = "#8e9bae"
|
| 29 |
BORDER = "#334155"
|
| 30 |
|
| 31 |
-
#
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
def
|
| 47 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
try:
|
| 49 |
-
|
| 50 |
-
return "No entries yet."
|
| 51 |
-
df = pd.read_csv(LOG_PATH)
|
| 52 |
if df.empty:
|
| 53 |
return "No entries yet."
|
| 54 |
-
if category
|
| 55 |
-
df = df[df["
|
| 56 |
if df.empty:
|
| 57 |
-
return "No entries for
|
| 58 |
-
|
|
|
|
|
|
|
| 59 |
except Exception as e:
|
| 60 |
-
print(f"Journal
|
| 61 |
-
return "Error
|
| 62 |
-
|
| 63 |
-
def save_note(note, category):
|
| 64 |
-
if note.strip():
|
| 65 |
-
log_entry(category, "manual note", note, note)
|
| 66 |
-
return "Note saved."
|
| 67 |
|
| 68 |
def clear_journal():
|
| 69 |
try:
|
| 70 |
-
if
|
| 71 |
-
|
| 72 |
return "Journal cleared."
|
| 73 |
-
except Exception:
|
|
|
|
| 74 |
return "Error clearing journal."
|
| 75 |
|
| 76 |
# ========== БАЗИ ДАНИХ ==========
|
|
@@ -227,9 +319,7 @@ PAML_BM_LNP = [
|
|
| 227 |
{"Formulation":"DLin-MC3-DPPC","BM_protein":"Vitronectin-rich","Size_nm":91,"Zeta_mV":-2.9,"Marrow_uptake_pct":19,"Priority":"MEDIUM"},
|
| 228 |
{"Formulation":"Cationic-DOTAP-Chol","BM_protein":"Opsonin-heavy","Size_nm":132,"Zeta_mV":+8.1,"Marrow_uptake_pct":8,"Priority":"LOW"},
|
| 229 |
]
|
| 230 |
-
|
| 231 |
-
# ========== ФУНКЦІЇ ПРЕДИКЦІЇ (з обробкою помилок) ==========
|
| 232 |
-
|
| 233 |
def safe_img_from_fig(fig):
|
| 234 |
"""Convert matplotlib figure to PIL Image safely."""
|
| 235 |
try:
|
|
@@ -243,22 +333,23 @@ def safe_img_from_fig(fig):
|
|
| 243 |
plt.close(fig)
|
| 244 |
return Image.new('RGB', (100, 100), color=CARD)
|
| 245 |
|
|
|
|
| 246 |
def predict_mirna(gene):
|
| 247 |
df = pd.DataFrame(MIRNA_DB.get(gene, []))
|
| 248 |
-
|
| 249 |
return df
|
| 250 |
|
| 251 |
def predict_sirna(cancer):
|
| 252 |
df = pd.DataFrame(SIRNA_DB.get(cancer, []))
|
| 253 |
-
|
| 254 |
return df
|
| 255 |
|
| 256 |
def get_lncrna():
|
| 257 |
-
|
| 258 |
return pd.DataFrame(CERNA)
|
| 259 |
|
| 260 |
def get_aso():
|
| 261 |
-
|
| 262 |
return pd.DataFrame(ASO)
|
| 263 |
|
| 264 |
def predict_drug(pocket):
|
|
@@ -275,10 +366,10 @@ def predict_drug(pocket):
|
|
| 275 |
ax.set_title(f"Top compounds — {pocket}", color=TXT, fontsize=10)
|
| 276 |
plt.tight_layout()
|
| 277 |
img = safe_img_from_fig(fig)
|
| 278 |
-
|
| 279 |
return df, img
|
| 280 |
except Exception as e:
|
| 281 |
-
|
| 282 |
return pd.DataFrame(), None
|
| 283 |
|
| 284 |
def predict_variant(hgvs, sift, polyphen, gnomad):
|
|
@@ -295,7 +386,7 @@ def predict_variant(hgvs, sift, polyphen, gnomad):
|
|
| 295 |
conf = "High" if (sift < 0.01 or sift > 0.9) else "Moderate"
|
| 296 |
colour = RED if "Pathogenic" in cls else GRN
|
| 297 |
icon = "⚠️ WARNING" if "Pathogenic" in cls else "✅ OK"
|
| 298 |
-
|
| 299 |
return (
|
| 300 |
f"<div style=\'background:{CARD};padding:16px;border-radius:8px;font-family:sans-serif;color:{TXT}\'>"
|
| 301 |
f"<p style=\'font-size:11px;color:{DIM};margin:0 0 8px\'>S1-A·R1a · OpenVariant</p>"
|
|
@@ -317,7 +408,7 @@ def predict_corona(size, zeta, peg, lipid):
|
|
| 317 |
if size < 100: score += 1
|
| 318 |
dominant = ["ApoE","Albumin","Fibrinogen","Vitronectin","ApoA-I"][min(score, 4)]
|
| 319 |
efficacy = "High" if score >= 4 else "Medium" if score >= 2 else "Low"
|
| 320 |
-
|
| 321 |
return f"**Dominant corona protein:** {dominant}\n\n**Predicted efficacy:** {efficacy}\n\n**Score:** {score}/6"
|
| 322 |
except Exception as e:
|
| 323 |
return f"Error: {str(e)}"
|
|
@@ -341,7 +432,7 @@ def predict_cancer(c1,c2,c3,c4,c5,c6,c7,c8,c9,c10):
|
|
| 341 |
ax.set_title("Protein contributions", color=TXT, fontsize=10)
|
| 342 |
plt.tight_layout()
|
| 343 |
img = safe_img_from_fig(fig)
|
| 344 |
-
|
| 345 |
html_out = (
|
| 346 |
f"<div style=\'background:{CARD};padding:14px;border-radius:8px;font-family:sans-serif;\'>"
|
| 347 |
f"<p style=\'font-size:11px;color:{DIM};margin:0 0 6px\'>S1-E·R1a · Liquid Biopsy</p>"
|
|
@@ -370,7 +461,7 @@ def predict_flow(size, zeta, peg, charge, flow_rate):
|
|
| 370 |
ax.set_title("Vroman Effect — flow vs static", color=TXT, fontsize=9)
|
| 371 |
plt.tight_layout()
|
| 372 |
img = safe_img_from_fig(fig)
|
| 373 |
-
|
| 374 |
return f"**Corona Shift Index: {csi}** — {stability}", img
|
| 375 |
except Exception as e:
|
| 376 |
return f"Error: {str(e)}", None
|
|
@@ -392,7 +483,7 @@ def predict_bbb(smiles, pka, zeta):
|
|
| 392 |
ax.tick_params(colors=TXT)
|
| 393 |
plt.tight_layout()
|
| 394 |
img = safe_img_from_fig(fig)
|
| 395 |
-
|
| 396 |
return f"**Predicted ApoE:** {apoe_pct:.1f}% — {tier}\n\n**BBB Probability:** {bbb_prob:.2f}", img
|
| 397 |
except Exception as e:
|
| 398 |
return f"Error: {str(e)}", None
|
|
@@ -418,15 +509,16 @@ def extract_corona(text):
|
|
| 418 |
if not out["zeta_mv"]: flags.append("zeta_mv not found")
|
| 419 |
if not out["corona_proteins"]: flags.append("no proteins detected")
|
| 420 |
summary = "All key fields extracted" if not flags else " | ".join(flags)
|
| 421 |
-
|
| 422 |
return json.dumps(out, indent=2), summary
|
| 423 |
except Exception as e:
|
| 424 |
return json.dumps({"error": str(e)}), "Extraction error"
|
| 425 |
|
|
|
|
| 426 |
def dipg_variants(sort_by):
|
| 427 |
df = pd.DataFrame(DIPG_VARIANTS).sort_values(
|
| 428 |
"Freq_pct" if sort_by == "Frequency" else "Drug_status", ascending=False)
|
| 429 |
-
|
| 430 |
return df
|
| 431 |
|
| 432 |
def dipg_csf(peg, size):
|
|
@@ -444,14 +536,14 @@ def dipg_csf(peg, size):
|
|
| 444 |
ax.set_title("DIPG — CSF LNP formulations (ApoE%)", color=TXT, fontsize=9)
|
| 445 |
plt.tight_layout()
|
| 446 |
img = safe_img_from_fig(fig)
|
| 447 |
-
|
| 448 |
return df[["Formulation","Size_nm","Zeta_mV","ApoE_pct","BBB_est","Priority"]], img
|
| 449 |
except Exception as e:
|
| 450 |
return pd.DataFrame(), None
|
| 451 |
|
| 452 |
def uvm_variants():
|
| 453 |
df = pd.DataFrame(UVM_VARIANTS)
|
| 454 |
-
|
| 455 |
return df
|
| 456 |
|
| 457 |
def uvm_vitreous():
|
|
@@ -467,7 +559,7 @@ def uvm_vitreous():
|
|
| 467 |
ax.set_title("UVM — LNP retention in vitreous humor", color=TXT, fontsize=9)
|
| 468 |
plt.tight_layout()
|
| 469 |
img = safe_img_from_fig(fig)
|
| 470 |
-
|
| 471 |
return df, img
|
| 472 |
except Exception as e:
|
| 473 |
return pd.DataFrame(), None
|
|
@@ -491,7 +583,7 @@ def paml_ferroptosis(variant):
|
|
| 491 |
ax.set_title(f"pAML · {row['Variant'][:20]}", color=TXT, fontsize=9)
|
| 492 |
plt.tight_layout()
|
| 493 |
img = safe_img_from_fig(fig)
|
| 494 |
-
|
| 495 |
_v = row["Variant"]
|
| 496 |
_p = row["Pathway"]
|
| 497 |
_d = row["Drug_status"]
|
|
@@ -512,29 +604,153 @@ def paml_ferroptosis(variant):
|
|
| 512 |
except Exception as e:
|
| 513 |
return f"<div style='color:{RED}'>Error: {str(e)}</div>", None
|
| 514 |
|
| 515 |
-
# ==========
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
|
| 528 |
-
def
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
|
| 539 |
# ========== 3D моделі ==========
|
| 540 |
def plot_nanoparticle(r, peg):
|
|
@@ -609,11 +825,35 @@ def plot_corona():
|
|
| 609 |
width=500, height=400
|
| 610 |
)
|
| 611 |
return fig
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 612 |
# ========== CSS ==========
|
| 613 |
css = f"""
|
| 614 |
body, .gradio-container {{ background: {BG} !important; color: {TXT} !important; }}
|
| 615 |
|
| 616 |
-
/* Вкладки верхнього рівня
|
| 617 |
.tabs-outer .tab-nav {{
|
| 618 |
display: flex;
|
| 619 |
flex-wrap: wrap;
|
|
@@ -631,6 +871,7 @@ body, .gradio-container {{ background: {BG} !important; color: {TXT} !important;
|
|
| 631 |
margin-right: 2px;
|
| 632 |
margin-bottom: 2px;
|
| 633 |
white-space: nowrap;
|
|
|
|
| 634 |
}}
|
| 635 |
.tabs-outer .tab-nav button.selected {{
|
| 636 |
border-bottom: 3px solid {ACC} !important;
|
|
@@ -649,6 +890,7 @@ body, .gradio-container {{ background: {BG} !important; color: {TXT} !important;
|
|
| 649 |
border: 1px solid {BORDER} !important;
|
| 650 |
border-bottom: none !important;
|
| 651 |
margin-right: 3px !important;
|
|
|
|
| 652 |
}}
|
| 653 |
.main-tabs .tab-nav button.selected {{
|
| 654 |
color: {ACC2} !important;
|
|
@@ -670,6 +912,7 @@ body, .gradio-container {{ background: {BG} !important; color: {TXT} !important;
|
|
| 670 |
font-size: 11px !important;
|
| 671 |
padding: 3px 8px !important;
|
| 672 |
border-radius: 3px 3px 0 0 !important;
|
|
|
|
| 673 |
}}
|
| 674 |
.sub-tabs .tab-nav button.selected {{
|
| 675 |
color: {ACC} !important;
|
|
@@ -702,7 +945,7 @@ body, .gradio-container {{ background: {BG} !important; color: {TXT} !important;
|
|
| 702 |
margin-left: 6px;
|
| 703 |
}}
|
| 704 |
|
| 705 |
-
/* Бічна панель
|
| 706 |
.sidebar-journal {{
|
| 707 |
background: {CARD};
|
| 708 |
border: 1px solid {BORDER};
|
|
@@ -716,19 +959,18 @@ body, .gradio-container {{ background: {BG} !important; color: {TXT} !important;
|
|
| 716 |
|
| 717 |
/* Загальні */
|
| 718 |
h1, h2, h3 {{ color: {ACC} !important; }}
|
| 719 |
-
.gr-button-primary {{ background: {ACC} !important; border: none !important; }}
|
|
|
|
| 720 |
footer {{ display: none !important; }}
|
| 721 |
|
| 722 |
-
/*
|
| 723 |
.gr-dropdown, .gr-button, .gr-slider, .gr-radio, .gr-checkbox,
|
| 724 |
-
.tab-nav button, .gr-accordion, .gr-
|
| 725 |
cursor: pointer !important;
|
| 726 |
}}
|
| 727 |
-
|
| 728 |
-
.gr-textbox input, .gr-textarea textarea {{
|
| 729 |
cursor: text !important;
|
| 730 |
}}
|
| 731 |
-
|
| 732 |
"""
|
| 733 |
|
| 734 |
# ========== MAP HTML ==========
|
|
@@ -754,10 +996,12 @@ MAP_HTML = f"""
|
|
| 754 |
├─ <b>S1-D·R2a</b> Flow corona — Vroman effect <span style="color:{GRN}"> ✅</span><br>
|
| 755 |
├─ <b>S1-D·R3a</b> LNP brain / BBB / ApoE <span style="color:{GRN}"> ✅</span><br>
|
| 756 |
├─ <b>S1-D·R4a</b> AutoCorona NLP <span style="color:{GRN}"> F1=0.71 ✅</span><br>
|
| 757 |
-
|
|
|
|
| 758 |
<span style="color:{ACC2};font-weight:600">S1-E · PHYLO-BIOMARKERS</span> — Detect without biopsy<br>
|
| 759 |
├─ <b>S1-E·R1a</b> Liquid Biopsy classifier <span style="color:{GRN}"> AUC=0.992* ✅</span><br>
|
| 760 |
-
|
|
|
|
| 761 |
<span style="color:{ACC2};font-weight:600">S1-F · PHYLO-RARE</span> — Where almost nobody has looked yet<br>
|
| 762 |
├─ <b>S1-F·R1a</b> DIPG toolkit (H3K27M + CSF LNP + Circadian) <span style="color:#f59e0b"> 🔶</span><br>
|
| 763 |
├─ <b>S1-F·R2a</b> UVM toolkit (GNAQ/GNA11 + vitreous + m6A) <span style="color:#f59e0b"> 🔶</span><br>
|
|
@@ -770,7 +1014,7 @@ MAP_HTML = f"""
|
|
| 770 |
</div>
|
| 771 |
"""
|
| 772 |
|
| 773 |
-
# ========== UI
|
| 774 |
with gr.Blocks(css=css, title="K R&D Lab · S1 Biomedical") as demo:
|
| 775 |
gr.Markdown(
|
| 776 |
"# 🔬 K R&D Lab · Science Sphere — S1 Biomedical\n"
|
|
@@ -959,7 +1203,7 @@ with gr.Blocks(css=css, title="K R&D Lab · S1 Biomedical") as demo:
|
|
| 959 |
"Incubated in human plasma. Corona: albumin, apolipoprotein E, fibrinogen."
|
| 960 |
]], inputs=[txt])
|
| 961 |
b10.click(extract_corona, txt, [o10j, o10f])
|
| 962 |
-
# R5 · Exotic fluids
|
| 963 |
with gr.TabItem("R5 · Exotic fluids 🔴⭐"):
|
| 964 |
with gr.Tabs(elem_classes="sub-tabs"):
|
| 965 |
with gr.TabItem("R5a · CSF/Vitreous/BM"):
|
|
@@ -970,6 +1214,28 @@ with gr.Blocks(css=css, title="K R&D Lab · S1 Biomedical") as demo:
|
|
| 970 |
"> **Target cancers:** DIPG (CSF) · UVM (vitreous) · pAML (bone marrow)\n\n"
|
| 971 |
"> **Expected timeline:** Q2–Q3 2026"
|
| 972 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
|
| 974 |
# === S1-E · PHYLO-BIOMARKERS ===
|
| 975 |
with gr.TabItem("🩸 S1-E"):
|
|
@@ -1003,6 +1269,28 @@ with gr.Blocks(css=css, title="K R&D Lab · S1 Biomedical") as demo:
|
|
| 1003 |
with gr.TabItem("R1b · Protein Validator 🔶"):
|
| 1004 |
gr.HTML(proj_badge("S1-E·R1b", "Protein Panel Validator", "🔶 In progress"))
|
| 1005 |
gr.Markdown("> Coming next — validates R1a results against GEO plasma proteomics datasets.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1006 |
|
| 1007 |
# === S1-F · PHYLO-RARE ===
|
| 1008 |
with gr.TabItem("🧠 S1-F"):
|
|
@@ -1212,48 +1500,62 @@ with gr.Blocks(css=css, title="K R&D Lab · S1 Biomedical") as demo:
|
|
| 1212 |
| S1-D·R3a | PHYLO-LNP | LNP Brain | pKa 6.2–6.8 |
|
| 1213 |
| S1-E·R1a | PHYLO-BIOMARKERS | Liquid Biopsy | AUC=0.992* |
|
| 1214 |
| S1-D·R4a | PHYLO-LNP | AutoCorona NLP | F1=0.71 |
|
|
|
|
|
|
|
| 1215 |
""")
|
|
|
|
| 1216 |
# === Journal (окрема вкладка) ===
|
| 1217 |
with gr.TabItem("📓 Journal"):
|
| 1218 |
gr.Markdown("## Lab Journal — Full History")
|
| 1219 |
with gr.Row():
|
| 1220 |
-
|
| 1221 |
-
choices=["All"] +
|
| 1222 |
value="All",
|
| 1223 |
-
label="Filter by
|
| 1224 |
)
|
| 1225 |
-
refresh_btn = gr.Button("🔄 Refresh", size="sm")
|
| 1226 |
-
clear_btn = gr.Button("🗑️ Clear Journal", size="sm")
|
| 1227 |
-
journal_display = gr.Markdown(value=
|
|
|
|
|
|
|
|
|
|
| 1228 |
|
| 1229 |
-
refresh_btn.click(
|
| 1230 |
clear_btn.click(clear_journal, [], journal_display).then(
|
| 1231 |
-
|
| 1232 |
)
|
| 1233 |
-
|
| 1234 |
|
| 1235 |
-
# Права колонка – швидке введення нотаток
|
| 1236 |
with gr.Column(scale=1, min_width=260):
|
| 1237 |
with gr.Group(elem_classes="sidebar-journal"):
|
| 1238 |
-
gr.Markdown("##
|
| 1239 |
-
|
| 1240 |
-
choices=
|
| 1241 |
-
value=
|
| 1242 |
-
label="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1243 |
)
|
| 1244 |
-
|
| 1245 |
-
|
|
|
|
| 1246 |
save_status = gr.Markdown("")
|
| 1247 |
-
|
| 1248 |
-
def
|
| 1249 |
if note.strip():
|
| 1250 |
-
|
| 1251 |
-
return "✅ Note saved."
|
| 1252 |
-
return "⚠️ Note is empty."
|
| 1253 |
-
|
| 1254 |
-
save_btn.click(
|
|
|
|
| 1255 |
|
| 1256 |
-
#
|
| 1257 |
with gr.Row():
|
| 1258 |
gr.Markdown("""
|
| 1259 |
---
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
K R&D Lab — Learning Playground
|
| 3 |
+
Author: Oksana Kolisnyk | kosatiks-group.pp.ua
|
| 4 |
+
Repo: github.com/TEZv/K-RnD-Lab-PHYLO-03_2026
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
import gradio as gr
|
| 8 |
import pandas as pd
|
| 9 |
import numpy as np
|
| 10 |
+
import json
|
| 11 |
+
import re
|
| 12 |
+
import csv
|
| 13 |
+
import os
|
| 14 |
+
import time
|
| 15 |
+
import math
|
| 16 |
+
import hashlib
|
| 17 |
+
import datetime
|
| 18 |
import matplotlib
|
| 19 |
matplotlib.use("Agg")
|
| 20 |
import matplotlib.pyplot as plt
|
| 21 |
from io import BytesIO
|
| 22 |
from PIL import Image
|
|
|
|
| 23 |
from pathlib import Path
|
| 24 |
+
from datetime import datetime
|
| 25 |
+
from functools import wraps
|
| 26 |
import plotly.graph_objects as go
|
| 27 |
import plotly.express as px
|
| 28 |
import tabulate
|
|
|
|
| 42 |
DIM = "#8e9bae"
|
| 43 |
BORDER = "#334155"
|
| 44 |
|
| 45 |
+
# ─────────────────────────────────────────────
|
| 46 |
+
# CACHE SYSTEM (TTL = 24 h)
|
| 47 |
+
# ─────────────────────────────────────────────
|
| 48 |
+
CACHE_DIR = "./cache"
|
| 49 |
+
os.makedirs(CACHE_DIR, exist_ok=True)
|
| 50 |
+
CACHE_TTL = 86400 # 24 hours in seconds
|
| 51 |
+
|
| 52 |
+
def _cache_key(endpoint: str, query: str) -> str:
|
| 53 |
+
raw = f"{endpoint}_{query}"
|
| 54 |
+
return hashlib.md5(raw.encode()).hexdigest()
|
| 55 |
+
|
| 56 |
+
def cache_get(endpoint: str, query: str):
|
| 57 |
+
key = _cache_key(endpoint, query)
|
| 58 |
+
path = os.path.join(CACHE_DIR, f"{endpoint}_{key}.json")
|
| 59 |
+
if os.path.exists(path):
|
| 60 |
+
mtime = os.path.getmtime(path)
|
| 61 |
+
if time.time() - mtime < CACHE_TTL:
|
| 62 |
+
with open(path) as f:
|
| 63 |
+
return json.load(f)
|
| 64 |
+
return None
|
| 65 |
+
|
| 66 |
+
def cache_set(endpoint: str, query: str, data):
|
| 67 |
+
key = _cache_key(endpoint, query)
|
| 68 |
+
path = os.path.join(CACHE_DIR, f"{endpoint}_{key}.json")
|
| 69 |
+
with open(path, "w") as f:
|
| 70 |
+
json.dump(data, f)
|
| 71 |
+
|
| 72 |
+
# ─────────────────────────────────────────────
|
| 73 |
+
# RETRY DECORATOR
|
| 74 |
+
# ─────────────────────────────────────────────
|
| 75 |
+
def retry(max_attempts=3, delay=1):
|
| 76 |
+
def decorator(func):
|
| 77 |
+
@wraps(func)
|
| 78 |
+
def wrapper(*args, **kwargs):
|
| 79 |
+
for attempt in range(max_attempts):
|
| 80 |
+
try:
|
| 81 |
+
return func(*args, **kwargs)
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print(f"Attempt {attempt+1} failed for {func.__name__}: {e}")
|
| 84 |
+
if attempt == max_attempts - 1:
|
| 85 |
+
raise
|
| 86 |
+
time.sleep(delay * (attempt + 1))
|
| 87 |
+
return None
|
| 88 |
+
return wrapper
|
| 89 |
+
return decorator
|
| 90 |
+
|
| 91 |
+
# ─────────────────────────────────────────────
|
| 92 |
+
# LAB JOURNAL (уніфікована система кодування S1)
|
| 93 |
+
# ─────────────────────────────────────────────
|
| 94 |
+
JOURNAL_FILE = "./lab_journal.csv"
|
| 95 |
+
JOURNAL_CATEGORIES = [
|
| 96 |
+
# S1-A Genomics
|
| 97 |
+
"S1-A·R1a", # OpenVariant
|
| 98 |
+
"S1-A·R1b", # Somatic Classifier (future)
|
| 99 |
+
# S1-B RNA
|
| 100 |
+
"S1-B·R1a", # BRCA2 miRNA
|
| 101 |
+
"S1-B·R2a", # TP53 siRNA
|
| 102 |
+
"S1-B·R3a", # lncRNA-TREM2
|
| 103 |
+
"S1-B·R3b", # ASO Designer
|
| 104 |
+
# S1-C Drug
|
| 105 |
+
"S1-C·R1a", # FGFR3 RNA Drug
|
| 106 |
+
"S1-C·R1b", # SL Drug Mapping (future)
|
| 107 |
+
"S1-C·R2a", # Frontier (future)
|
| 108 |
+
# S1-D LNP
|
| 109 |
+
"S1-D·R1a", # LNP Corona
|
| 110 |
+
"S1-D·R2a", # Flow Corona
|
| 111 |
+
"S1-D·R3a", # LNP Brain
|
| 112 |
+
"S1-D·R4a", # AutoCorona NLP
|
| 113 |
+
"S1-D·R5a", # CSF/Vitreous/BM (future)
|
| 114 |
+
"S1-D·R6a", # Corona Database (NEW)
|
| 115 |
+
# S1-E Biomarkers
|
| 116 |
+
"S1-E·R1a", # Liquid Biopsy
|
| 117 |
+
"S1-E·R1b", # Protein Validator (future)
|
| 118 |
+
"S1-E·R2a", # Multi-protein Biomarkers (NEW)
|
| 119 |
+
# S1-F Rare
|
| 120 |
+
"S1-F·R1a", # DIPG Toolkit
|
| 121 |
+
"S1-F·R2a", # UVM Toolkit
|
| 122 |
+
"S1-F·R3a", # pAML Toolkit
|
| 123 |
+
# S1-G 3D
|
| 124 |
+
"S1-G·General", # 3D Models
|
| 125 |
+
"Manual"
|
| 126 |
+
]
|
| 127 |
|
| 128 |
+
def journal_log(category: str, action: str, result: str, note: str = ""):
|
| 129 |
+
"""Log an entry with category."""
|
| 130 |
+
ts = datetime.now().isoformat()
|
| 131 |
+
row = [ts, category, action, result[:200], note]
|
| 132 |
+
write_header = not os.path.exists(JOURNAL_FILE)
|
| 133 |
+
with open(JOURNAL_FILE, "a", newline="", encoding="utf-8") as f:
|
| 134 |
+
w = csv.writer(f)
|
| 135 |
+
if write_header:
|
| 136 |
+
w.writerow(["timestamp", "category", "action", "result_summary", "note"])
|
| 137 |
+
w.writerow(row)
|
| 138 |
+
return ts
|
| 139 |
+
|
| 140 |
+
def journal_read(category: str = "All") -> str:
|
| 141 |
+
"""Read journal entries, optionally filtered by category. Returns markdown."""
|
| 142 |
+
if not os.path.exists(JOURNAL_FILE):
|
| 143 |
+
return "No entries yet."
|
| 144 |
try:
|
| 145 |
+
df = pd.read_csv(JOURNAL_FILE)
|
|
|
|
|
|
|
| 146 |
if df.empty:
|
| 147 |
return "No entries yet."
|
| 148 |
+
if category != "All":
|
| 149 |
+
df = df[df["category"] == category]
|
| 150 |
if df.empty:
|
| 151 |
+
return f"No entries for category: {category}"
|
| 152 |
+
df_display = df[["timestamp", "category", "action", "result_summary", "note"]].tail(50)
|
| 153 |
+
df_display.columns = ["Timestamp", "Category", "Action", "Result", "Observation"]
|
| 154 |
+
return df_display.to_markdown(index=False)
|
| 155 |
except Exception as e:
|
| 156 |
+
print(f"Journal read error: {e}")
|
| 157 |
+
return "Error reading journal."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
def clear_journal():
|
| 160 |
try:
|
| 161 |
+
if os.path.exists(JOURNAL_FILE):
|
| 162 |
+
os.remove(JOURNAL_FILE)
|
| 163 |
return "Journal cleared."
|
| 164 |
+
except Exception as e:
|
| 165 |
+
print(f"Clear journal error: {e}")
|
| 166 |
return "Error clearing journal."
|
| 167 |
|
| 168 |
# ========== БАЗИ ДАНИХ ==========
|
|
|
|
| 319 |
{"Formulation":"DLin-MC3-DPPC","BM_protein":"Vitronectin-rich","Size_nm":91,"Zeta_mV":-2.9,"Marrow_uptake_pct":19,"Priority":"MEDIUM"},
|
| 320 |
{"Formulation":"Cationic-DOTAP-Chol","BM_protein":"Opsonin-heavy","Size_nm":132,"Zeta_mV":+8.1,"Marrow_uptake_pct":8,"Priority":"LOW"},
|
| 321 |
]
|
| 322 |
+
# ========== ДОПОМІЖНІ ФУНКЦІЇ ==========
|
|
|
|
|
|
|
| 323 |
def safe_img_from_fig(fig):
|
| 324 |
"""Convert matplotlib figure to PIL Image safely."""
|
| 325 |
try:
|
|
|
|
| 333 |
plt.close(fig)
|
| 334 |
return Image.new('RGB', (100, 100), color=CARD)
|
| 335 |
|
| 336 |
+
# ========== ФУНКЦІЇ ПРЕДИКЦІЇ ==========
|
| 337 |
def predict_mirna(gene):
|
| 338 |
df = pd.DataFrame(MIRNA_DB.get(gene, []))
|
| 339 |
+
journal_log("S1-B·R1a", gene, f"{len(df)} miRNAs")
|
| 340 |
return df
|
| 341 |
|
| 342 |
def predict_sirna(cancer):
|
| 343 |
df = pd.DataFrame(SIRNA_DB.get(cancer, []))
|
| 344 |
+
journal_log("S1-B·R2a", cancer, f"{len(df)} targets")
|
| 345 |
return df
|
| 346 |
|
| 347 |
def get_lncrna():
|
| 348 |
+
journal_log("S1-B·R3a", "load", "ceRNA")
|
| 349 |
return pd.DataFrame(CERNA)
|
| 350 |
|
| 351 |
def get_aso():
|
| 352 |
+
journal_log("S1-B·R3b", "load", "ASO")
|
| 353 |
return pd.DataFrame(ASO)
|
| 354 |
|
| 355 |
def predict_drug(pocket):
|
|
|
|
| 366 |
ax.set_title(f"Top compounds — {pocket}", color=TXT, fontsize=10)
|
| 367 |
plt.tight_layout()
|
| 368 |
img = safe_img_from_fig(fig)
|
| 369 |
+
journal_log("S1-C·R1a", pocket, f"Top: {df.iloc[0]['Compound'] if len(df) else 'none'}")
|
| 370 |
return df, img
|
| 371 |
except Exception as e:
|
| 372 |
+
journal_log("S1-C·R1a", pocket, f"Error: {str(e)}")
|
| 373 |
return pd.DataFrame(), None
|
| 374 |
|
| 375 |
def predict_variant(hgvs, sift, polyphen, gnomad):
|
|
|
|
| 386 |
conf = "High" if (sift < 0.01 or sift > 0.9) else "Moderate"
|
| 387 |
colour = RED if "Pathogenic" in cls else GRN
|
| 388 |
icon = "⚠️ WARNING" if "Pathogenic" in cls else "✅ OK"
|
| 389 |
+
journal_log("S1-A·R1a", hgvs or f"SIFT={sift}", f"{cls} score={score}")
|
| 390 |
return (
|
| 391 |
f"<div style=\'background:{CARD};padding:16px;border-radius:8px;font-family:sans-serif;color:{TXT}\'>"
|
| 392 |
f"<p style=\'font-size:11px;color:{DIM};margin:0 0 8px\'>S1-A·R1a · OpenVariant</p>"
|
|
|
|
| 408 |
if size < 100: score += 1
|
| 409 |
dominant = ["ApoE","Albumin","Fibrinogen","Vitronectin","ApoA-I"][min(score, 4)]
|
| 410 |
efficacy = "High" if score >= 4 else "Medium" if score >= 2 else "Low"
|
| 411 |
+
journal_log("S1-D·R1a", f"size={size},peg={peg},lipid={lipid}", f"dominant={dominant}")
|
| 412 |
return f"**Dominant corona protein:** {dominant}\n\n**Predicted efficacy:** {efficacy}\n\n**Score:** {score}/6"
|
| 413 |
except Exception as e:
|
| 414 |
return f"Error: {str(e)}"
|
|
|
|
| 432 |
ax.set_title("Protein contributions", color=TXT, fontsize=10)
|
| 433 |
plt.tight_layout()
|
| 434 |
img = safe_img_from_fig(fig)
|
| 435 |
+
journal_log("S1-E·R1a", f"CTHRC1={c1},FHL2={c2}", f"{label} {prob:.2f}")
|
| 436 |
html_out = (
|
| 437 |
f"<div style=\'background:{CARD};padding:14px;border-radius:8px;font-family:sans-serif;\'>"
|
| 438 |
f"<p style=\'font-size:11px;color:{DIM};margin:0 0 6px\'>S1-E·R1a · Liquid Biopsy</p>"
|
|
|
|
| 461 |
ax.set_title("Vroman Effect — flow vs static", color=TXT, fontsize=9)
|
| 462 |
plt.tight_layout()
|
| 463 |
img = safe_img_from_fig(fig)
|
| 464 |
+
journal_log("S1-D·R2a", f"flow={flow_rate}", f"CSI={csi}")
|
| 465 |
return f"**Corona Shift Index: {csi}** — {stability}", img
|
| 466 |
except Exception as e:
|
| 467 |
return f"Error: {str(e)}", None
|
|
|
|
| 483 |
ax.tick_params(colors=TXT)
|
| 484 |
plt.tight_layout()
|
| 485 |
img = safe_img_from_fig(fig)
|
| 486 |
+
journal_log("S1-D·R3a", f"pka={pka},zeta={zeta}", f"ApoE={apoe_pct:.1f}%")
|
| 487 |
return f"**Predicted ApoE:** {apoe_pct:.1f}% — {tier}\n\n**BBB Probability:** {bbb_prob:.2f}", img
|
| 488 |
except Exception as e:
|
| 489 |
return f"Error: {str(e)}", None
|
|
|
|
| 509 |
if not out["zeta_mv"]: flags.append("zeta_mv not found")
|
| 510 |
if not out["corona_proteins"]: flags.append("no proteins detected")
|
| 511 |
summary = "All key fields extracted" if not flags else " | ".join(flags)
|
| 512 |
+
journal_log("S1-D·R4a", text[:80], f"proteins={len(out['corona_proteins'])}")
|
| 513 |
return json.dumps(out, indent=2), summary
|
| 514 |
except Exception as e:
|
| 515 |
return json.dumps({"error": str(e)}), "Extraction error"
|
| 516 |
|
| 517 |
+
# ========== ФУНКЦІЇ ДЛЯ РІДКІСНИХ РАКІВ ==========
|
| 518 |
def dipg_variants(sort_by):
|
| 519 |
df = pd.DataFrame(DIPG_VARIANTS).sort_values(
|
| 520 |
"Freq_pct" if sort_by == "Frequency" else "Drug_status", ascending=False)
|
| 521 |
+
journal_log("S1-F·R1a", sort_by, f"{len(df)} variants")
|
| 522 |
return df
|
| 523 |
|
| 524 |
def dipg_csf(peg, size):
|
|
|
|
| 536 |
ax.set_title("DIPG — CSF LNP formulations (ApoE%)", color=TXT, fontsize=9)
|
| 537 |
plt.tight_layout()
|
| 538 |
img = safe_img_from_fig(fig)
|
| 539 |
+
journal_log("S1-F·R1a", f"peg={peg},size={size}", "formulation ranking")
|
| 540 |
return df[["Formulation","Size_nm","Zeta_mV","ApoE_pct","BBB_est","Priority"]], img
|
| 541 |
except Exception as e:
|
| 542 |
return pd.DataFrame(), None
|
| 543 |
|
| 544 |
def uvm_variants():
|
| 545 |
df = pd.DataFrame(UVM_VARIANTS)
|
| 546 |
+
journal_log("S1-F·R2a", "load", f"{len(df)} variants")
|
| 547 |
return df
|
| 548 |
|
| 549 |
def uvm_vitreous():
|
|
|
|
| 559 |
ax.set_title("UVM — LNP retention in vitreous humor", color=TXT, fontsize=9)
|
| 560 |
plt.tight_layout()
|
| 561 |
img = safe_img_from_fig(fig)
|
| 562 |
+
journal_log("S1-F·R2a", "load", "vitreous LNP ranking")
|
| 563 |
return df, img
|
| 564 |
except Exception as e:
|
| 565 |
return pd.DataFrame(), None
|
|
|
|
| 583 |
ax.set_title(f"pAML · {row['Variant'][:20]}", color=TXT, fontsize=9)
|
| 584 |
plt.tight_layout()
|
| 585 |
img = safe_img_from_fig(fig)
|
| 586 |
+
journal_log("S1-F·R3a", variant, f"ferr={ferr_score:.2f}")
|
| 587 |
_v = row["Variant"]
|
| 588 |
_p = row["Pathway"]
|
| 589 |
_d = row["Drug_status"]
|
|
|
|
| 604 |
except Exception as e:
|
| 605 |
return f"<div style='color:{RED}'>Error: {str(e)}</div>", None
|
| 606 |
|
| 607 |
+
# ========== НОВІ ІНСТРУМЕНТИ (з комерційної версії) ==========
|
| 608 |
+
|
| 609 |
+
# --- S1-D·R6a — Corona Database (Protein Corona Atlas) ---
|
| 610 |
+
def load_corona_database():
|
| 611 |
+
"""Завантажує дані про білки з Protein Corona Database (симульовані)."""
|
| 612 |
+
corona_proteins = [
|
| 613 |
+
{"Protein": "Apolipoprotein A-I", "UniProt": "P02647", "Frequency": 0.95, "MW_kDa": 30.8, "Function": "Lipid metabolism"},
|
| 614 |
+
{"Protein": "Apolipoprotein A-II", "UniProt": "P02652", "Frequency": 0.92, "MW_kDa": 11.2, "Function": "Lipid metabolism"},
|
| 615 |
+
{"Protein": "Apolipoprotein E", "UniProt": "P02649", "Frequency": 0.89, "MW_kDa": 36.1, "Function": "Lipid transport, brain targeting"},
|
| 616 |
+
{"Protein": "Apolipoprotein B-100", "UniProt": "P04114", "Frequency": 0.87, "MW_kDa": 515.6, "Function": "LDL component"},
|
| 617 |
+
{"Protein": "Complement C3", "UniProt": "P01024", "Frequency": 0.86, "MW_kDa": 187.0, "Function": "Innate immunity"},
|
| 618 |
+
{"Protein": "Albumin", "UniProt": "P02768", "Frequency": 0.85, "MW_kDa": 66.5, "Function": "Carrier protein"},
|
| 619 |
+
{"Protein": "Fibrinogen alpha chain", "UniProt": "P02671", "Frequency": 0.82, "MW_kDa": 94.9, "Function": "Blood coagulation"},
|
| 620 |
+
{"Protein": "Fibrinogen beta chain", "UniProt": "P02675", "Frequency": 0.81, "MW_kDa": 55.9, "Function": "Blood coagulation"},
|
| 621 |
+
{"Protein": "Fibrinogen gamma chain", "UniProt": "P02679", "Frequency": 0.81, "MW_kDa": 51.5, "Function": "Blood coagulation"},
|
| 622 |
+
{"Protein": "Ig gamma-1 chain", "UniProt": "P01857", "Frequency": 0.78, "MW_kDa": 36.1, "Function": "Immune response"},
|
| 623 |
+
{"Protein": "Clusterin", "UniProt": "P10909", "Frequency": 0.74, "MW_kDa": 52.5, "Function": "Chaperone, apoptosis"},
|
| 624 |
+
{"Protein": "Alpha-2-macroglobulin", "UniProt": "P01023", "Frequency": 0.72, "MW_kDa": 163.2, "Function": "Protease inhibitor"},
|
| 625 |
+
{"Protein": "Vitronectin", "UniProt": "P04004", "Frequency": 0.70, "MW_kDa": 54.3, "Function": "Cell adhesion"},
|
| 626 |
+
{"Protein": "Transferrin", "UniProt": "P02787", "Frequency": 0.68, "MW_kDa": 77.0, "Function": "Iron transport"},
|
| 627 |
+
]
|
| 628 |
+
return pd.DataFrame(corona_proteins)
|
| 629 |
+
|
| 630 |
+
def corona_db_query(np_type="Lipid", size_nm=100, zeta_mv=-5, peg_pct=1.5):
|
| 631 |
+
"""
|
| 632 |
+
Повертає топ-10 білків, що адсорбуються на наночастинках заданого типу.
|
| 633 |
+
Частоти модифікуються залежно від параметрів наночастинки.
|
| 634 |
+
"""
|
| 635 |
+
df = load_corona_database()
|
| 636 |
+
df = df.copy()
|
| 637 |
+
|
| 638 |
+
# Модифікуємо частоти на основі параметрів
|
| 639 |
+
if zeta_mv < -10:
|
| 640 |
+
df.loc[df["Protein"].str.contains("Apolipoprotein E", na=False), "Frequency"] *= 1.2
|
| 641 |
+
elif zeta_mv > 5:
|
| 642 |
+
df.loc[df["Protein"].str.contains("Albumin", na=False), "Frequency"] *= 1.1
|
| 643 |
+
|
| 644 |
+
if size_nm > 150:
|
| 645 |
+
df.loc[df["Function"].str.contains("coagulation", na=False), "Frequency"] *= 1.15
|
| 646 |
+
|
| 647 |
+
peg_factor = max(0.5, 1.0 - peg_pct * 0.2)
|
| 648 |
+
df["Frequency"] *= peg_factor
|
| 649 |
+
df["Frequency"] = df["Frequency"].clip(0, 1)
|
| 650 |
+
df = df.sort_values("Frequency", ascending=False)
|
| 651 |
+
df["Predicted_Conc_nM"] = (df["Frequency"] * 100 / df["MW_kDa"]).round(2)
|
| 652 |
+
|
| 653 |
+
journal_log("S1-D·R6a", f"query: {np_type}, size={size_nm}, zeta={zeta_mv}", f"top_protein={df.iloc[0]['Protein']}")
|
| 654 |
+
return df.head(10)
|
| 655 |
+
|
| 656 |
+
def plot_corona_db(df):
|
| 657 |
+
"""Створює графік топ-10 білків у короні."""
|
| 658 |
+
fig, ax = plt.subplots(figsize=(10, 6), facecolor="white")
|
| 659 |
+
ax.set_facecolor("white")
|
| 660 |
+
colors = plt.cm.Blues(np.linspace(0.3, 0.9, len(df)))
|
| 661 |
+
ax.barh(df["Protein"], df["Frequency"], color=colors)
|
| 662 |
+
ax.set_xlabel("Relative Abundance (Frequency)", fontsize=11)
|
| 663 |
+
ax.set_title("Top 10 Proteins in Nanoparticle Corona", fontsize=12, fontweight="bold")
|
| 664 |
+
ax.invert_yaxis()
|
| 665 |
+
ax.spines["top"].set_visible(False)
|
| 666 |
+
ax.spines["right"].set_visible(False)
|
| 667 |
+
plt.tight_layout()
|
| 668 |
+
buf = BytesIO()
|
| 669 |
+
fig.savefig(buf, format="png", dpi=150, facecolor="white")
|
| 670 |
+
buf.seek(0)
|
| 671 |
+
img = Image.open(buf)
|
| 672 |
+
plt.close(fig)
|
| 673 |
+
return img
|
| 674 |
+
|
| 675 |
+
# --- S1-E·R2a — Multi-protein Biomarkers (XProteome) ---
|
| 676 |
+
DISEASE_BIOMARKERS = {
|
| 677 |
+
"Breast Cancer": {
|
| 678 |
+
"proteins": ["CTHRC1", "FHL2", "LDHA", "P4HA1", "SERPINH1", "CDK1", "MKI67"],
|
| 679 |
+
"specificity": 0.92,
|
| 680 |
+
"sensitivity": 0.89,
|
| 681 |
+
},
|
| 682 |
+
"Lung Cancer": {
|
| 683 |
+
"proteins": ["LDHA", "SERPINH1", "CEACAM5", "CEACAM6", "CYFRA21-1", "PROX1", "SPC24"],
|
| 684 |
+
"specificity": 0.91,
|
| 685 |
+
"sensitivity": 0.88,
|
| 686 |
+
},
|
| 687 |
+
"Colorectal Cancer": {
|
| 688 |
+
"proteins": ["CEACAM5", "CEACAM6", "CTHRC1", "LDHA", "SERPINH1", "MUC1", "MUC4"],
|
| 689 |
+
"specificity": 0.94,
|
| 690 |
+
"sensitivity": 0.90,
|
| 691 |
+
},
|
| 692 |
+
"Prostate Cancer": {
|
| 693 |
+
"proteins": ["KLK3", "KLK2", "AMACR", "PCA3", "GOLM1", "FHL2", "CTHRC1"],
|
| 694 |
+
"specificity": 0.93,
|
| 695 |
+
"sensitivity": 0.91,
|
| 696 |
+
},
|
| 697 |
+
"Alzheimer Disease": {
|
| 698 |
+
"proteins": ["APP", "PSEN1", "PSEN2", "MAPT", "APOE", "CLU", "PICALM"],
|
| 699 |
+
"specificity": 0.89,
|
| 700 |
+
"sensitivity": 0.87,
|
| 701 |
+
},
|
| 702 |
+
"Cardiovascular Disease": {
|
| 703 |
+
"proteins": ["APOA1", "APOB", "APOE", "LDLR", "PCSK9", "FGA", "FGB", "FGG"],
|
| 704 |
+
"specificity": 0.88,
|
| 705 |
+
"sensitivity": 0.86,
|
| 706 |
+
},
|
| 707 |
+
"Parkinson Disease": {
|
| 708 |
+
"proteins": ["SNCA", "LRRK2", "GBA", "PARK7", "PINK1", "HTRA2", "DJ-1"],
|
| 709 |
+
"specificity": 0.90,
|
| 710 |
+
"sensitivity": 0.88,
|
| 711 |
+
},
|
| 712 |
+
"Type 2 Diabetes": {
|
| 713 |
+
"proteins": ["INS", "IRS1", "IRS2", "PPARG", "SLC2A4", "ADIPOQ", "LEP"],
|
| 714 |
+
"specificity": 0.87,
|
| 715 |
+
"sensitivity": 0.85,
|
| 716 |
+
},
|
| 717 |
+
}
|
| 718 |
|
| 719 |
+
def get_biomarker_panel(disease):
|
| 720 |
+
"""Повертає білкову панель для заданої хвороби."""
|
| 721 |
+
if disease in DISEASE_BIOMARKERS:
|
| 722 |
+
data = DISEASE_BIOMARKERS[disease]
|
| 723 |
+
df = pd.DataFrame({
|
| 724 |
+
"Protein": data["proteins"],
|
| 725 |
+
"Role": ["Biomarker"] * len(data["proteins"]),
|
| 726 |
+
"Validation": ["Validated" if i < 5 else "Candidate" for i in range(len(data["proteins"]))],
|
| 727 |
+
"Expression": ["Upregulated" if "C" in p or "K" in p else "Altered" for p in data["proteins"]]
|
| 728 |
+
})
|
| 729 |
+
note = f"**Specificity:** {data['specificity']} | **Sensitivity:** {data['sensitivity']}"
|
| 730 |
+
journal_log("S1-E·R2a", f"disease={disease}", f"panel_size={len(data['proteins'])}")
|
| 731 |
+
return df, note
|
| 732 |
+
else:
|
| 733 |
+
return pd.DataFrame(), "No data for selected disease."
|
| 734 |
+
|
| 735 |
+
def find_common_biomarkers(disease1, disease2):
|
| 736 |
+
"""Знаходить спільні біомаркери для двох хвороб."""
|
| 737 |
+
if disease1 not in DISEASE_BIOMARKERS or disease2 not in DISEASE_BIOMARKERS:
|
| 738 |
+
return pd.DataFrame(), "Disease not found."
|
| 739 |
+
|
| 740 |
+
set1 = set(DISEASE_BIOMARKERS[disease1]["proteins"])
|
| 741 |
+
set2 = set(DISEASE_BIOMARKERS[disease2]["proteins"])
|
| 742 |
+
common = set1.intersection(set2)
|
| 743 |
+
|
| 744 |
+
if common:
|
| 745 |
+
df = pd.DataFrame({
|
| 746 |
+
"Protein": list(common),
|
| 747 |
+
"Present in": f"{disease1} & {disease2}",
|
| 748 |
+
"Potential Role": ["Shared biomarker" for _ in common]
|
| 749 |
+
})
|
| 750 |
+
journal_log("S1-E·R2a", f"common: {disease1} & {disease2}", f"found={len(common)}")
|
| 751 |
+
return df, f"Found {len(common)} common biomarkers."
|
| 752 |
+
else:
|
| 753 |
+
return pd.DataFrame(), "No common biomarkers found."
|
| 754 |
|
| 755 |
# ========== 3D моделі ==========
|
| 756 |
def plot_nanoparticle(r, peg):
|
|
|
|
| 825 |
width=500, height=400
|
| 826 |
)
|
| 827 |
return fig
|
| 828 |
+
|
| 829 |
+
# ========== ДОПОМІЖНІ ФУНКЦІЇ ДЛЯ UI ==========
|
| 830 |
+
def section_header(code, name, tagline, projects_html):
|
| 831 |
+
return (
|
| 832 |
+
f"<div style=\'background:{BG};border:1px solid {BORDER};padding:14px 18px;"
|
| 833 |
+
f"border-radius:8px;margin-bottom:12px;\'>"
|
| 834 |
+
f"<div style=\'display:flex;align-items:baseline;gap:10px;\'>"
|
| 835 |
+
f"<span style=\'color:{ACC2};font-size:16px;font-weight:700\'>{code}</span>"
|
| 836 |
+
f"<span style=\'color:{TXT};font-size:14px;font-weight:600\'>{name}</span>"
|
| 837 |
+
f"<span style=\'color:{DIM};font-size:12px\'>{tagline}</span></div>"
|
| 838 |
+
f"<div style=\'margin-top:8px;font-size:12px;color:{DIM}\'>{projects_html}</div>"
|
| 839 |
+
f"</div>"
|
| 840 |
+
)
|
| 841 |
+
|
| 842 |
+
def proj_badge(code, title, metric=""):
|
| 843 |
+
m = (f"<span style=\'background:#0f2a3f;color:{ACC2};padding:1px 7px;border-radius:3px;"
|
| 844 |
+
f"font-size:10px;margin-left:6px\'>{metric}</span>") if metric else ""
|
| 845 |
+
return (
|
| 846 |
+
f"<div style=\'background:{CARD};border-left:3px solid {ACC};"
|
| 847 |
+
f"padding:8px 12px;border-radius:0 6px 6px 0;margin-bottom:8px;\'>"
|
| 848 |
+
f"<span style=\'color:{DIM};font-size:11px\'>{code}</span>{m}<br>"
|
| 849 |
+
f"<span style=\'color:{TXT};font-size:14px;font-weight:600\'>{title}</span>"
|
| 850 |
+
f"</div>"
|
| 851 |
+
)
|
| 852 |
# ========== CSS ==========
|
| 853 |
css = f"""
|
| 854 |
body, .gradio-container {{ background: {BG} !important; color: {TXT} !important; }}
|
| 855 |
|
| 856 |
+
/* Вкладки верхнього рівня з переносом */
|
| 857 |
.tabs-outer .tab-nav {{
|
| 858 |
display: flex;
|
| 859 |
flex-wrap: wrap;
|
|
|
|
| 871 |
margin-right: 2px;
|
| 872 |
margin-bottom: 2px;
|
| 873 |
white-space: nowrap;
|
| 874 |
+
cursor: pointer !important;
|
| 875 |
}}
|
| 876 |
.tabs-outer .tab-nav button.selected {{
|
| 877 |
border-bottom: 3px solid {ACC} !important;
|
|
|
|
| 890 |
border: 1px solid {BORDER} !important;
|
| 891 |
border-bottom: none !important;
|
| 892 |
margin-right: 3px !important;
|
| 893 |
+
cursor: pointer !important;
|
| 894 |
}}
|
| 895 |
.main-tabs .tab-nav button.selected {{
|
| 896 |
color: {ACC2} !important;
|
|
|
|
| 912 |
font-size: 11px !important;
|
| 913 |
padding: 3px 8px !important;
|
| 914 |
border-radius: 3px 3px 0 0 !important;
|
| 915 |
+
cursor: pointer !important;
|
| 916 |
}}
|
| 917 |
.sub-tabs .tab-nav button.selected {{
|
| 918 |
color: {ACC} !important;
|
|
|
|
| 945 |
margin-left: 6px;
|
| 946 |
}}
|
| 947 |
|
| 948 |
+
/* Бічна панель */
|
| 949 |
.sidebar-journal {{
|
| 950 |
background: {CARD};
|
| 951 |
border: 1px solid {BORDER};
|
|
|
|
| 959 |
|
| 960 |
/* Загальні */
|
| 961 |
h1, h2, h3 {{ color: {ACC} !important; }}
|
| 962 |
+
.gr-button-primary {{ background: {ACC} !important; border: none !important; cursor: pointer !important; }}
|
| 963 |
+
.gr-button-secondary {{ cursor: pointer !important; }}
|
| 964 |
footer {{ display: none !important; }}
|
| 965 |
|
| 966 |
+
/* Курсори */
|
| 967 |
.gr-dropdown, .gr-button, .gr-slider, .gr-radio, .gr-checkbox,
|
| 968 |
+
.tab-nav button, .gr-accordion, .gr-dataset, .gr-dropdown * {{
|
| 969 |
cursor: pointer !important;
|
| 970 |
}}
|
| 971 |
+
.gr-dropdown input, .gr-textbox input, .gr-textarea textarea {{
|
|
|
|
| 972 |
cursor: text !important;
|
| 973 |
}}
|
|
|
|
| 974 |
"""
|
| 975 |
|
| 976 |
# ========== MAP HTML ==========
|
|
|
|
| 996 |
├─ <b>S1-D·R2a</b> Flow corona — Vroman effect <span style="color:{GRN}"> ✅</span><br>
|
| 997 |
├─ <b>S1-D·R3a</b> LNP brain / BBB / ApoE <span style="color:{GRN}"> ✅</span><br>
|
| 998 |
├─ <b>S1-D·R4a</b> AutoCorona NLP <span style="color:{GRN}"> F1=0.71 ✅</span><br>
|
| 999 |
+
├─ <b>S1-D·R5a</b> CSF · Vitreous · Bone Marrow <span style="color:{DIM}"> 🔴 0 prior studies</span><br>
|
| 1000 |
+
└─ <b>S1-D·R6a</b> Corona Database <span style="color:{GRN}"> ✅</span><br><br>
|
| 1001 |
<span style="color:{ACC2};font-weight:600">S1-E · PHYLO-BIOMARKERS</span> — Detect without biopsy<br>
|
| 1002 |
├─ <b>S1-E·R1a</b> Liquid Biopsy classifier <span style="color:{GRN}"> AUC=0.992* ✅</span><br>
|
| 1003 |
+
├─ <b>S1-E·R1b</b> Protein panel validator <span style="color:#f59e0b"> 🔶</span><br>
|
| 1004 |
+
└─ <b>S1-E·R2a</b> Multi-protein biomarkers <span style="color:{GRN}"> ✅</span><br><br>
|
| 1005 |
<span style="color:{ACC2};font-weight:600">S1-F · PHYLO-RARE</span> — Where almost nobody has looked yet<br>
|
| 1006 |
├─ <b>S1-F·R1a</b> DIPG toolkit (H3K27M + CSF LNP + Circadian) <span style="color:#f59e0b"> 🔶</span><br>
|
| 1007 |
├─ <b>S1-F·R2a</b> UVM toolkit (GNAQ/GNA11 + vitreous + m6A) <span style="color:#f59e0b"> 🔶</span><br>
|
|
|
|
| 1014 |
</div>
|
| 1015 |
"""
|
| 1016 |
|
| 1017 |
+
# ========== UI ==========
|
| 1018 |
with gr.Blocks(css=css, title="K R&D Lab · S1 Biomedical") as demo:
|
| 1019 |
gr.Markdown(
|
| 1020 |
"# 🔬 K R&D Lab · Science Sphere — S1 Biomedical\n"
|
|
|
|
| 1203 |
"Incubated in human plasma. Corona: albumin, apolipoprotein E, fibrinogen."
|
| 1204 |
]], inputs=[txt])
|
| 1205 |
b10.click(extract_corona, txt, [o10j, o10f])
|
| 1206 |
+
# R5 · Exotic fluids (future)
|
| 1207 |
with gr.TabItem("R5 · Exotic fluids 🔴⭐"):
|
| 1208 |
with gr.Tabs(elem_classes="sub-tabs"):
|
| 1209 |
with gr.TabItem("R5a · CSF/Vitreous/BM"):
|
|
|
|
| 1214 |
"> **Target cancers:** DIPG (CSF) · UVM (vitreous) · pAML (bone marrow)\n\n"
|
| 1215 |
"> **Expected timeline:** Q2–Q3 2026"
|
| 1216 |
)
|
| 1217 |
+
# R6 · Corona Database (NEW)
|
| 1218 |
+
with gr.TabItem("R6 · Corona Database"):
|
| 1219 |
+
with gr.Tabs(elem_classes="sub-tabs"):
|
| 1220 |
+
with gr.TabItem("S1-D·R6a · Corona Database"):
|
| 1221 |
+
gr.Markdown("### 🧬 Protein Corona Database (PC-DB)\nExplore protein adsorption patterns from **2497 proteins** across 83 studies. Data simulated from [PC-DB](https://pc-db.org/).")
|
| 1222 |
+
with gr.Row():
|
| 1223 |
+
np_type = gr.Dropdown(["Lipid", "Polymeric", "Inorganic", "Metal"], value="Lipid", label="Nanoparticle Type")
|
| 1224 |
+
size_db = gr.Slider(20, 300, value=100, step=5, label="Size (nm)")
|
| 1225 |
+
with gr.Row():
|
| 1226 |
+
zeta_db = gr.Slider(-40, 20, value=-5, step=1, label="Zeta Potential (mV)")
|
| 1227 |
+
peg_db = gr.Slider(0, 5, value=1.5, step=0.1, label="PEG mol%")
|
| 1228 |
+
btn_db = gr.Button("🔍 Query Corona Database", variant="primary")
|
| 1229 |
+
db_table = gr.Dataframe(label="Top 10 Corona Proteins")
|
| 1230 |
+
db_plot = gr.Image(label="Protein Abundance")
|
| 1231 |
+
|
| 1232 |
+
def query_db(np_type, size, zeta, peg):
|
| 1233 |
+
df = corona_db_query(np_type, size, zeta, peg)
|
| 1234 |
+
img = plot_corona_db(df)
|
| 1235 |
+
return df, img
|
| 1236 |
+
|
| 1237 |
+
btn_db.click(query_db, inputs=[np_type, size_db, zeta_db, peg_db], outputs=[db_table, db_plot])
|
| 1238 |
+
gr.Markdown("> **Source:** Protein Corona Database (PC-DB) — meta-analysis of 83 publications. Frequencies adjusted for nanoparticle properties.")
|
| 1239 |
|
| 1240 |
# === S1-E · PHYLO-BIOMARKERS ===
|
| 1241 |
with gr.TabItem("🩸 S1-E"):
|
|
|
|
| 1269 |
with gr.TabItem("R1b · Protein Validator 🔶"):
|
| 1270 |
gr.HTML(proj_badge("S1-E·R1b", "Protein Panel Validator", "🔶 In progress"))
|
| 1271 |
gr.Markdown("> Coming next — validates R1a results against GEO plasma proteomics datasets.")
|
| 1272 |
+
# R2 · Multi-protein biomarkers (NEW)
|
| 1273 |
+
with gr.TabItem("R2 · Multi-protein biomarkers"):
|
| 1274 |
+
with gr.Tabs(elem_classes="sub-tabs"):
|
| 1275 |
+
with gr.TabItem("S1-E·R2a · Multi-protein Biomarkers"):
|
| 1276 |
+
gr.Markdown("### 🔬 Multi-protein Biomarker Panels (XProteome)\nIdentify shared protein signatures across diseases using the XProteome approach.")
|
| 1277 |
+
with gr.Tabs():
|
| 1278 |
+
with gr.TabItem("Single Disease"):
|
| 1279 |
+
disease_sel = gr.Dropdown(list(DISEASE_BIOMARKERS.keys()), value="Breast Cancer", label="Select Disease")
|
| 1280 |
+
btn_panel = gr.Button("Get Biomarker Panel", variant="primary")
|
| 1281 |
+
panel_table = gr.Dataframe(label="Protein Panel")
|
| 1282 |
+
panel_note = gr.Markdown()
|
| 1283 |
+
btn_panel.click(get_biomarker_panel, inputs=[disease_sel], outputs=[panel_table, panel_note])
|
| 1284 |
+
|
| 1285 |
+
with gr.TabItem("Cross-Disease Comparison"):
|
| 1286 |
+
with gr.Row():
|
| 1287 |
+
disease1 = gr.Dropdown(list(DISEASE_BIOMARKERS.keys()), value="Breast Cancer", label="Disease 1")
|
| 1288 |
+
disease2 = gr.Dropdown(list(DISEASE_BIOMARKERS.keys()), value="Lung Cancer", label="Disease 2")
|
| 1289 |
+
btn_common = gr.Button("Find Common Biomarkers", variant="primary")
|
| 1290 |
+
common_table = gr.Dataframe(label="Shared Proteins")
|
| 1291 |
+
common_note = gr.Markdown()
|
| 1292 |
+
btn_common.click(find_common_biomarkers, inputs=[disease1, disease2], outputs=[common_table, common_note])
|
| 1293 |
+
gr.Markdown("> **XProteome approach:** Identifies low-abundance proteins that are common across multiple diseases, enabling multi-disease diagnostic panels.")
|
| 1294 |
|
| 1295 |
# === S1-F · PHYLO-RARE ===
|
| 1296 |
with gr.TabItem("🧠 S1-F"):
|
|
|
|
| 1500 |
| S1-D·R3a | PHYLO-LNP | LNP Brain | pKa 6.2–6.8 |
|
| 1501 |
| S1-E·R1a | PHYLO-BIOMARKERS | Liquid Biopsy | AUC=0.992* |
|
| 1502 |
| S1-D·R4a | PHYLO-LNP | AutoCorona NLP | F1=0.71 |
|
| 1503 |
+
| S1-D·R6a | PHYLO-LNP | Corona Database | simulated |
|
| 1504 |
+
| S1-E·R2a | PHYLO-BIOMARKERS | Multi-protein biomarkers | simulated |
|
| 1505 |
""")
|
| 1506 |
+
|
| 1507 |
# === Journal (окрема вкладка) ===
|
| 1508 |
with gr.TabItem("📓 Journal"):
|
| 1509 |
gr.Markdown("## Lab Journal — Full History")
|
| 1510 |
with gr.Row():
|
| 1511 |
+
journal_filter = gr.Dropdown(
|
| 1512 |
+
choices=["All"] + JOURNAL_CATEGORIES,
|
| 1513 |
value="All",
|
| 1514 |
+
label="Filter by category"
|
| 1515 |
)
|
| 1516 |
+
refresh_btn = gr.Button("🔄 Refresh", size="sm", variant="secondary")
|
| 1517 |
+
clear_btn = gr.Button("🗑️ Clear Journal", size="sm", variant="stop")
|
| 1518 |
+
journal_display = gr.Markdown(value=journal_read())
|
| 1519 |
+
|
| 1520 |
+
def refresh_journal(category):
|
| 1521 |
+
return journal_read(category)
|
| 1522 |
|
| 1523 |
+
refresh_btn.click(refresh_journal, inputs=[journal_filter], outputs=journal_display)
|
| 1524 |
clear_btn.click(clear_journal, [], journal_display).then(
|
| 1525 |
+
refresh_journal, inputs=[journal_filter], outputs=journal_display
|
| 1526 |
)
|
| 1527 |
+
journal_filter.change(refresh_journal, inputs=[journal_filter], outputs=journal_display)
|
| 1528 |
|
| 1529 |
+
# Права колонка – швидке введення нотаток
|
| 1530 |
with gr.Column(scale=1, min_width=260):
|
| 1531 |
with gr.Group(elem_classes="sidebar-journal"):
|
| 1532 |
+
gr.Markdown("## 📝 Quick Note")
|
| 1533 |
+
note_category = gr.Dropdown(
|
| 1534 |
+
choices=JOURNAL_CATEGORIES,
|
| 1535 |
+
value="Manual",
|
| 1536 |
+
label="Category",
|
| 1537 |
+
allow_custom_value=False
|
| 1538 |
+
)
|
| 1539 |
+
note_input = gr.Textbox(
|
| 1540 |
+
label="Observation",
|
| 1541 |
+
placeholder="Type your note here...",
|
| 1542 |
+
lines=3
|
| 1543 |
)
|
| 1544 |
+
with gr.Row():
|
| 1545 |
+
save_btn = gr.Button("💾 Save Note", size="sm", variant="primary")
|
| 1546 |
+
clear_note_btn = gr.Button("🗑️ Clear", size="sm")
|
| 1547 |
save_status = gr.Markdown("")
|
| 1548 |
+
|
| 1549 |
+
def save_note(category, note):
|
| 1550 |
if note.strip():
|
| 1551 |
+
journal_log(category, "manual note", note, note)
|
| 1552 |
+
return "✅ Note saved.", ""
|
| 1553 |
+
return "⚠️ Note is empty.", ""
|
| 1554 |
+
|
| 1555 |
+
save_btn.click(save_note, inputs=[note_category, note_input], outputs=[save_status, note_input])
|
| 1556 |
+
clear_note_btn.click(lambda: ("", ""), outputs=[note_input, save_status])
|
| 1557 |
|
| 1558 |
+
# Інтеграція з Research Suite
|
| 1559 |
with gr.Row():
|
| 1560 |
gr.Markdown("""
|
| 1561 |
---
|