import json
import numpy as np
import pandas as pd
import plotly.express as px
import requests
import streamlit as st
import torch
from data_loader import (
BENCHMARK_DRUGS,
GTEX_TISSUE_PROFILES,
MEDDRA_ADR_CLASSES,
ORGAN_NAMES,
)
from model import EpiADRNet
from utils import highlight_toxic_subgraph, smiles_to_graph
# Streamlit Page Config
st.set_page_config(
page_title="EpiADR-Net — Organ-Conditioned Zero-Shot ADR Platform",
page_icon="🧬",
layout="wide",
initial_sidebar_state="expanded"
)
# Custom CSS Styling
st.markdown("""
""", unsafe_allow_html=True)
# Load Local Model Instance
@st.cache_resource
def load_cached_model():
model = EpiADRNet(in_features=10, hidden_dim=256, num_classes=10, dropout=0.3)
try:
model.load_state_dict(torch.load("model.pt", map_location=torch.device('cpu')))
except Exception:
pass
model.eval()
return model
model = load_cached_model()
# Header Section
st.markdown('
🧬 EpiADR-Net Research Platform
', unsafe_allow_html=True)
st.markdown('Tissue-Conditioned Zero-Shot Side Effect Disaggregation via 4-Layer GAT & GTEx FiLM Conditioning
', unsafe_allow_html=True)
# Sidebar Configuration
st.sidebar.header("⚙️ Inference Controls")
preset_drug = st.sidebar.selectbox(
"Select Benchmark Drug Preset",
["Custom SMILES"] + [d["name"] for d in BENCHMARK_DRUGS]
)
if preset_drug != "Custom SMILES":
selected_item = next(d for d in BENCHMARK_DRUGS if d["name"] == preset_drug)
default_smiles = selected_item["smiles"]
else:
default_smiles = "CC(=O)NC1=CC=C(O)C=C1"
smiles_input = st.sidebar.text_area("SMILES String", value=default_smiles, height=80)
organ_selection = st.sidebar.selectbox("Conditioning Organ", ORGAN_NAMES, index=0)
mc_passes = st.sidebar.slider("Monte Carlo Dropout Passes (N)", min_value=5, max_value=50, value=20, step=5)
confidence_thresh = st.sidebar.slider("Significance Threshold", min_value=0.1, max_value=0.9, value=0.45, step=0.05)
# Main Navigation Tabs
tab1, tab2, tab3, tab4 = st.tabs([
"🎯 Single Organ Predictor & XAI",
"⚖️ Dual Organ Comparative Analysis",
"📊 Model Benchmarks & Math",
"🔌 REST API & Playground"
])
# --- TAB 1: Single Organ Predictor & XAI ---
with tab1:
col_input, col_info = st.columns([2, 1])
with col_input:
st.subheader(f"Predicting ADRs conditioned on **{organ_selection}** Tissue Vector")
with col_info:
st.info(f"Target Tissue: GTEx {organ_selection} (128-dim)")
if st.button("🚀 Run Tissue-Conditioned Inference", type="primary", use_container_width=True):
node_feats, edge_index, atom_symbols = smiles_to_graph(smiles_input)
batch = torch.zeros(node_feats.size(0), dtype=torch.long)
tissue_vec = GTEX_TISSUE_PROFILES[organ_selection].unsqueeze(0)
mc_res = model.predict_mc_dropout(node_feats, edge_index, batch, tissue_vec, num_samples=mc_passes)
mu = mc_res["mean_probabilities"][0].cpu().numpy()
sigma = mc_res["uncertainty_sigma"][0].cpu().numpy()
attn = mc_res["attention_weights"]
df_res = pd.DataFrame({
"MedDRA Term": MEDDRA_ADR_CLASSES,
"Probability (μ)": mu,
"Uncertainty (σ)": sigma,
"Significant": mu >= confidence_thresh
})
col_left, col_right = st.columns([3, 2])
with col_left:
st.markdown("### 📈 Predicted ADR Probabilities & Epistemic Uncertainty")
fig = px.bar(
df_res,
x="Probability (μ)",
y="MedDRA Term",
error_x="Uncertainty (σ)",
orientation='h',
color="Probability (μ)",
color_continuous_scale="Reds",
range_x=[0, 1.0]
)
fig.add_vline(x=confidence_thresh, line_dash="dash", line_color="black", annotation_text="Threshold")
fig.update_layout(height=450, margin={"l": 0, "r": 0, "t": 30, "b": 0})
st.plotly_chart(fig, use_container_width=True)
with col_right:
st.markdown("### 🔬 Explainable AI (Layer 4 GAT Hotspots)")
xai_res = highlight_toxic_subgraph(smiles_input, attn, top_k=3)
st.write(f"**Total Atoms parsed**: `{xai_res['total_atoms']}`")
st.write("**Top Toxic Atomic Subgraphs (GAT Attention):**")
for hs in xai_res["top_toxic_hotspots"]:
st.markdown(
f''
f'Atom #{hs["atom_index"]} ({hs["atom_symbol"]}) — GAT Attention Score: '
f'{hs["attention_score"]}'
f'
',
unsafe_allow_allowed_html=True
)
st.markdown("#### Raw Atom Symbols")
st.code(" ".join(xai_res["atom_symbols"]))
# --- TAB 2: Dual Organ Comparative Analysis ---
with tab2:
st.subheader("⚖️ Comparative Organ-Specific Side Effect Disaggregation")
st.write("Compare how identical molecular structures produce distinct toxicities when conditioned on different tissue expression profiles.")
c1, c2 = st.columns(2)
with c1:
organ_a = st.selectbox("Select First Tissue", ORGAN_NAMES, index=0)
with c2:
organ_b = st.selectbox("Select Second Tissue", ORGAN_NAMES, index=1)
if st.button("⚔️ Generate Comparative Profiles", use_container_width=True):
node_feats, edge_index, _ = smiles_to_graph(smiles_input)
batch = torch.zeros(node_feats.size(0), dtype=torch.long)
vec_a = GTEX_TISSUE_PROFILES[organ_a].unsqueeze(0)
vec_b = GTEX_TISSUE_PROFILES[organ_b].unsqueeze(0)
res_a = model.predict_mc_dropout(node_feats, edge_index, batch, vec_a, num_samples=mc_passes)
res_b = model.predict_mc_dropout(node_feats, edge_index, batch, vec_b, num_samples=mc_passes)
mu_a = res_a["mean_probabilities"][0].cpu().numpy()
mu_b = res_b["mean_probabilities"][0].cpu().numpy()
df_comp = pd.DataFrame({
"MedDRA Term": MEDDRA_ADR_CLASSES * 2,
"Probability": np.concatenate([mu_a, mu_b]),
"Organ": [organ_a] * 10 + [organ_b] * 10
})
fig_comp = px.bar(
df_comp,
x="MedDRA Term",
y="Probability",
color="Organ",
barmode="group",
color_discrete_sequence=["#1E88E5", "#D81B60"]
)
fig_comp.update_layout(height=480, xaxis_tickangle=-45)
st.plotly_chart(fig_comp, use_container_width=True)
# --- TAB 3: Model Benchmarks & Math ---
with tab3:
st.subheader("📊 Generalization Benchmark & Mathematical Formulation")
st.markdown("""
| Experiment Split | Test Macro-AUROC | Test Micro-AUPRC | Epochs | Split Strategy |
| :--- | :--- | :--- | :--- | :--- |
| **Bemis-Murcko Scaffold Split** | **0.4938** | **0.2589** | 10 | Zero structural overlap between train and test |
| **Random Split** | 0.4638 | 0.3018 | 10 | Standard random baseline |
""")
st.markdown("### 🧮 Mathematical Formulations")
st.latex(r"""
\alpha_{ij}^{(l)} = \frac{\exp\left(\text{LeakyReLU}\left(\mathbf{a}^T [\mathbf{W} h_i^{(l-1)} \Vert \mathbf{W} h_j^{(l-1)}]\right)\right)}{\sum_{k \in \mathcal{N}(i)} \exp\left(\text{LeakyReLU}\left(\mathbf{a}^T [\mathbf{W} h_i^{(l-1)} \Vert \mathbf{W} h_k^{(l-1)}]\right)\right)}
""")
st.latex(r"""
\gamma = \text{MLP}_\gamma(v_{\text{tissue}}) \in \mathbb{R}^{128}, \quad \beta = \text{MLP}_\beta(v_{\text{tissue}}) \in \mathbb{R}^{128}
""")
st.latex(r"""
h_i^{\text{conditioned}} = \gamma \odot h_i + \beta
""")
st.latex(r"""
\mu_q = \frac{1}{N} \sum_{n=1}^N \sigma(\hat{y}_{q, n}), \quad \sigma_q = \sqrt{\frac{1}{N} \sum_{n=1}^N \left(\sigma(\hat{y}_{q, n}) - \mu_q\right)^2}
""")
# --- TAB 4: REST API & Playground ---
with tab4:
st.subheader("🔌 FastAPI REST Microservice Playground")
st.markdown("Interact directly with the local FastAPI microservice running at `http://localhost:8000`.")
st.info("Interactive Swagger UI Documentation available at: [http://localhost:8000/docs](http://localhost:8000/docs)")
api_endpoint = st.text_input("API URL Target", value="http://localhost:8000/predict")
sample_json = {
"smiles": smiles_input,
"organ": organ_selection,
"mc_samples": mc_passes
}
json_payload = st.text_area("JSON Request Payload", value=json.dumps(sample_json, indent=2), height=150)
if st.button("📡 Send API Request"):
try:
res = requests.post(api_endpoint, json=json.loads(json_payload), timeout=5)
st.json(res.json())
except Exception as e:
st.error(f"Could not connect to FastAPI server at {api_endpoint}: {e!s}")
st.warning("Note: Make sure `uvicorn api:app --reload` is running on port 8000.")