amitpande74 commited on
Commit
478984f
Β·
verified Β·
1 Parent(s): 712366b

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +197 -0
app.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Flexynesis Tissue VAE – Web Application
4
+ ========================================
5
+ Upload a gene expression matrix β†’ get UBERON tissue classification + embeddings.
6
+
7
+ Run locally:
8
+ streamlit run app.py
9
+
10
+ Deploy on HuggingFace Spaces:
11
+ See setup_webapp.py for file preparation.
12
+
13
+ Author: Amit Pande, MDC Berlin/BIMSB
14
+ """
15
+
16
+ import streamlit as st
17
+ import pandas as pd
18
+ import numpy as np
19
+ import torch
20
+ import joblib
21
+ from pathlib import Path
22
+ from sklearn.neighbors import KNeighborsClassifier
23
+ from collections import Counter
24
+
25
+ # ── Config ──
26
+ MODEL_DIR = Path("model")
27
+ K = 5
28
+
29
+ st.set_page_config(page_title="Flexynesis Tissue VAE", page_icon="🧬", layout="wide")
30
+ st.title("🧬 Flexynesis Tissue VAE")
31
+ st.markdown(
32
+ "Upload a bulk RNA-seq gene expression matrix to classify tissue-of-origin "
33
+ "using a supervised VAE trained on **75,619 samples** from TCGA, GTEx, DepMap, and ARCHS4 "
34
+ "across **43 UBERON tissue categories** (90.7% balanced accuracy, 121-dim latent space)."
35
+ )
36
+
37
+ # ── Load model + artifacts ──
38
+ @st.cache_resource
39
+ def load_all():
40
+ model = torch.load(MODEL_DIR / "vae_tissue.final_model.pth",
41
+ map_location='cpu', weights_only=False)
42
+ model.eval()
43
+
44
+ art = joblib.load(MODEL_DIR / "vae_tissue.artifacts.joblib")
45
+ gene_list = list(art['feature_lists']['gex'])
46
+ scaler = art['transforms']['gex']
47
+ label_enc = art['label_encoders']['uberon_tissue']
48
+
49
+ train_emb = pd.read_csv(MODEL_DIR / "embeddings_train.csv", index_col=0)
50
+ test_emb = pd.read_csv(MODEL_DIR / "embeddings_test.csv", index_col=0)
51
+ train_clin = pd.read_csv(MODEL_DIR / "train_clin.csv", index_col=0)
52
+ test_clin = pd.read_csv(MODEL_DIR / "test_clin.csv", index_col=0)
53
+
54
+ all_emb = pd.concat([train_emb, test_emb])
55
+ all_clin = pd.concat([train_clin, test_clin])
56
+ idx = all_emb.index.intersection(all_clin.index)
57
+ ref_emb = all_emb.loc[idx]
58
+ ref_clin = all_clin.loc[idx]
59
+ mask = (ref_clin['uberon_tissue'].notna() &
60
+ ~ref_clin['uberon_tissue'].isin(['unknown','other','unmapped','nan','']))
61
+ ref_emb = ref_emb[mask]
62
+ ref_clin = ref_clin[mask]
63
+
64
+ knn = KNeighborsClassifier(n_neighbors=K, metric='cosine', n_jobs=-1)
65
+ knn.fit(ref_emb.values, ref_clin['uberon_tissue'].values)
66
+
67
+ classes = list(label_enc.categories_[0]) if hasattr(label_enc, 'categories_') else None
68
+
69
+ return model, gene_list, scaler, knn, ref_emb, ref_clin, classes
70
+
71
+ try:
72
+ model, gene_list, scaler, knn_ref, ref_emb, ref_clin, classes = load_all()
73
+ st.success(
74
+ f"βœ… {len(gene_list):,} genes Β· "
75
+ f"{len(ref_emb):,} reference samples Β· "
76
+ f"{ref_clin['uberon_tissue'].nunique()} tissues"
77
+ )
78
+ except Exception as e:
79
+ st.error(f"Load error: {e}")
80
+ st.info("Place model files in `model/` directory. Run `python setup_webapp.py` first.")
81
+ st.stop()
82
+
83
+ # ── Helpers ──
84
+ def orient_matrix(df):
85
+ genes_in_cols = len(set(df.columns) & set(gene_list))
86
+ genes_in_rows = len(set(df.index) & set(gene_list))
87
+ if genes_in_rows > genes_in_cols:
88
+ df = df.T
89
+ return df
90
+
91
+ def encode(model, X_tensor):
92
+ with torch.no_grad():
93
+ h = model.encoders[0](X_tensor)
94
+ mu = model.FC_mean(h[0])
95
+ return mu
96
+
97
+ def classify_from_logits(model, mu, classes):
98
+ with torch.no_grad():
99
+ logits = model.MLPs['uberon_tissue'](mu)
100
+ # Mask out nan class if present
101
+ label_mapping = model.dataset.label_mappings['uberon_tissue']
102
+ name_to_idx = {v: k for k, v in label_mapping.items()}
103
+ nan_idx = name_to_idx.get('nan', None)
104
+ if nan_idx is not None:
105
+ logits[:, nan_idx] = -1e9
106
+ probs = torch.softmax(logits, dim=1)
107
+ pred_idx = logits.argmax(dim=1)
108
+ pred_labels = [label_mapping[int(i)] for i in pred_idx]
109
+ return pred_labels, probs.numpy()
110
+
111
+ # ── File upload ──
112
+ st.markdown("---")
113
+ col1, col2 = st.columns([2, 1])
114
+ with col1:
115
+ uploaded = st.file_uploader(
116
+ "Upload CSV/TSV (genes Γ— samples or samples Γ— genes)",
117
+ type=["csv", "tsv", "txt"],
118
+ help="HGNC gene symbols. Log2-transformed expression values (TPM, RPKM, or counts).")
119
+ with col2:
120
+ st.markdown(f"""
121
+ **Expected input:**
122
+ - HGNC gene symbols
123
+ - {len(gene_list):,} genes used by model
124
+ - Log2-transformed expression
125
+ """)
126
+
127
+ if uploaded:
128
+ sep = '\t' if uploaded.name.endswith(('.tsv', '.txt')) else ','
129
+ df = pd.read_csv(uploaded, index_col=0, sep=sep)
130
+ st.write(f"**Uploaded:** {df.shape[0]:,} Γ— {df.shape[1]:,}")
131
+ st.dataframe(df.iloc[:5, :5], use_container_width=True)
132
+
133
+ if st.button("πŸš€ Classify Tissues", type="primary"):
134
+ with st.spinner("Processing..."):
135
+ df = orient_matrix(df)
136
+ overlap = len(set(df.columns) & set(gene_list))
137
+ st.write(f"Gene overlap: **{overlap:,}/{len(gene_list):,}** ({100*overlap/len(gene_list):.1f}%)")
138
+ if overlap < 1000:
139
+ st.warning("Low gene overlap β€” results may be unreliable.")
140
+
141
+ aligned = pd.DataFrame(0.0, index=df.index, columns=gene_list)
142
+ common = [g for g in gene_list if g in df.columns]
143
+ aligned[common] = df[common].values
144
+ aligned = aligned.fillna(0)
145
+
146
+ X_scaled = scaler.transform(aligned.values)
147
+ X_tensor = torch.tensor(X_scaled, dtype=torch.float32)
148
+
149
+ mu = encode(model, X_tensor)
150
+ embeddings = mu.numpy()
151
+
152
+ pred_labels, probs = classify_from_logits(model, mu, classes)
153
+ max_probs = probs.max(axis=1)
154
+
155
+ distances, indices = knn_ref.kneighbors(embeddings)
156
+ breakdowns = []
157
+ for i in range(len(embeddings)):
158
+ nn_clin = ref_clin.iloc[indices[i]]
159
+ src_counts = Counter(nn_clin['source'].values)
160
+ breakdowns.append('; '.join(f"{s}:{c}" for s, c in src_counts.most_common()))
161
+
162
+ st.markdown("---")
163
+ st.subheader("πŸ“Š Results")
164
+ results = pd.DataFrame({
165
+ 'Sample': df.index,
166
+ 'Tissue': pred_labels,
167
+ 'Confidence': [f"{p:.1%}" for p in max_probs],
168
+ 'kNN Dist': distances.mean(axis=1).round(4),
169
+ 'Sources': breakdowns,
170
+ })
171
+ st.dataframe(results, use_container_width=True, height=400)
172
+
173
+ col1, col2 = st.columns(2)
174
+ with col1:
175
+ st.subheader("Tissue Distribution")
176
+ st.bar_chart(pd.Series(pred_labels).value_counts())
177
+ with col2:
178
+ st.subheader("Confidence Distribution")
179
+ st.bar_chart(pd.DataFrame({'Confidence': max_probs}, index=df.index))
180
+
181
+ st.markdown("---")
182
+ c1, c2 = st.columns(2)
183
+ with c1:
184
+ emb_df = pd.DataFrame(embeddings, index=df.index,
185
+ columns=[f"z{i}" for i in range(121)])
186
+ st.download_button("πŸ“₯ Embeddings (CSV)", emb_df.to_csv(),
187
+ "flexynesis_embeddings.csv", "text/csv")
188
+ with c2:
189
+ st.download_button("πŸ“₯ Classifications (CSV)", results.to_csv(index=False),
190
+ "flexynesis_classifications.csv", "text/csv")
191
+
192
+ st.markdown("---")
193
+ st.caption(
194
+ "Flexynesis Tissue VAE Β· Akalin Lab, MDC Berlin/BIMSB Β· "
195
+ "75,619 samples Β· 43 UBERON tissues Β· 90.7% balanced accuracy Β· "
196
+ "GitHub: github.com/BIMSBbioinfo/flexynesis"
197
+ )