kdir commited on
Commit
e762f1e
Β·
verified Β·
1 Parent(s): 103096d

Create app.py

Browse files

This is a streamlit to gradio direct conversion from Geonomic's Genomic Classifier Capstone.

Files changed (1) hide show
  1. app.py +239 -0
app.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import re
4
+ import requests
5
+ import torch
6
+ import joblib
7
+ import numpy as np
8
+ import torch.nn.functional as F
9
+ from Bio.Blast import NCBIWWW
10
+ from Bio.Blast import NCBIXML
11
+ from transformers import AutoTokenizer, AutoModelForSequenceClassification, AutoModel
12
+
13
+ # ==========================================
14
+ # 1. LOAD AI MODELS (CACHED IN RAM)
15
+ # ==========================================
16
+ @torch.no_grad()
17
+ def load_oracle_brains():
18
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
19
+
20
+ clf = joblib.load("coding_classifier_universal.joblib")
21
+
22
+ tokenizer = AutoTokenizer.from_pretrained("DNABERT_Local", trust_remote_code=True)
23
+ model_base = AutoModel.from_pretrained("DNABERT_Local", trust_remote_code=True).to(device)
24
+ model_base.eval()
25
+
26
+ model_promoter = AutoModelForSequenceClassification.from_pretrained("llm_promoter_classifier_v2", trust_remote_code=True).to(device)
27
+ model_promoter.eval()
28
+
29
+ return device, tokenizer, clf, model_base, model_promoter
30
+
31
+ # Initialize models once (Gradio handles caching across sessions)
32
+ device, tokenizer, clf_coding, model_base, model_promoter = load_oracle_brains()
33
+
34
+ # ==========================================
35
+ # 2. BIOINFORMATICS PIPELINE (unchanged logic)
36
+ # ==========================================
37
+ def analyze_sequence(dna_sequence):
38
+ clean_seq = "".join(dna_sequence.split()).upper()
39
+ inputs = tokenizer([clean_seq], return_tensors="pt", max_length=300, truncation=True, padding=True)
40
+ inputs = {k: v.to(device) for k, v in inputs.items()}
41
+
42
+ with torch.no_grad():
43
+ out_base = model_base(**inputs)
44
+ mask = inputs["attention_mask"].unsqueeze(-1)
45
+ embedding = (out_base[0] * mask).sum(dim=1) / mask.sum(dim=1).clamp(min=1)
46
+ vector = embedding.float().cpu().numpy()
47
+
48
+ p_coding = clf_coding.predict_proba(vector)[0][1]
49
+
50
+ if p_coding >= 0.60:
51
+ return "GENE (Protein-Coding DNA)", p_coding, {
52
+ "Level 1 (Gene)": p_coding,
53
+ "Level 2 (Promoter)": "Skipped",
54
+ "Level 3 (Intergenic)": "Skipped"
55
+ }
56
+
57
+ with torch.no_grad():
58
+ outputs_promoter = model_promoter(**inputs)
59
+ logits = outputs_promoter.logits
60
+ probs = F.softmax(logits, dim=-1)
61
+ p_promoter = probs[0][0].item()
62
+
63
+ if p_promoter >= 0.50:
64
+ return "PROMOTER (Regulatory DNA)", p_promoter, {
65
+ "Level 1 (Gene)": p_coding,
66
+ "Level 2 (Promoter)": p_promoter,
67
+ "Level 3 (Intergenic)": 1 - p_promoter
68
+ }
69
+
70
+ return "INTERGENIC (Non-Functional Junk DNA)", 1 - p_promoter, {
71
+ "Level 1 (Gene)": p_coding,
72
+ "Level 2 (Promoter)": p_promoter,
73
+ "Level 3 (Intergenic)": 1 - p_promoter
74
+ }
75
+
76
+ def get_genomic_context(sequence, feature_type):
77
+ try:
78
+ result_handle = NCBIWWW.qblast("blastn", "nt", sequence,
79
+ entrez_query="Homo sapiens[Organism] AND biomol_genomic[PROP]",
80
+ hitlist_size=1)
81
+ blast_record = NCBIXML.read(result_handle)
82
+ except Exception as e:
83
+ return {"error": f"BLAST Connection Error: {e}"}
84
+
85
+ if not blast_record.alignments:
86
+ return {"error": "No human genome match found for this sequence."}
87
+
88
+ alignment = blast_record.alignments[0]
89
+ hsp = alignment.hsps[0]
90
+ accession = alignment.accession
91
+ full_title = alignment.title.split('|')[-1].strip()
92
+
93
+ chrom_match = re.search(r"chromosome\s([0-9XYMT]+)", alignment.title, re.IGNORECASE)
94
+ chrom = chrom_match.group(1) if chrom_match else None
95
+ location_string = f"Chromosome {chrom}" if chrom else f"Accession {accession} | {full_title}"
96
+
97
+ start = hsp.sbjct_start
98
+ end = hsp.sbjct_end
99
+ is_forward = (start < end)
100
+ strand_txt = "Forward (+)" if is_forward else "Reverse (-)"
101
+
102
+ if chrom is None:
103
+ return {"location": location_string, "start": start, "end": end, "strand": strand_txt,
104
+ "metadata": "BLAST returned a localized record without a chromosome. Ensembl mapping skipped."}
105
+
106
+ search_start = min(start, end) if feature_type == "CODING" else (end if is_forward else max(1, end - 15000))
107
+ search_end = max(start, end) if feature_type == "CODING" else (end + 15000 if is_forward else end)
108
+
109
+ try:
110
+ response = requests.get(
111
+ f"https://rest.ensembl.org/overlap/region/human/{chrom}:{search_start}-{search_end}?feature=gene",
112
+ headers={"Content-Type": "application/json"}
113
+ )
114
+ response.raise_for_status()
115
+ genes = response.json()
116
+ except Exception as e:
117
+ return {"location": location_string, "start": start, "end": end, "strand": strand_txt,
118
+ "metadata": f"Ensembl mapping unavailable: {e}"}
119
+
120
+ if not genes:
121
+ gene_desc = "No annotated genes found in this specific region."
122
+ else:
123
+ if feature_type == "PROMOTER":
124
+ genes.sort(key=lambda x: min(abs(x['start'] - end), abs(x['end'] - end)))
125
+ top_gene = genes[0]
126
+ name = top_gene.get('external_name', 'Unknown')
127
+ biotype = top_gene.get('biotype', 'Unknown').replace('_', ' ').title()
128
+ desc = top_gene.get('description', 'No description available.').split(' [')[0]
129
+ gene_desc = (f"Matches Gene: {name} | Type: {biotype} | Function: {desc}"
130
+ if feature_type == "CODING"
131
+ else f"Regulates Downstream Gene: {name} | Type: {biotype} | Function: {desc}")
132
+
133
+ return {"location": location_string, "start": start, "end": end, "strand": strand_txt, "metadata": gene_desc}
134
+
135
+
136
+ # ==========================================
137
+ # 3. GRADIO INTERFACE
138
+ # ==========================================
139
+ def gradio_inference(dna_sequence, run_mapping):
140
+ if len(dna_sequence.strip()) < 10:
141
+ return ("❌ Sequence too short!", "",
142
+ {"Level 1 (Gene)": "N/A", "Level 2 (Promoter)": "N/A", "Level 3 (Intergenic)": "N/A"}, "",
143
+ "⚠️ Please enter at least 10 base pairs.")
144
+
145
+ # Primary classification
146
+ label, conf, raw_scores = analyze_sequence(dna_sequence)
147
+
148
+ # Format internal statistics
149
+ def fmt(v):
150
+ if isinstance(v, str): return v
151
+ return f"{v:.2%}"
152
+
153
+ stats_lines = [
154
+ f"- Level 1 (Coding): {fmt(raw_scores['Level 1 (Gene)'])}",
155
+ f"- Level 2 (Promoter): {fmt(raw_scores['Level 2 (Promoter)'])}",
156
+ f"- Level 3 (Intergenic): {fmt(raw_scores['Level 3 (Intergenic)'])}"
157
+ ]
158
+
159
+ # Context mapping (optional, slow)
160
+ context_output = ""
161
+ if run_mapping:
162
+ if "Junk" in label or "INTERGENIC" in label:
163
+ context_output = "⚠️ Spatial mapping skipped β€” sequence classified as non-functional."
164
+ else:
165
+ context_type = "CODING" if "Coding" in label else "PROMOTER"
166
+ try:
167
+ # Run BLAST + Ensembl lookup (blocking)
168
+ import time; start_time = time.time()
169
+ context = get_genomic_context(dna_sequence, context_type)
170
+ elapsed = int(time.time() - start_time)
171
+
172
+ if "error" in context:
173
+ context_output = f"❌ Mapping failed: {context['error']}"
174
+ elif "location" in context:
175
+ context_lines = [
176
+ f"πŸ“ **Location:** {context['location']}",
177
+ f"🧬 **Strand:** {context['strand']}",
178
+ f"πŸ“ **Coordinates:** {context['start']:,} – {context['end']:,}",
179
+ f"ℹ️ **Notes:** {context['metadata']}"
180
+ ]
181
+ context_output = "\n".join(context_lines)
182
+ else:
183
+ context_output = "⚠️ Could not map sequence."
184
+ except Exception as e:
185
+ context_output = f"❌ Mapping error: {str(e)}"
186
+ else:
187
+ context_output = "⏸️ Spatial mapping skipped (disable checkbox to run)."
188
+
189
+ summary = (
190
+ f"βœ… Classification complete in ~1–2 sec.\n"
191
+ f"\n"
192
+ f"🎯 **Result:** {label}\n"
193
+ f"πŸ“Š **Confidence:** {conf:.2%}"
194
+ )
195
+
196
+ return summary, "\n".join(stats_lines), raw_scores, context_output, "⏳ Mapping query in progress..." if run_mapping else ""
197
+
198
+ with gr.Blocks(theme=gr.themes.Soft(), title="🧬 The Genomic Oracle") as demo:
199
+ gr.Markdown("# 🧬 The Genomic Oracle")
200
+ gr.Markdown("### A Deep Learning Cascade for DNA Sequence Classification")
201
+ gr.Info("πŸ’‘ Tip: Sequences longer than 50 bp yield more accurate predictions.")
202
+
203
+ with gr.Row():
204
+ with gr.Column(scale=1):
205
+ dna_input = gr.Textbox(
206
+ label="Enter DNA Sequence",
207
+ placeholder="e.g., ATGCGATCGATCGATCG...",
208
+ lines=6,
209
+ elem_id="dna_input"
210
+ )
211
+ run_mapping_cb = gr.Checkbox(
212
+ value=False,
213
+ label="Query NCBI BLAST for spatial mapping (Takes 1–3 minutes)"
214
+ )
215
+ submit_btn = gr.Button("πŸš€ Initialize Deep Scan", variant="primary")
216
+
217
+ with gr.Column(scale=2):
218
+ output_summary = gr.Textbox(label="βœ… Classification Summary", lines=8)
219
+ stats_panel = gr.Textbox(label="πŸ“Š Internal Statistics", lines=6, show_copy_button=True)
220
+
221
+ mapping_section = gr.Accordion("πŸ“ Genomic Context (BLAST/Ensembl)", open=False)
222
+ with mapping_section:
223
+ context_output = gr.Textbox(
224
+ label="Mapping Results",
225
+ lines=5,
226
+ placeholder="Results will appear here..."
227
+ )
228
+
229
+ # Live feedback
230
+ info_box = gr.Markdown("", elem_id="info_box")
231
+
232
+ submit_btn.click(
233
+ fn=lambda seq, map: (
234
+ *gradio_inference(seq, map)[:4],
235
+ gr.Textbox(visible=True) if map else gr.Textbox(visible=False)
236
+ ),
237
+ inputs=[dna_input, run_mapping_cb],
238
+ outputs=[output_summary, stats_panel, gr.State(), context_output]
239
+ )