lyimo commited on
Commit
18ea056
·
verified ·
1 Parent(s): 43a91f2

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +259 -0
app.py ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import plotly.express as px
4
+ import plotly.graph_objects as go
5
+ from Bio import pairwise2
6
+ from collections import defaultdict
7
+ import re
8
+
9
+ # Define important gene regions (positions based on H37Rv)
10
+ IMPORTANT_GENES = {
11
+ 'rpoB': {'range': (759807, 763325), 'description': 'RNA polymerase β subunit (Rifampicin resistance)'},
12
+ 'katG': {'range': (2153889, 2156111), 'description': 'Catalase-peroxidase (Isoniazid resistance)'},
13
+ 'inhA': {'range': (1674202, 1675011), 'description': 'Enoyl-ACP reductase (Isoniazid resistance)'},
14
+ 'gyrA': {'range': (7302, 9818), 'description': 'DNA gyrase subunit A (Fluoroquinolone resistance)'}
15
+ }
16
+
17
+ def read_fasta_from_upload(uploaded_file):
18
+ """Read a FASTA file from Streamlit upload"""
19
+ content = uploaded_file.getvalue().decode('utf-8').strip()
20
+ parts = content.split('\n', 1)
21
+ sequence = ''.join(parts[1].split('\n')).replace(' ', '')
22
+ return sequence.upper()
23
+
24
+ def split_genome_into_chunks(sequence, chunk_size=10000, overlap=100):
25
+ """Split genome into manageable chunks for alignment"""
26
+ chunks = []
27
+ positions = []
28
+ for i in range(0, len(sequence), chunk_size - overlap):
29
+ chunk = sequence[i:i + chunk_size]
30
+ chunks.append(chunk)
31
+ positions.append(i)
32
+ return chunks, positions
33
+
34
+ def find_mutations_in_chunk(ref_chunk, query_chunk, chunk_start):
35
+ """Find mutations in a genome chunk"""
36
+ mutations = []
37
+
38
+ alignments = pairwise2.align.globalms(ref_chunk, query_chunk,
39
+ match=2,
40
+ mismatch=-3,
41
+ open=-10,
42
+ extend=-0.5)
43
+
44
+ if not alignments:
45
+ return mutations
46
+
47
+ alignment = alignments[0]
48
+ ref_aligned, query_aligned = alignment[0], alignment[1]
49
+
50
+ real_pos = 0
51
+ for i in range(len(ref_aligned)):
52
+ if ref_aligned[i] != '-':
53
+ real_pos += 1
54
+
55
+ if ref_aligned[i] != query_aligned[i]:
56
+ abs_pos = chunk_start + real_pos - 1
57
+ mut = {
58
+ 'position': abs_pos,
59
+ 'ref_base': ref_aligned[i],
60
+ 'query_base': query_aligned[i] if query_aligned[i] != '-' else 'None',
61
+ 'type': 'SNP' if ref_aligned[i] != '-' and query_aligned[i] != '-' else 'INDEL',
62
+ 'context': {
63
+ 'ref': ref_aligned[max(0,i-5):i] + '[' + ref_aligned[i] + ']' + ref_aligned[i+1:i+6],
64
+ 'query': query_aligned[max(0,i-5):i] + '[' + query_aligned[i] + ']' + query_aligned[i+1:i+6]
65
+ }
66
+ }
67
+
68
+ # Check if mutation is in an important gene
69
+ for gene, info in IMPORTANT_GENES.items():
70
+ start, end = info['range']
71
+ if start <= abs_pos <= end:
72
+ mut['gene'] = gene
73
+ mut['gene_position'] = abs_pos - start + 1
74
+ mut['gene_description'] = info['description']
75
+
76
+ mutations.append(mut)
77
+
78
+ return mutations
79
+
80
+ def visualize_mutations(mutations, genome_length):
81
+ """Create mutation visualization plots"""
82
+ # Prepare data for gene region visualization
83
+ gene_regions = []
84
+ for gene, info in IMPORTANT_GENES.items():
85
+ start, end = info['range']
86
+ gene_regions.append({
87
+ 'gene': gene,
88
+ 'start': start,
89
+ 'end': end,
90
+ 'y': 1
91
+ })
92
+
93
+ # Create genome-wide plot
94
+ fig = go.Figure()
95
+
96
+ # Add gene regions as rectangles
97
+ for region in gene_regions:
98
+ fig.add_trace(go.Scatter(
99
+ x=[region['start'], region['end']],
100
+ y=[region['y'], region['y']],
101
+ mode='lines',
102
+ name=region['gene'],
103
+ line=dict(width=10),
104
+ hoverinfo='text',
105
+ hovertext=f"{region['gene']}: {region['start']}-{region['end']}"
106
+ ))
107
+
108
+ # Add mutations as scatter points
109
+ mutation_data = pd.DataFrame(mutations)
110
+ if not mutation_data.empty:
111
+ fig.add_trace(go.Scatter(
112
+ x=mutation_data['position'],
113
+ y=[1.1] * len(mutation_data),
114
+ mode='markers',
115
+ name='Mutations',
116
+ marker=dict(
117
+ color=['red' if t == 'SNP' else 'blue' for t in mutation_data['type']],
118
+ size=8
119
+ ),
120
+ hoverinfo='text',
121
+ hovertext=mutation_data.apply(
122
+ lambda x: f"Position: {x['position']}<br>"
123
+ f"Type: {x['type']}<br>"
124
+ f"Change: {x['ref_base']}->{x['query_base']}",
125
+ axis=1
126
+ )
127
+ ))
128
+
129
+ fig.update_layout(
130
+ title="Genome-wide Mutation Distribution",
131
+ xaxis_title="Genome Position",
132
+ yaxis_visible=False,
133
+ showlegend=True,
134
+ height=400
135
+ )
136
+
137
+ return fig
138
+
139
+ def analyze_mutations(mutations):
140
+ """Generate comprehensive mutation statistics"""
141
+ stats = {
142
+ 'total_mutations': len(mutations),
143
+ 'snps': len([m for m in mutations if m['type'] == 'SNP']),
144
+ 'indels': len([m for m in mutations if m['type'] == 'INDEL']),
145
+ 'by_gene': defaultdict(int),
146
+ 'important_mutations': []
147
+ }
148
+
149
+ for mut in mutations:
150
+ if 'gene' in mut:
151
+ stats['by_gene'][mut['gene']] += 1
152
+ stats['important_mutations'].append(mut)
153
+
154
+ return stats
155
+
156
+ def main():
157
+ st.title("M. tuberculosis Full Genome Comparison")
158
+
159
+ st.markdown("""
160
+ This tool performs whole-genome comparison of M. tuberculosis strains, identifying mutations
161
+ and analyzing resistance-associated genes.
162
+
163
+ **Instructions:**
164
+ 1. Upload your reference genome (typically H37Rv)
165
+ 2. Upload your query genome (clinical isolate)
166
+ 3. Configure analysis parameters if needed
167
+ 4. Run the analysis
168
+ """)
169
+
170
+ # File upload
171
+ col1, col2 = st.columns(2)
172
+ with col1:
173
+ reference_file = st.file_uploader("Reference Genome (FASTA)", type=['fasta', 'fa'])
174
+ with col2:
175
+ query_file = st.file_uploader("Query Genome (FASTA)", type=['fasta', 'fa'])
176
+
177
+ # Analysis parameters
178
+ with st.expander("Advanced Settings"):
179
+ chunk_size = st.slider("Analysis chunk size (bp)", 5000, 20000, 10000, 1000)
180
+ overlap = st.slider("Chunk overlap (bp)", 50, 200, 100, 10)
181
+
182
+ if reference_file and query_file:
183
+ if st.button("Run Analysis"):
184
+ with st.spinner("Analyzing genomes..."):
185
+ try:
186
+ # Read sequences
187
+ ref_genome = read_fasta_from_upload(reference_file)
188
+ query_genome = read_fasta_from_upload(query_file)
189
+
190
+ # Show progress
191
+ progress_bar = st.progress(0)
192
+ status = st.empty()
193
+
194
+ # Split genomes
195
+ status.text("Splitting genomes into chunks...")
196
+ ref_chunks, chunk_positions = split_genome_into_chunks(ref_genome, chunk_size, overlap)
197
+ query_chunks, _ = split_genome_into_chunks(query_genome, chunk_size, overlap)
198
+
199
+ # Process chunks
200
+ status.text("Analyzing mutations...")
201
+ all_mutations = []
202
+ total_chunks = len(ref_chunks)
203
+
204
+ for i, (ref_chunk, query_chunk, chunk_start) in enumerate(zip(ref_chunks, query_chunks, chunk_positions)):
205
+ progress_bar.progress((i + 1) / total_chunks)
206
+ mutations = find_mutations_in_chunk(ref_chunk, query_chunk, chunk_start)
207
+ all_mutations.extend(mutations)
208
+
209
+ # Analysis complete
210
+ progress_bar.empty()
211
+ status.empty()
212
+
213
+ # Generate results
214
+ stats = analyze_mutations(all_mutations)
215
+
216
+ # Display results
217
+ st.success("Analysis complete!")
218
+
219
+ # Summary statistics
220
+ st.header("Results Summary")
221
+ col1, col2, col3 = st.columns(3)
222
+ col1.metric("Total Mutations", stats['total_mutations'])
223
+ col2.metric("SNPs", stats['snps'])
224
+ col3.metric("INDELs", stats['indels'])
225
+
226
+ # Genome-wide visualization
227
+ st.plotly_chart(visualize_mutations(all_mutations, len(ref_genome)))
228
+
229
+ # Gene-specific results
230
+ st.header("Resistance-Associated Genes")
231
+ gene_mutations = pd.DataFrame([
232
+ {"Gene": gene, "Mutations": count, "Description": IMPORTANT_GENES[gene]['description']}
233
+ for gene, count in stats['by_gene'].items()
234
+ ])
235
+
236
+ if not gene_mutations.empty:
237
+ st.dataframe(gene_mutations)
238
+
239
+ # Detailed mutation table
240
+ if stats['important_mutations']:
241
+ st.header("Detailed Mutation Analysis")
242
+ mutations_df = pd.DataFrame(stats['important_mutations'])
243
+ st.dataframe(mutations_df)
244
+
245
+ # Download option
246
+ csv = mutations_df.to_csv(index=False)
247
+ st.download_button(
248
+ "Download Results (CSV)",
249
+ csv,
250
+ "mtb_mutations.csv",
251
+ "text/csv",
252
+ key='download-csv'
253
+ )
254
+
255
+ except Exception as e:
256
+ st.error(f"Analysis error: {str(e)}")
257
+
258
+ if __name__ == "__main__":
259
+ main()