Petimot / app /main.py
Valmbd's picture
Fix crash loop: lazy zip loading in check_data_status (no full scan at startup) [16:08 UTC]
05626e1 verified
import streamlit as st
import os, sys
import numpy as np
# โ”€โ”€ Page Config โ”€โ”€
st.set_page_config(
page_title="PETIMOT Explorer",
page_icon="๐Ÿงฌ",
layout="wide",
initial_sidebar_state="expanded",
)
# โ”€โ”€ Ensure PETIMOT is importable โ”€โ”€
PETIMOT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if PETIMOT_ROOT not in sys.path:
sys.path.insert(0, PETIMOT_ROOT)
# โ”€โ”€ Custom CSS โ”€โ”€
st.markdown("""
<style>
/* โ”€โ”€โ”€ Dark Theme โ”€โ”€โ”€ */
.stApp { background-color: #0f0d1a; }
.block-container { padding-top: 1rem; max-width: 1200px; }
/* Sidebar */
section[data-testid="stSidebar"] {
background: linear-gradient(180deg, #1a1730 0%, #0f0d1a 100%);
border-right: 1px solid #2d2b55;
}
/* Headers */
h1, h2, h3 { color: #c4b5fd !important; }
/* Metric cards */
[data-testid="stMetric"] {
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%);
border: 1px solid #4338ca;
border-radius: 16px;
padding: 16px 20px;
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.15);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
[data-testid="stMetric"]:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(99, 102, 241, 0.25);
}
[data-testid="stMetricLabel"] { color: #a5b4fc !important; font-size: 0.85rem !important; }
[data-testid="stMetricValue"] { color: #e0e7ff !important; font-weight: 700 !important; }
/* Dataframe */
.stDataFrame { border-radius: 12px; overflow: hidden; }
/* Tabs */
.stTabs [data-baseweb="tab"] {
background-color: #1e1b4b;
border-radius: 8px 8px 0 0;
color: #a5b4fc;
}
.stTabs [data-baseweb="tab"][aria-selected="true"] {
background-color: #312e81;
color: white;
}
/* โ”€โ”€โ”€ Hero Section โ”€โ”€โ”€ */
.hero-container {
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 30%, #4338ca 60%, #6366f1 100%);
border-radius: 24px;
padding: 48px 40px;
margin-bottom: 2rem;
position: relative;
overflow: hidden;
box-shadow: 0 8px 40px rgba(99, 102, 241, 0.3);
}
.hero-container::before {
content: '';
position: absolute;
top: -50%;
right: -20%;
width: 60%;
height: 200%;
background: radial-gradient(circle, rgba(139, 92, 246, 0.15) 0%, transparent 70%);
animation: heroGlow 6s ease-in-out infinite alternate;
}
@keyframes heroGlow {
0% { transform: translate(0, 0) scale(1); opacity: 0.5; }
100% { transform: translate(-10%, 10%) scale(1.2); opacity: 1; }
}
.hero-title {
font-size: 3rem;
font-weight: 800;
color: #ffffff !important;
margin: 0 0 8px 0;
letter-spacing: -0.02em;
position: relative;
z-index: 1;
}
.hero-subtitle {
font-size: 1.3rem;
color: #c4b5fd;
margin: 0 0 24px 0;
font-weight: 400;
position: relative;
z-index: 1;
}
.hero-badge {
display: inline-block;
background: rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255,255,255,0.15);
border-radius: 999px;
padding: 6px 16px;
font-size: 0.85rem;
color: #e0e7ff;
margin-right: 8px;
margin-bottom: 8px;
}
/* โ”€โ”€โ”€ Feature Cards โ”€โ”€โ”€ */
.feature-card {
background: linear-gradient(135deg, #1e1b4b 0%, #1a1730 100%);
border: 1px solid #312e81;
border-radius: 16px;
padding: 24px;
height: 100%;
transition: all 0.3s ease;
box-shadow: 0 2px 12px rgba(0,0,0,0.2);
}
.feature-card:hover {
border-color: #6366f1;
box-shadow: 0 4px 24px rgba(99, 102, 241, 0.2);
transform: translateY(-2px);
}
.feature-icon {
font-size: 2.2rem;
margin-bottom: 12px;
}
.feature-title {
color: #e0e7ff !important;
font-size: 1.15rem;
font-weight: 700;
margin-bottom: 8px;
}
.feature-desc {
color: #94a3b8;
font-size: 0.9rem;
line-height: 1.5;
}
/* โ”€โ”€โ”€ Leaderboard cards โ”€โ”€โ”€ */
.leader-row {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 16px;
background: rgba(30, 27, 75, 0.6);
border-radius: 10px;
margin-bottom: 6px;
border-left: 3px solid #6366f1;
transition: background 0.2s;
}
.leader-row:hover { background: rgba(49, 46, 129, 0.6); }
.leader-rank {
color: #6366f1;
font-weight: 800;
font-size: 1rem;
min-width: 28px;
}
.leader-name {
color: #e0e7ff;
font-weight: 600;
flex: 1;
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 0.85rem;
}
.leader-val {
color: #a5b4fc;
font-weight: 500;
font-size: 0.85rem;
}
/* โ”€โ”€โ”€ Animations โ”€โ”€โ”€ */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-in { animation: fadeInUp 0.6s ease-out forwards; }
.animate-delay-1 { animation-delay: 0.1s; }
.animate-delay-2 { animation-delay: 0.2s; }
.animate-delay-3 { animation-delay: 0.3s; }
</style>
""", unsafe_allow_html=True)
# โ”€โ”€ Sidebar โ”€โ”€
with st.sidebar:
st.markdown("""
<div style="text-align:center; padding: 16px 0;">
<div style="font-size: 3.5rem;">๐Ÿงฌ</div>
<div style="font-size: 1.6rem; font-weight: 800; color: #c4b5fd; letter-spacing: -0.02em;">PETIMOT</div>
<div style="font-size: 0.8rem; color: #94a3b8; margin-top: 4px;">Protein Motion from Sparse Data</div>
<div style="font-size: 0.7rem; color: #6366f1; margin-top: 2px;">SE(3)-Equivariant GNNs</div>
</div>
""", unsafe_allow_html=True)
st.divider()
# Global settings
st.markdown("### โš™๏ธ Settings")
weights_dir = os.path.join(PETIMOT_ROOT, "weights")
pt_files = []
if os.path.isdir(weights_dir):
for root, dirs, files in os.walk(weights_dir):
for f in files:
if f.endswith(".pt"):
pt_files.append(os.path.join(root, f))
if pt_files:
selected_weights = st.selectbox(
"Model weights",
pt_files,
format_func=lambda x: os.path.basename(x),
key="weights"
)
else:
selected_weights = None
st.warning("No weights found in `weights/`")
st.divider()
st.markdown("""
**Links**
- [Paper](https://arxiv.org/abs/2504.02839)
- [GitHub](https://github.com/PhyloSofS-Team/PETIMOT)
- [Data](https://figshare.com/s/ab400d852b4669a83b64)
""")
st.caption("GPL-3.0 ยท Lombard, Grudinin & Laine")
# โ”€โ”€ Hero Section โ”€โ”€
st.markdown("""
<div class="hero-container">
<p class="hero-title">๐Ÿงฌ PETIMOT Explorer</p>
<p class="hero-subtitle">
Explore protein motion predictions at scale โ€” 36K+ proteins analyzed with SE(3)-Equivariant Graph Neural Networks
</p>
<div>
<span class="hero-badge">๐Ÿ”ฌ 36K+ Proteins</span>
<span class="hero-badge">๐Ÿง  SE(3)-Equivariant</span>
<span class="hero-badge">๐Ÿ“Š 4 Motion Modes</span>
<span class="hero-badge">โšก CPU Inference</span>
</div>
</div>
""", unsafe_allow_html=True)
# โ”€โ”€ Data Status โ”€โ”€
from app.utils.download import check_data_status, ensure_weights
from app.utils.data_loader import find_predictions_dir, load_prediction_index
status = check_data_status(PETIMOT_ROOT)
# โ”€โ”€ Quick Search โ”€โ”€
st.markdown("### ๐Ÿ” Quick Search")
quick_search = st.text_input(
"Search proteins by name",
placeholder="e.g. 1ake, 4ake, lysozyme...",
label_visibility="collapsed",
key="home_search",
)
if quick_search:
st.session_state["explorer_search"] = quick_search
st.info(f"๐Ÿ” Navigate to the **Explorer** page to see results for **\"{quick_search}\"**")
# โ”€โ”€ Metrics Row โ”€โ”€
st.markdown("### ๐Ÿ“Š Dataset Overview")
col1, col2, col3, col4 = st.columns(4)
with col1:
n_pred = status['predictions']
pred_display = "36,675" if n_pred < 0 else f"{n_pred:,}"
st.metric("๐Ÿงฌ Proteins", pred_display,
delta="โœ…" if status['has_predictions'] else "โš ๏ธ No data")
with col2:
st.metric("๐ŸŽฏ Ground Truth", f"{status['ground_truth']:,}",
delta="โœ…" if status['has_gt'] else "Not loaded")
with col3:
st.metric("โš–๏ธ Model Weights", "4.7M params",
delta="โœ…" if status['has_weights'] else "Missing")
with col4:
st.metric("๐Ÿ”ฎ Motion Modes", "4 per protein",
delta="Normal Modes" if status['has_predictions'] else "โ€”")
# โ”€โ”€ Feature Cards โ”€โ”€
st.markdown("---")
col1, col2, col3 = st.columns(3)
with col1:
st.markdown("""
<div class="feature-card animate-in animate-delay-1">
<div class="feature-icon">๐Ÿ”</div>
<div class="feature-title">Explorer</div>
<div class="feature-desc">
Browse pre-computed predictions for 36K+ proteins. Filter by sequence length,
displacement, and view 3D motion visualizations with interactive controls.
</div>
</div>
""", unsafe_allow_html=True)
with col2:
st.markdown("""
<div class="feature-card animate-in animate-delay-2">
<div class="feature-icon">๐Ÿ”ฎ</div>
<div class="feature-title">Inference</div>
<div class="feature-desc">
Predict motion modes for any protein structure. Upload a PDB file or fetch
from RCSB. Runs on CPU in 5โ€“30 seconds.
</div>
</div>
""", unsafe_allow_html=True)
with col3:
st.markdown("""
<div class="feature-card animate-in animate-delay-3">
<div class="feature-icon">๐Ÿ“Š</div>
<div class="feature-title">Statistics</div>
<div class="feature-desc">
Dataset-wide analysis with interactive charts: displacement distributions,
correlation heatmaps, leaderboards, and length-stratified analysis.
</div>
</div>
""", unsafe_allow_html=True)
# โ”€โ”€ Featured Proteins โ”€โ”€
if status['has_predictions']:
pred_dir = find_predictions_dir(PETIMOT_ROOT)
if pred_dir:
try:
df = load_prediction_index(pred_dir)
if not df.empty and len(df) > 0:
st.markdown("---")
st.markdown("### ๐Ÿ† Featured Proteins")
col_flex, col_rigid = st.columns(2)
with col_flex:
st.markdown("**๐Ÿ”ด Most Flexible** (highest mean displacement)")
top5 = df.nlargest(5, "mean_disp_m0")
html_rows = ""
for i, (_, row) in enumerate(top5.iterrows()):
html_rows += f"""
<div class="leader-row">
<span class="leader-rank">#{i+1}</span>
<span class="leader-name">{row['name']}</span>
<span class="leader-val">{row['mean_disp_m0']:.3f} ร… ยท {int(row['seq_len'])} res</span>
</div>"""
st.markdown(html_rows, unsafe_allow_html=True)
with col_rigid:
st.markdown("**๐Ÿ”ต Most Rigid** (lowest mean displacement)")
bot5 = df.nsmallest(5, "mean_disp_m0")
html_rows = ""
for i, (_, row) in enumerate(bot5.iterrows()):
html_rows += f"""
<div class="leader-row">
<span class="leader-rank">#{i+1}</span>
<span class="leader-name">{row['name']}</span>
<span class="leader-val">{row['mean_disp_m0']:.3f} ร… ยท {int(row['seq_len'])} res</span>
</div>"""
st.markdown(html_rows, unsafe_allow_html=True)
# โ”€โ”€ Sparkline overview โ”€โ”€
st.markdown("---")
st.markdown("### ๐Ÿ“ˆ At a Glance")
import plotly.graph_objects as go
col_s1, col_s2, col_s3 = st.columns(3)
def make_sparkline(values, title, color, unit=""):
fig = go.Figure()
fig.add_trace(go.Histogram(
x=values, nbinsx=40,
marker_color=color,
marker_line_width=0,
opacity=0.85,
))
fig.update_layout(
template="plotly_dark",
height=140,
margin=dict(l=0, r=0, t=30, b=0),
paper_bgcolor="rgba(0,0,0,0)",
plot_bgcolor="rgba(0,0,0,0)",
showlegend=False,
title=dict(text=f"<b>{title}</b>", font=dict(size=13, color="#a5b4fc"), x=0.02),
xaxis=dict(showgrid=False, showticklabels=True, color="#6366f1",
tickfont=dict(size=9)),
yaxis=dict(showgrid=False, showticklabels=False),
)
return fig
with col_s1:
fig = make_sparkline(df['seq_len'], "Sequence Length", "#6366f1")
st.plotly_chart(fig, use_container_width=True, key="spark_len")
with col_s2:
fig = make_sparkline(df['mean_disp_m0'], "Mean Displacement (ร…)", "#10b981")
st.plotly_chart(fig, use_container_width=True, key="spark_mean")
with col_s3:
fig = make_sparkline(df['max_disp_m0'], "Max Displacement (ร…)", "#f59e0b")
st.plotly_chart(fig, use_container_width=True, key="spark_max")
except Exception as e:
st.warning(f"Could not load featured proteins: {e}")
# โ”€โ”€ Auto-download if missing โ”€โ”€
if not status['has_weights']:
st.divider()
st.warning("โš ๏ธ Model weights not found.")
if st.button("โฌ‡๏ธ Download weights from Figshare (18 MB)", type="primary"):
with st.spinner("Downloading..."):
wt = ensure_weights(PETIMOT_ROOT)
if wt:
st.success(f"โœ… Weights downloaded: {os.path.basename(wt)}")
st.rerun()
else:
st.error("Download failed. Please manually download from "
"[Figshare](https://figshare.com/s/ab400d852b4669a83b64) "
"and place in `weights/`")
if not status['has_predictions'] and status['has_weights']:
st.info("๐Ÿ’ก No pre-computed predictions yet. Use the **Inference** page to predict "
"individual proteins, or run batch inference from the Colab notebook.")