jiehou commited on
Commit
c13d3ce
Β·
verified Β·
1 Parent(s): a344e1c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +628 -0
app.py ADDED
@@ -0,0 +1,628 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RNA Motif Multi-Structure Comparison Tool
3
+ Streamlit app for comparing multiple RNA motif structures simultaneously
4
+ Based on working pairwise alignment code
5
+ """
6
+
7
+ import streamlit as st
8
+ import numpy as np
9
+ import pandas as pd
10
+ from pathlib import Path
11
+ import io
12
+ import tempfile
13
+ import os
14
+ from itertools import combinations
15
+
16
+ # Import our RMSD calculation functions
17
+ from rmsd_utils import (
18
+ parse_residue_atoms,
19
+ get_backbone_sugar_and_selectbase_coords_fixed,
20
+ calculate_COM,
21
+ calculate_rotation_rmsd,
22
+ translate_rotate_coords,
23
+ get_backbone_sugar_coords_from_residue,
24
+ get_base_coords_from_residue
25
+ )
26
+
27
+ # Page configuration
28
+ st.set_page_config(
29
+ page_title="RNA Motif Multi-Structure Comparison",
30
+ page_icon="🧬",
31
+ layout="wide",
32
+ initial_sidebar_state="expanded"
33
+ )
34
+
35
+
36
+
37
+ # Custom CSS
38
+ st.markdown("""
39
+ <style>
40
+ /* Global base font size increase */
41
+ html {
42
+ font-size: 16px !important;
43
+ }
44
+ body {
45
+ font-size: 1.05rem !important;
46
+ }
47
+
48
+ .main-header {
49
+ font-size: 2.5rem;
50
+ font-weight: bold;
51
+ color: #1f77b4;
52
+ margin-bottom: 1rem;
53
+ }
54
+ .sub-header {
55
+ font-size: 1.2rem;
56
+ color: #666;
57
+ margin-bottom: 2rem;
58
+ }
59
+ .metric-box {
60
+ background-color: #f0f2f6;
61
+ padding: 1rem;
62
+ border-radius: 0.5rem;
63
+ margin: 0.5rem 0;
64
+ }
65
+
66
+ /* GLOBAL font size increase for main content */
67
+ .main * {
68
+ font-size: 1.05rem !important;
69
+ }
70
+
71
+ /* Restore proper header sizes */
72
+ .main h1 {
73
+ font-size: 2.5rem !important;
74
+ }
75
+ .main h2 {
76
+ font-size: 1.75rem !important;
77
+ }
78
+ .main h3 {
79
+ font-size: 1.5rem !important;
80
+ }
81
+
82
+ /* Button text should stay normal */
83
+ .main button {
84
+ font-size: 1rem !important;
85
+ }
86
+
87
+ /* Ultra-compact sidebar - MAXIMUM density */
88
+ section[data-testid="stSidebar"] {
89
+ padding-top: 0.2rem !important;
90
+ }
91
+ section[data-testid="stSidebar"] > div {
92
+ padding-top: 0.2rem !important;
93
+ }
94
+ /* Zero margin on markdown */
95
+ section[data-testid="stSidebar"] [data-testid="stMarkdownContainer"] {
96
+ margin: 0rem !important;
97
+ }
98
+ /* Minimal subheader spacing - THE KEY FIX */
99
+ section[data-testid="stSidebar"] h1,
100
+ section[data-testid="stSidebar"] h2,
101
+ section[data-testid="stSidebar"] h3 {
102
+ margin-top: 0.1rem !important;
103
+ margin-bottom: 0.2rem !important;
104
+ padding: 0rem !important;
105
+ line-height: 1.2 !important;
106
+ }
107
+ /* Tight widget spacing */
108
+ section[data-testid="stSidebar"] .stSelectbox,
109
+ section[data-testid="stSidebar"] .stNumberInput,
110
+ section[data-testid="stSidebar"] .stRadio,
111
+ section[data-testid="stSidebar"] .stFileUploader {
112
+ margin-top: 0.1rem !important;
113
+ margin-bottom: 0.2rem !important;
114
+ }
115
+ section[data-testid="stSidebar"] .stButton {
116
+ margin: 0.2rem 0 !important;
117
+ }
118
+ /* Minimal element container spacing */
119
+ section[data-testid="stSidebar"] .element-container {
120
+ margin: 0.1rem 0 !important;
121
+ }
122
+ /* Compact alert boxes */
123
+ section[data-testid="stSidebar"] .stAlert {
124
+ padding: 0.3rem 0.5rem !important;
125
+ margin: 0.1rem 0 !important;
126
+ }
127
+ /* Remove extra label spacing */
128
+ section[data-testid="stSidebar"] label {
129
+ margin-bottom: 0.1rem !important;
130
+ }
131
+ /* Compact captions */
132
+ section[data-testid="stSidebar"] .stCaptionContainer {
133
+ margin: 0.1rem 0 !important;
134
+ }
135
+ /* Remove hr spacing completely */
136
+ section[data-testid="stSidebar"] hr {
137
+ margin: 0.2rem 0 !important;
138
+ }
139
+ /* Keep sidebar font size normal */
140
+ section[data-testid="stSidebar"] * {
141
+ font-size: 0.875rem !important;
142
+ }
143
+ section[data-testid="stSidebar"] h3 {
144
+ font-size: 1rem !important;
145
+ }
146
+ </style>
147
+ """, unsafe_allow_html=True)
148
+
149
+
150
+
151
+ def save_uploaded_file(uploaded_file, directory):
152
+ """Save an uploaded file to a temporary directory"""
153
+ file_path = os.path.join(directory, uploaded_file.name)
154
+ with open(file_path, "wb") as f:
155
+ f.write(uploaded_file.getbuffer())
156
+ return file_path
157
+
158
+
159
+ def get_structure_info(pdb_path):
160
+ """Get information about a structure's residues."""
161
+ residues = parse_residue_atoms(pdb_path)
162
+
163
+ structure_info = []
164
+ for idx, res in enumerate(residues):
165
+ structure_info.append({
166
+ 'index': idx,
167
+ 'resnum': res['resnum'],
168
+ 'resname': res['resname'],
169
+ 'full_name': f"{idx+1}. {res['resname']} (residue #{res['resnum']})"
170
+ })
171
+
172
+ return structure_info
173
+
174
+
175
+ def extract_window_coords(residues, window_indices):
176
+ """Extract coordinates for a specific window of residues."""
177
+ all_coords = []
178
+ for idx in window_indices:
179
+ if idx < len(residues):
180
+ residue = residues[idx]
181
+ backbone_coords = get_backbone_sugar_coords_from_residue(residue)
182
+ all_coords.extend(backbone_coords)
183
+ base_coords = get_base_coords_from_residue(residue)
184
+ all_coords.extend(base_coords)
185
+
186
+ return np.asarray(all_coords)
187
+
188
+
189
+ def generate_windows_from_selection(selected_indices, win_size, win_type):
190
+ """Generate windows from selected indices"""
191
+ if len(selected_indices) < win_size:
192
+ return []
193
+
194
+ if win_type == "contiguous":
195
+ windows = []
196
+ for i in range(len(selected_indices) - win_size + 1):
197
+ windows.append(selected_indices[i:i + win_size])
198
+ return windows
199
+ else: # non-contiguous
200
+ return [list(combo) for combo in combinations(selected_indices, win_size)]
201
+
202
+
203
+ def main():
204
+ # Header
205
+ st.markdown('<p class="main-header">🧬 RNA Motif Multi-Structure Comparison</p>', unsafe_allow_html=True)
206
+ st.markdown('<p class="sub-header">Compare multiple RNA motifs simultaneously with window-based alignment</p>', unsafe_allow_html=True)
207
+
208
+ # Sidebar
209
+ st.sidebar.header("βš™οΈ Configuration")
210
+
211
+ # Step 1: File upload
212
+ st.sidebar.subheader("1️⃣ Upload Structures")
213
+ uploaded_files = st.sidebar.file_uploader(
214
+ "Upload RNA Motif PDB files",
215
+ type=['pdb', 'PDB'],
216
+ accept_multiple_files=True,
217
+ key="structures",
218
+ help="Upload all RNA motif structures to compare"
219
+ )
220
+
221
+ if not uploaded_files:
222
+ st.info("πŸ‘ˆ Please upload RNA motif PDB files to begin analysis")
223
+ with st.expander("ℹ️ About this tool"):
224
+ st.markdown("""
225
+ ### Multi-Structure RNA Motif Comparison
226
+
227
+ This tool compares multiple RNA motif structures simultaneously using window-based alignment.
228
+
229
+ **Workflow:**
230
+ 1. Upload all PDB structures
231
+ 2. Structures ranked by length (shortest first)
232
+ 3. Select residues for each structure via dropdown
233
+ 4. Choose reference structure (default: shortest)
234
+ 5. Configure window size and type
235
+ 6. Run comparison - all structures aligned to reference
236
+ 7. View all structures superimposed in 3D
237
+
238
+ **Features:**
239
+ - Window-based comparison (contiguous or non-contiguous)
240
+ - Best match selection per structure
241
+ - Interactive 3D visualization with all structures
242
+ - Color-coded structures
243
+ - RMSD-based alignment quality
244
+ """)
245
+ return
246
+
247
+ # Create temporary directory
248
+ temp_dir = tempfile.mkdtemp()
249
+
250
+ # Save uploaded files
251
+ for file in uploaded_files:
252
+ save_uploaded_file(file, temp_dir)
253
+
254
+ # Step 2: Rank structures by length and create dropdown
255
+ #st.sidebar.markdown("---")
256
+ st.sidebar.subheader("2️⃣ Structure Selection")
257
+
258
+ # Get structure sizes
259
+ structure_data = []
260
+ for file in uploaded_files:
261
+ file_path = os.path.join(temp_dir, file.name)
262
+ residues = parse_residue_atoms(file_path)
263
+ structure_data.append({
264
+ 'file': file,
265
+ 'name': file.name,
266
+ 'path': file_path,
267
+ 'num_residues': len(residues),
268
+ 'residues': residues
269
+ })
270
+
271
+ # Sort by number of residues (shortest first)
272
+ structure_data.sort(key=lambda x: x['num_residues'])
273
+
274
+ # Display ranked structures
275
+ st.markdown("---")
276
+ st.subheader("πŸ“Š Uploaded Structures (Ranked by Length)")
277
+
278
+ rank_df = pd.DataFrame([
279
+ {'Rank': i+1, 'Filename': s['name'], 'Residues': s['num_residues']}
280
+ for i, s in enumerate(structure_data)
281
+ ])
282
+ st.dataframe(rank_df, use_container_width=True)
283
+
284
+ # Step 3: Atom selection for each structure using dropdown
285
+ st.markdown("---")
286
+ st.subheader("πŸ”¬ Configure Atom Selections")
287
+
288
+ # Dropdown to select structure
289
+ selected_structure_name = st.selectbox(
290
+ "Select structure to configure (excluding two bases in 5' and 3' by default)",
291
+ options=[s['name'] for s in structure_data],
292
+ help="Choose a structure to configure its residue selection"
293
+ )
294
+
295
+ # Initialize session state for selections
296
+ if 'selections' not in st.session_state:
297
+ st.session_state['selections'] = {}
298
+
299
+ # Auto-initialize selections for all structures (exclude first and last residue)
300
+ if 'auto_initialized' not in st.session_state:
301
+ for struct in structure_data:
302
+ num_res = struct['num_residues']
303
+ if num_res > 4: # Need at least 5 residues to do 2 to len-2
304
+ # Auto-select from index 1 to index len-2 (which is residue 2 to residue len-1)
305
+ auto_selection = list(range(1, num_res - 1))
306
+ st.session_state['selections'][struct['name']] = auto_selection
307
+ else:
308
+ # For small structures, use all residues
309
+ st.session_state['selections'][struct['name']] = list(range(num_res))
310
+ st.session_state['auto_initialized'] = True
311
+
312
+ # Find selected structure data
313
+ selected_struct = next((s for s in structure_data if s['name'] == selected_structure_name), None)
314
+
315
+ if selected_struct:
316
+ st.markdown(f"### {selected_struct['name']} ({selected_struct['num_residues']} residues)")
317
+
318
+ # Display residue table
319
+ structure_info = get_structure_info(selected_struct['path'])
320
+ info_df = pd.DataFrame(structure_info)[['index', 'resnum', 'resname']]
321
+ info_df.columns = ['Index (0-based)', 'Residue Number', 'Base Type']
322
+ info_df['Index (1-based)'] = info_df['Index (0-based)'] + 1
323
+ info_df = info_df[['Index (1-based)', 'Index (0-based)', 'Residue Number', 'Base Type']]
324
+
325
+ with st.expander(f"πŸ“‹ View Residue Table", expanded=False):
326
+ st.dataframe(info_df, use_container_width=True, height=min(300, len(structure_info) * 35 + 38))
327
+
328
+ # Selection method
329
+ selection_method = st.radio(
330
+ f"Selection method for {selected_struct['name']}",
331
+ ["Select by range", "Select specific residues", "Use all residues"],
332
+ key=f"method_{selected_struct['name']}",
333
+ index = 1,
334
+ horizontal=True
335
+ )
336
+
337
+ selected_indices = []
338
+
339
+ if selection_method == "Select by range":
340
+ # Get current saved selection or auto-initialized values
341
+ current_selection = st.session_state['selections'].get(selected_struct['name'], [])
342
+ default_start = current_selection[0] + 2 if current_selection else 2
343
+ default_end = current_selection[-1] + 1 if current_selection else max(2, len(structure_info) - 2)
344
+
345
+ col1, col2 = st.columns(2)
346
+ with col1:
347
+ start_idx = st.number_input(
348
+ "Start index (1-based)",
349
+ min_value=1,
350
+ max_value=len(structure_info),
351
+ value=default_start,
352
+ key=f"start_{selected_struct['name']}"
353
+ )
354
+ with col2:
355
+ end_idx = st.number_input(
356
+ "End index (1-based, inclusive)",
357
+ min_value=1,
358
+ max_value=len(structure_info),
359
+ value=default_end,
360
+ key=f"end_{selected_struct['name']}"
361
+ )
362
+
363
+ if start_idx <= end_idx:
364
+ selected_indices = list(range(start_idx - 1, end_idx))
365
+ st.success(f"βœ“ Selected residues: {[i+1 for i in selected_indices]}")
366
+ else:
367
+ st.error("Start index must be ≀ end index")
368
+
369
+ elif selection_method == "Select specific residues":
370
+ # Get current selection or default
371
+ current_selection = st.session_state['selections'].get(selected_struct['name'], [])
372
+ default_names = [structure_info[i]['full_name'] for i in range(2, len(structure_info)-2)] if current_selection else []
373
+
374
+
375
+
376
+ selected_names = st.multiselect(
377
+ "Select residues",
378
+ options=[info['full_name'] for info in structure_info],
379
+ default=default_names,
380
+ key=f"specific_{selected_struct['name']}"
381
+ )
382
+
383
+ name_to_idx = {info['full_name']: info['index'] for info in structure_info}
384
+ selected_indices = [name_to_idx[name] for name in selected_names]
385
+ selected_indices.sort()
386
+
387
+ if selected_indices:
388
+ st.success(f"βœ“ Selected {len(selected_indices)} residues: {[i+1 for i in selected_indices]}")
389
+
390
+ else: # Use all residues
391
+ selected_indices = list(range(len(structure_info)))
392
+ st.info(f"βœ“ Using all {len(selected_indices)} residues")
393
+
394
+ # Save selection button
395
+ if st.button(f"πŸ’Ύ Save Selection for {selected_struct['name']}", type="primary"):
396
+ st.session_state['selections'][selected_struct['name']] = selected_indices
397
+ st.success(f"βœ… Saved selection for {selected_struct['name']}")
398
+
399
+ # Show current saved selections
400
+ if selected_struct['name'] in st.session_state['selections']:
401
+ saved_indices = st.session_state['selections'][selected_struct['name']]
402
+ st.info(f"**Current saved selection:** {len(saved_indices)} residues: {[i+1 for i in saved_indices]}")
403
+
404
+ # Step 4: Reference structure selection
405
+ #st.sidebar.markdown("---")
406
+ st.sidebar.subheader("3️⃣ Reference Structure")
407
+
408
+ # Default: shortest structure (first in sorted list)
409
+ default_ref = structure_data[0]['name']
410
+
411
+ reference_structure_name = st.sidebar.selectbox(
412
+ "Select reference structure",
413
+ options=[s['name'] for s in structure_data],
414
+ index=0,
415
+ help="All other structures will be aligned to this reference (default: shortest)"
416
+ )
417
+
418
+ ref_struct = next((s for s in structure_data if s['name'] == reference_structure_name), None)
419
+ if ref_struct:
420
+ st.sidebar.info(f"**Reference:** {ref_struct['name']} ({ref_struct['num_residues']} residues)")
421
+
422
+ # Step 5: Window configuration
423
+ #st.sidebar.markdown("---")
424
+ st.sidebar.subheader("4️⃣ Window Configuration")
425
+
426
+ # Check if all structures have selections
427
+ all_have_selections = all(s['name'] in st.session_state.get('selections', {}) for s in structure_data)
428
+
429
+ if all_have_selections:
430
+ selections = st.session_state['selections']
431
+ min_selection_size = min(len(selections[s['name']]) for s in structure_data)
432
+
433
+ window_size = st.sidebar.number_input(
434
+ "Window Size",
435
+ min_value=2,
436
+ max_value=min_selection_size,
437
+ value=min(4, min_selection_size),
438
+ step=1,
439
+ help="Number of residues per comparison window"
440
+ )
441
+
442
+ window_type = st.sidebar.radio(
443
+ "Window Type",
444
+ ["contiguous", "non-contiguous"],
445
+ index=0,
446
+ help="Contiguous: sliding windows. Non-contiguous: all combinations"
447
+ )
448
+ else:
449
+ st.sidebar.warning("⚠️ Configure selections for all structures first")
450
+ window_size = 4
451
+ window_type = "contiguous"
452
+
453
+ # Step 6: Run analysis
454
+ #st.sidebar.markdown("---")
455
+ st.sidebar.subheader("5️⃣ Run Analysis")
456
+
457
+ if st.sidebar.button("πŸš€ Run Multi-Structure Analysis", type="primary", disabled=not all_have_selections):
458
+ if not all_have_selections:
459
+ st.error("Please configure atom selections for all structures")
460
+ return
461
+
462
+ # Get selections
463
+ selections = st.session_state['selections']
464
+
465
+ # Find reference structure
466
+ ref_struct = next((s for s in structure_data if s['name'] == reference_structure_name), None)
467
+ ref_indices = selections[ref_struct['name']]
468
+
469
+ # Generate reference windows
470
+ ref_windows = generate_windows_from_selection(ref_indices, window_size, window_type)
471
+
472
+ if not ref_windows:
473
+ st.error(f"Reference structure needs at least {window_size} selected residues")
474
+ return
475
+
476
+ # Run comparisons
477
+ with st.spinner("Analyzing structures..."):
478
+ results = []
479
+
480
+ # For each reference window
481
+ for ref_window in ref_windows:
482
+ # Extract reference coords
483
+ ref_coords = extract_window_coords(ref_struct['residues'], ref_window)
484
+ ref_com = calculate_COM(ref_coords)
485
+ ref_sequence = ''.join([ref_struct['residues'][i]['resname'] for i in ref_window])
486
+
487
+ # Compare against all other structures
488
+ for query_struct in structure_data:
489
+ if query_struct['name'] == ref_struct['name']:
490
+ continue # Skip self-comparison
491
+
492
+ query_indices = selections[query_struct['name']]
493
+ query_windows = generate_windows_from_selection(query_indices, window_size, window_type)
494
+
495
+ for query_window in query_windows:
496
+ # Extract query coords
497
+ query_coords = extract_window_coords(query_struct['residues'], query_window)
498
+ query_com = calculate_COM(query_coords)
499
+ query_sequence = ''.join([query_struct['residues'][i]['resname'] for i in query_window])
500
+
501
+ # Calculate RMSD
502
+ U, RMSD = calculate_rotation_rmsd(ref_coords, query_coords, ref_com, query_com)
503
+
504
+ if U is None or RMSD is None:
505
+ RMSD = 999.0
506
+ U = np.eye(3)
507
+
508
+ results.append({
509
+ 'Reference': ref_struct['name'],
510
+ 'Ref_Window': ref_window,
511
+ 'Ref_Sequence': ref_sequence,
512
+ 'Query': query_struct['name'],
513
+ 'Query_Window': query_window,
514
+ 'Query_Sequence': query_sequence,
515
+ 'RMSD': RMSD,
516
+ 'Rotation_Matrix': U,
517
+ 'Ref_COM': ref_com,
518
+ 'Query_COM': query_com,
519
+ 'Ref_Path': ref_struct['path'],
520
+ 'Query_Path': query_struct['path']
521
+ })
522
+
523
+ results_df = pd.DataFrame(results)
524
+ st.session_state['results'] = results_df
525
+ st.session_state['structure_data'] = structure_data
526
+ st.session_state['reference_name'] = reference_structure_name
527
+
528
+ st.success(f"βœ… Analysis complete! {len(results_df)} comparisons performed.")
529
+
530
+ # Display results
531
+ if 'results' in st.session_state:
532
+ results_df = st.session_state['results']
533
+ structure_data = st.session_state['structure_data']
534
+ reference_name = st.session_state['reference_name']
535
+
536
+ st.markdown("---")
537
+ st.subheader("πŸ“Š Results Summary")
538
+
539
+ # RMSD threshold filter
540
+ col1, col2 = st.columns([1, 3])
541
+ with col1:
542
+ rmsd_threshold = st.slider(
543
+ "RMSD Threshold (Γ…)",
544
+ min_value=0.0,
545
+ max_value=10.0,
546
+ value=3.0,
547
+ step=0.1
548
+ )
549
+
550
+ filtered_df = results_df[results_df['RMSD'] <= rmsd_threshold]
551
+
552
+ with col2:
553
+ st.metric("Comparisons Below Threshold", f"{len(filtered_df)} / {len(results_df)}")
554
+
555
+ # Best match per structure
556
+ st.markdown("### πŸ† Best Match per Structure")
557
+ best_matches = results_df.loc[results_df.groupby('Query')['RMSD'].idxmin()]
558
+
559
+ best_display = best_matches[['Query', 'Query_Sequence', 'RMSD']].copy()
560
+ best_display['RMSD'] = best_display['RMSD'].round(3)
561
+ best_display.columns = ['Structure', 'Sequence', 'RMSD (Γ…)']
562
+ st.dataframe(best_display, use_container_width=True)
563
+
564
+ # Full results
565
+ with st.expander("πŸ“‹ All Comparison Results"):
566
+ display_df = filtered_df[['Reference', 'Ref_Sequence', 'Query', 'Query_Sequence', 'RMSD']].copy()
567
+ display_df['RMSD'] = display_df['RMSD'].round(3)
568
+ display_df = display_df.sort_values('RMSD').reset_index(drop=True)
569
+ st.dataframe(display_df, use_container_width=True, height=300)
570
+
571
+ # Visualization
572
+ st.markdown("---")
573
+ st.subheader("πŸ”¬ 3D Structure Visualization")
574
+
575
+ st.markdown("**Select a comparison to visualize:**")
576
+
577
+ # Create dropdown options showing all comparisons
578
+ viz_options = []
579
+ for idx, row in filtered_df.iterrows():
580
+ ref_res_str = ','.join([str(i+1) for i in row['Ref_Window']])
581
+ query_res_str = ','.join([str(i+1) for i in row['Query_Window']])
582
+ option_text = f"{row['Reference']}[{ref_res_str}] ({row['Ref_Sequence']}) vs {row['Query']}[{query_res_str}] ({row['Query_Sequence']}) | RMSD: {row['RMSD']:.3f} Γ…"
583
+ viz_options.append((idx, option_text))
584
+
585
+ if viz_options:
586
+ # Sort by RMSD (best first)
587
+ viz_options.sort(key=lambda x: filtered_df.loc[x[0], 'RMSD'])
588
+
589
+ selected_viz_idx = st.selectbox(
590
+ "Choose comparison to visualize",
591
+ options=[opt[0] for opt in viz_options],
592
+ format_func=lambda idx: next(opt[1] for opt in viz_options if opt[0] == idx),
593
+ help="All comparisons below RMSD threshold, sorted by RMSD"
594
+ )
595
+
596
+ # Get the selected comparison
597
+ selected_comparison = filtered_df.loc[selected_viz_idx]
598
+
599
+ # Import visualization function
600
+ from visualization_multi import create_pairwise_visualization
601
+
602
+ # Create visualization for selected comparison
603
+ try:
604
+ viz_html = create_pairwise_visualization(
605
+ ref_path=selected_comparison['Ref_Path'],
606
+ query_path=selected_comparison['Query_Path'],
607
+ ref_window=selected_comparison['Ref_Window'],
608
+ query_window=selected_comparison['Query_Window'],
609
+ rotation_matrix=selected_comparison['Rotation_Matrix'],
610
+ ref_com=selected_comparison['Ref_COM'],
611
+ query_com=selected_comparison['Query_COM'],
612
+ rmsd=selected_comparison['RMSD'],
613
+ ref_name=selected_comparison['Reference'],
614
+ query_name=selected_comparison['Query']
615
+ )
616
+
617
+ st.components.v1.html(viz_html, width=1400, height=750, scrolling=False)
618
+
619
+ except Exception as e:
620
+ st.error(f"Error creating visualization: {str(e)}")
621
+ import traceback
622
+ st.code(traceback.format_exc())
623
+ else:
624
+ st.warning("No comparisons below RMSD threshold to visualize")
625
+
626
+
627
+ if __name__ == "__main__":
628
+ main()