-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathapp.py
More file actions
2247 lines (2031 loc) · 88.7 KB
/
Copy pathapp.py
File metadata and controls
2247 lines (2031 loc) · 88.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
app.py — Streamlit UI layer for Anyone Can Dock.
Responsibilities:
- All st.* widgets, layout, CSS, HTML, JavaScript
- Session state management
- Imports all computation from core.py
- No business logic here (no bond-order math, no receptor prep, etc.)
Architecture rule: if a function has no st.* call, it belongs in core.py.
"""
import os
import io
import sys
import json
import time
import base64
import tempfile
import zipfile
import textwrap
from pathlib import Path
import streamlit as st
# ---------------------------------------------------------------------------
# Core imports (computation layer — no Streamlit inside)
# ---------------------------------------------------------------------------
from core import (
# Vina
get_vina_binary, run_vina,
# Receptor
prepare_receptor, scan_ligands, detect_cocrystal_ligand,
write_box_pdb, write_vina_config, convert_cif_to_pdb, is_cif_file,
strip_and_convert_receptor,
# Ligand
prepare_ligand, prepare_ligand_from_file, smiles_from_file,
# Bond orders / SDF
fix_sdf_bond_orders, load_mols_from_sdf, write_single_pose,
write_single_pose_pdb, convert_sdf_to_v2000,
# Analysis
get_interacting_residues, calc_rmsd_heavy,
# PoseView
call_poseview_v1, call_poseview2_ref, warm_poseview_cache,
# Image
svg_to_png, stamp_png,
# 2D diagram
draw_interaction_diagram, draw_interaction_diagram_data,
draw_interactions_rdkit, draw_interactions_rdkit_classic,
draw_interaction_diagram_interactive,
# Search (moved from old app.py)
search_pubchem, search_rcsb,
# ADMET (moved + upgraded)
calc_adme_properties, _load_admet_model,
# Utilities
run_cmd, check_obabel,
HEME_RESNAMES, METAL_RESNAMES,
)
# ---------------------------------------------------------------------------
# Page config
# ---------------------------------------------------------------------------
st.set_page_config(
page_title="Anyone can dock, Everyone can do!",
page_icon="🧩",
layout="wide",
initial_sidebar_state="collapsed",
)
# ---------------------------------------------------------------------------
# CSS
# ---------------------------------------------------------------------------
st.markdown("""
<style>
/* General */
.main .block-container { padding-top: 1.2rem; }
h1 { font-size: 1.65rem !important; font-weight: 700; }
h2 { font-size: 1.25rem !important; }
h3 { font-size: 1.05rem !important; }
/* Score badge */
.score-badge {
display: inline-block;
background: #0e4d92;
color: white;
font-weight: 700;
font-size: 1.25rem;
padding: 4px 18px;
border-radius: 20px;
letter-spacing: 0.03em;
}
/* Metric card */
.metric-card {
background: #f7f9fc;
border: 1.5px solid #dde3ef;
border-radius: 10px;
padding: 10px 14px;
text-align: center;
margin-bottom: 6px;
}
.metric-card .label {
font-size: 11px;
color: #666;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.metric-card .value {
font-size: 1.5rem;
font-weight: 700;
color: #1a1a2e;
}
/* ADME ML badge */
.ml-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: #EAF3E5;
border: 1.5px solid #4CAF50;
border-radius: 20px;
padding: 4px 14px;
font-size: 12px;
font-weight: 600;
color: #2e7d32;
margin-bottom: 8px;
}
.ml-badge-fallback {
display: inline-flex;
align-items: center;
gap: 6px;
background: #FFF8E1;
border: 1.5px solid #FFA000;
border-radius: 20px;
padding: 4px 14px;
font-size: 12px;
font-weight: 600;
color: #e65100;
margin-bottom: 8px;
}
/* CYP bar */
.cyp-bar-wrap {
background: #eee;
border-radius: 6px;
height: 8px;
width: 100%;
margin-top: 3px;
}
.cyp-bar-fill {
height: 8px;
border-radius: 6px;
background: linear-gradient(90deg, #43a047, #e53935);
}
</style>
""", unsafe_allow_html=True)
# ---------------------------------------------------------------------------
# Cached resource loaders
# ---------------------------------------------------------------------------
@st.cache_resource(show_spinner="Locating AutoDock-Vina binary…")
def _cached_vina():
return get_vina_binary()
@st.cache_resource(show_spinner="Loading ADMET-AI ML models (first run may take ~30 s)…")
def _cached_admet_model():
"""Cache the ADMET-AI model at the Streamlit resource level."""
return _load_admet_model()
# ---------------------------------------------------------------------------
# Session state defaults
# ---------------------------------------------------------------------------
_SS_DEFAULTS = {
# Receptor
"src_mode": "Download PDB",
"pdb_id": "",
"pdb_token": "",
"rcsb_fmt": "CIF",
"rec_search_query": "",
"rec_search_results": [],
"rec_prepared": False,
"rec_info": {},
"center_mode": "Auto-detect co-crystal ligand",
"mda_sel": "",
"cx": 0.0, "cy": 0.0, "cz": 0.0,
"sx": 16, "sy": 16, "sz": 16,
"blind_docking": False,
"keep_cofactors": True,
"keep_metals": True,
"rcsb_prefer_complete": True,
# Ligand
"lig_input_mode": "PubChem / SMILES",
"smiles_in": "",
"lig_name_in": "ligand",
"ph_in": 7.4,
"lig_upload_prot": "Use the uploaded form",
"lig_prepared": False,
"lig_info": {},
"pub_search_query": "",
"pub_search_result": {},
# Docking
"docking_done": False,
"docking_result": {},
"exh_slider": 16,
"n_modes": 10,
"e_range": 3,
"do_redock": True,
"redock_smiles": "",
# Results
"pose_sel": 1,
"anim_spd": 1500,
"bp_cutoff": 3.5,
"bp_show_labels": True,
"bp_show_surface": False,
# Batch
"b_input_mode": "SMILES list (text area)",
"b_smiles_text": "",
"b_exh": 8,
"b_nm": 9,
"b_do_redock": False,
# ADME
"adme_results": {},
}
for _k, _v in _SS_DEFAULTS.items():
if _k not in st.session_state:
st.session_state[_k] = _v
# ---------------------------------------------------------------------------
# Working directory helpers
# ---------------------------------------------------------------------------
def _wdir() -> Path:
if "wdir" not in st.session_state:
st.session_state["wdir"] = Path(tempfile.mkdtemp(prefix="acd_"))
return st.session_state["wdir"]
# ---------------------------------------------------------------------------
# 50 widget help strings (English only)
# ---------------------------------------------------------------------------
_H = {
# ---- Receptor (10) -----------------------------------------------
"src_mode": (
"Source of the protein 3D structure.\n\n"
"📖 Download = fetch from RCSB by 4-letter PDB ID.\n"
" Upload = use your own .pdb or .cif file.\n"
"⚙️ Use Upload for homology models or pre-prepared structures.\n"
"⚠️ CIF recommended for large entries (>100 kDa) — PDB truncates at 99,999 atoms."
),
"pdb_id": (
"4-letter Protein Data Bank identifier (e.g. 1M17, 4AGN).\n\n"
"📖 Each ID is a unique experimental structure.\n"
" Same protein can have many IDs with different resolutions or ligands.\n"
"⚙️ Prefer resolution < 2.5 Å, no missing residues.\n"
"⚠️ Different entries of the same protein can give different docking results.\n"
" Search at rcsb.org to compare options."
),
"rcsb_fmt": (
"Download format from RCSB.\n\n"
"📖 PDB = classic format, widely compatible.\n"
" CIF = modern mmCIF, required for large structures.\n"
"⚙️ Use CIF if the protein has > 62 chains or > 99,999 atoms.\n"
"⚠️ If PDB download fails, CIF is tried automatically."
),
"center_mode": (
"Method to set the center of the docking search box.\n\n"
"📖 The box MUST cover the binding site.\n"
" Wrong center = Vina searches the wrong region.\n"
"⚙️ Auto-detect: best when co-crystal ligand is in the PDB.\n"
" Manual XYZ : enter coordinates from a known binding site.\n"
" ProDy sel : select by residue/chain string.\n"
"⚠️ If no ligand is found, grid defaults to protein centroid."
),
"mda_sel": (
"ProDy atom selection string to define the grid center.\n"
"The centroid of selected atoms becomes the box center.\n\n"
"📖 Examples:\n"
" resid 702 and chain A\n"
" resname ATP and chain B\n"
" resid 100 to 120 and chain A\n"
"⚙️ Use residue numbers from literature or binding site databases.\n"
"⚠️ Chain IDs must match exactly — check your PDB file header."
),
"box_size": (
"Docking search box size along this axis (Angstroms).\n\n"
"📖 Vina only searches for poses within this box.\n"
"⚙️ 16–20 Å : most drug-like molecules (default).\n"
" 24–30 Å : large ligands or flexible loops.\n"
" 12 Å : tight rigid binding pockets.\n"
"⚠️ Box > 30 Å significantly increases calculation time."
),
"blind_docking": (
"Expand the search box to cover the entire protein.\n\n"
"📖 Use when the binding site is completely unknown.\n"
" Vina will explore all surface pockets simultaneously.\n"
"⚙️ Enable for novel targets with no structural data.\n"
"⚠️ 5–20× slower than focused docking.\n"
" Results are less reliable — use to identify candidate sites,\n"
" then re-dock with a focused box."
),
"keep_cofactors": (
"Retain cofactors (ATP, FAD, NAD, CoA, heme…) in the receptor.\n\n"
"📖 Cofactors shape the binding pocket.\n"
" Removing them creates a cavity that may not exist in vivo.\n"
"⚙️ Keep ON : cofactor is always present (most cases).\n"
" Turn OFF : designing a competitive cofactor-site inhibitor.\n"
"⚠️ ATP in kinases and FAD in oxidoreductases are critical —\n"
" removing them usually gives unrealistic poses."
),
"keep_metals": (
"Retain metal ions (Zn, Mg, Ca, Fe, Cu…) in the receptor.\n\n"
"📖 Metal ions in active sites act as coordination centers.\n"
" Chelating ligands must be placed near the ion.\n"
"⚙️ Keep ON : metalloenzymes (proteases, carbonic anhydrase).\n"
" Turn OFF : testing non-chelating pose hypotheses.\n"
"⚠️ Vina does not model metal coordination explicitly.\n"
" Validate metal-binding poses with QM or specialized tools."
),
"rcsb_prefer_complete": (
"Prefer RCSB entries with no missing residues in search results.\n\n"
"📖 Missing residues = structural gaps.\n"
" Gaps near the binding site reduce docking reliability.\n"
"⚙️ Keep ON for most targets (default).\n"
" Disable if the best-resolution structure has missing residues.\n"
"⚠️ Even 'complete' structures may have flexible loops.\n"
" Always inspect the structure before docking."
),
# ---- Ligand (5) --------------------------------------------------
"lig_input_mode": (
"How to provide the ligand structure.\n\n"
"📖 PubChem/SMILES : text-based molecule representation.\n"
" Upload : use an existing 3D file (.sdf/.mol2/.pdb).\n"
" Ketcher : draw the structure interactively.\n"
"⚙️ SMILES for known drugs — search PubChem by name.\n"
" Upload if you already have a prepared 3D file.\n"
"⚠️ Uploaded files skip protonation — ensure correct H atoms."
),
"smiles_in": (
"SMILES string for the ligand (e.g. CCO = ethanol).\n\n"
"📖 Text representation of molecular structure.\n"
" Each letter = atom, numbers = ring closures.\n"
"⚙️ Copy from PubChem, ChEMBL, or DrugBank.\n"
" Use the search box above to auto-fill from compound name.\n"
"⚠️ Include correct stereochemistry (@/@@) if chiral centers exist.\n"
" Wrong stereochemistry = unreliable docking results."
),
"lig_name_in": (
"Short identifier for output files from this ligand.\n\n"
"📖 Used to name: name.pdbqt, name_out.sdf, name_scores.csv.\n"
"⚙️ Keep it short and descriptive (e.g. Erlotinib, Cpd_01).\n"
"⚠️ No spaces or special characters — use underscores instead."
),
"ph_in": (
"pH used to calculate the ligand protonation state.\n\n"
"📖 pH determines which atoms are charged.\n"
" Amines (pKa ~10) are +1 at pH 7.4.\n"
" Carboxylic acids (pKa ~4.5) are -1 at pH 7.4.\n"
"⚙️ 7.4 = plasma/cytosol (default).\n"
" 6.5 = tumor microenvironment.\n"
" 5.0 = lysosome / endosome.\n"
"⚠️ Wrong pH can shift net charge ±1 — large effect on affinity."
),
"lig_upload_prot": (
"How to handle hydrogens for an uploaded structure file.\n\n"
"📖 Use uploaded form : keep all atoms exactly as in the file.\n"
" Protonate at pH : re-run Dimorphite-DL on extracted SMILES.\n"
"⚙️ 'As uploaded' if file is already correctly prepared.\n"
" 'At pH' if file is only a 3D template for coordinates.\n"
"⚠️ 'As uploaded' skips all protonation checks.\n"
" Incorrect H atoms in the file = poor docking quality."
),
# ---- Docking (5) -------------------------------------------------
"exh_slider": (
"Search thoroughness — higher = more reliable, slower.\n\n"
"📖 Controls number of Monte Carlo steps Vina performs.\n"
" Higher values reduce chance of missing the global minimum.\n"
"⚙️ 8 = quick test / screening.\n"
" 16 = standard work (default).\n"
" 32 = publication quality.\n"
" 64 = maximum (30–60 min per ligand).\n"
"⚠️ Run time ∝ exhaustiveness — plan accordingly."
),
"n_modes": (
"Maximum number of binding poses to output.\n\n"
"📖 Poses ranked best (1) to worst by binding affinity.\n"
" Pose 1 = predicted best binding mode.\n"
"⚙️ 9–10 : standard (default).\n"
" 15–20 : when diverse alternative poses are needed.\n"
"⚠️ More poses does not improve the best result.\n"
" Poses below rank 5 are often unreliable."
),
"e_range": (
"Max allowed energy gap from the best pose (kcal/mol).\n\n"
"📖 Poses worse than (best + range) are discarded.\n"
" Example: best=-9.0, range=3 → discard above -6.0.\n"
"⚙️ 3 kcal/mol = standard (default).\n"
" 1–2 = only tightly clustered top poses.\n"
" 4–5 = explore diverse binding modes.\n"
"⚠️ Large range + many poses can produce low-quality output."
),
"do_redock": (
"Re-dock the known co-crystal ligand as a validation control.\n\n"
"📖 If re-docking reproduces the crystal pose (RMSD ≤ 2 Å),\n"
" the protocol is validated. Score = reference baseline.\n"
"⚙️ Enable when a co-crystal ligand exists in the PDB entry.\n"
" Disable for fast screening (saves one docking run).\n"
"⚠️ RMSD > 2 Å = protocol may have issues.\n"
" Check: box placement, receptor prep, ligand SMILES."
),
"redock_smiles": (
"SMILES and name for the redocking reference ligand.\n"
"Format: SMILES LigandName (space-separated).\n\n"
"📖 Example: CCO Ethanol\n"
" Name labels output files and score plots.\n"
"⚙️ Copy SMILES from PubChem or ChEMBL.\n"
"⚠️ SMILES must exactly match the PDB co-crystal ligand\n"
" chemistry — not just a similar compound."
),
# ---- Results (7) -------------------------------------------------
"pose_sel": (
"Select which docking pose to inspect.\n\n"
"📖 Pose 1 = best predicted binding affinity.\n"
" Higher poses = progressively less favorable.\n"
"⚙️ Start with pose 1. Check pose 2–3 for alternative modes.\n"
"⚠️ Pose rank ≠ biological relevance.\n"
" A slightly worse pose with better pharmacophore match\n"
" may be more meaningful."
),
"anim_spd": (
"Interval between frames in the pose animation (milliseconds).\n\n"
"📖 Lower = faster cycling. Higher = longer per pose.\n"
"⚙️ 500 ms = quick overview of all poses.\n"
" 1500 ms = default, comfortable viewing speed.\n"
" 3000 ms = slow, careful inspection per pose."
),
"bp_cutoff": (
"Max distance (Å) to count a residue as interacting with the ligand.\n\n"
"📖 Residues within cutoff → shown as orange sticks + labeled.\n"
" < 3.5 Å : direct interactions (H-bond, ionic).\n"
" < 5.0 Å : includes hydrophobic contacts.\n"
"⚙️ Start at 3.5 Å. Increase if too few residues shown.\n"
"⚠️ > 5.0 Å includes irrelevant residues — clutters the view."
),
"bp_show_labels": (
"Show residue name + number + chain as 3D labels.\n\n"
"📖 Yellow labels on each interacting residue in the viewer.\n"
"⚙️ Enable for analysis, disable for clean screenshots.\n"
"⚠️ Dense pockets → overlapping labels.\n"
" Reduce distance cutoff to show fewer residues."
),
"bp_show_surface": (
"Render a solvent-excluded surface (SES) around the protein.\n\n"
"📖 Shows the 3D shape of the binding pocket.\n"
" Good fit = ligand sits snugly inside the cavity.\n"
"⚙️ Enable to assess shape complementarity.\n"
" Disable for clearer residue contact view.\n"
"⚠️ Increases GPU load — may be slow on mobile or large proteins."
),
"diag_cutoff": (
"Interaction distance cutoff for the 2D diagram (Å).\n\n"
"📖 Residues within this distance → drawn as labeled circles.\n"
" Circle color = interaction type (see legend below diagram).\n"
"⚙️ 4.0–4.5 Å = captures most meaningful interactions.\n"
" Can differ from the 3D pocket viewer cutoff.\n"
"⚠️ < 3.0 Å may miss hydrophobic contacts.\n"
" > 5.0 Å clutters the diagram with weak contacts."
),
"diag_max_res": (
"Max residues shown in the 2D interaction diagram.\n\n"
"📖 Top N by priority (metal > ionic > H-bond > hydrophobic).\n"
" Remaining residues are omitted for readability.\n"
"⚙️ 10–14 : standard (default).\n"
" 6–8 : simple ligands with few contacts.\n"
" 18–20 : complex multi-residue interactions.\n"
"⚠️ Too many residues → unreadable diagram."
),
# ---- ADME (13) ---------------------------------------------------
"adme_mw": (
"Total molecular mass (Daltons).\n\n"
"📖 ≤ 500 Da : passes Lipinski RO5.\n"
" > 600 Da : likely poor oral absorption.\n"
" Most oral drugs: 200–500 Da.\n"
"⚙️ Reduce: remove heavy groups, use bioisosteres.\n"
"⚠️ MW alone is insufficient — consider all descriptors."
),
"adme_logp": (
"Lipophilicity (octanol-water log partition coefficient).\n\n"
"📖 < 0 : too hydrophilic — may not cross cell membranes.\n"
" 0–3 : ideal range.\n"
" 3–5 : acceptable (Lipinski ≤ 5).\n"
" > 5 : poor solubility, toxicity risk.\n"
"⚙️ Reduce: add polar groups (OH, NH, COOH).\n"
" Increase: add aromatic rings, halogens.\n"
"⚠️ LogP > 5 correlates strongly with hERG inhibition."
),
"adme_tpsa": (
"Sum of polar atom surface areas — predicts membrane permeability.\n\n"
"📖 < 90 Ų : good GI absorption + BBB penetration.\n"
" 90–140 Ų: moderate GI absorption.\n"
" > 140 Ų : poor oral bioavailability.\n"
" > 200 Ų : essentially impermeable.\n"
"⚙️ CNS drugs: target TPSA < 90 Ų.\n"
" Reduce: N-methylation, prodrug strategies.\n"
"⚠️ Calculated from 2D topology — an approximation, not true 3D."
),
"adme_hbd": (
"Number of NH and OH groups (hydrogen bond donors).\n\n"
"📖 ≤ 3 : good for membrane permeability.\n"
" ≤ 5 : Lipinski RO5 threshold.\n"
" > 5 : Lipinski violation, poor membrane crossing.\n"
"⚙️ Reduce: N-methylation, prodrug strategies.\n"
"⚠️ Each donor has a desolvation cost at the membrane.\n"
" Too many donors = high penalty even if LogP is acceptable."
),
"adme_hba": (
"Number of N and O atoms (hydrogen bond acceptors).\n\n"
"📖 ≤ 7 : good.\n"
" ≤ 10 : Lipinski RO5 threshold.\n"
" > 10 : Lipinski violation.\n"
"⚙️ Reduce: remove ether oxygens, use bioisosteres.\n"
"⚠️ HBA > 12 strongly predicts poor oral absorption."
),
"adme_qed": (
"Composite drug-likeness score (0 = worst, 1 = best).\n\n"
"📖 0.67–1.0 : high drug-likeness.\n"
" 0.35–0.67: moderate.\n"
" 0–0.35 : poor drug-likeness.\n"
" Most oral drugs: 0.5–0.9.\n"
"⚙️ Improve by optimizing the 8 underlying properties.\n"
"⚠️ Derived from oral small molecules — not valid\n"
" for biologics, PROTACs, or other modalities."
),
"adme_fsp3": (
"Fraction of carbon atoms that are sp³ (non-aromatic).\n\n"
"📖 > 0.25: better solubility, selectivity, clinical success.\n"
" < 0.25: flat molecule — tends to aggregate, less selective.\n"
"⚙️ Increase: add piperidine, cyclohexane, chiral centers.\n"
" 'Escape from flatland' — a key medicinal chemistry strategy.\n"
"⚠️ A guideline, not a rule. Some flat drugs work very well."
),
"adme_gi": (
"Predicted oral GI absorption.\n\n"
"📖 High : good for oral dosing.\n"
" Medium : may need formulation help.\n"
" Low : consider IV or alternative route.\n"
"⚙️ Improve: reduce MW, HBD, TPSA.\n"
"⚠️ Estimated value only.\n"
" Confirm with Caco-2 assay or in vivo data."
),
"adme_bbb": (
"Predicted blood-brain barrier penetration.\n\n"
"📖 Penetrant : expected CNS exposure.\n"
" Possible : partial penetration.\n"
" Non-penetrant : mainly peripheral.\n"
"⚙️ Increase: TPSA < 90, MW < 450, HBD ≤ 3, LogP 1–3.\n"
" Decrease: add polar groups, increase MW.\n"
"⚠️ For peripheral targets, Non-penetrant is preferred\n"
" to avoid CNS side effects."
),
"adme_pgp": (
"Predicted P-glycoprotein efflux transporter substrate.\n\n"
"📖 Likely : P-gp may actively efflux the drug.\n"
" Reduces CNS penetration + oral bioavailability.\n"
" Unlikely : lower efflux liability.\n"
"⚙️ Reduce liability: lower MW, HBA/HBD, polar surface.\n"
"⚠️ Critical for CNS drugs — P-gp is highly expressed at BBB."
),
"adme_cyp": (
"Predicted CYP enzyme inhibition (drug metabolism).\n\n"
"📖 Possible : may inhibit this isoform → DDI risk.\n"
" Unlikely : lower inhibition risk.\n"
" CYP3A4 metabolizes ~50% of all drugs.\n"
"⚙️ Reduce risk: avoid basic N + aromatic ring combos.\n"
"⚠️ SMARTS flags have ~40% false positive rate.\n"
" Use as a signal to investigate, not a definitive result."
),
"adme_pains": (
"Pan-Assay INterference compound alerts.\n\n"
"📖 0 alerts : clean scaffold.\n"
" 1+ alerts: may give false positives in biochemical screens.\n"
" Common issues: aggregation, fluorescence, redox cycling.\n"
"⚙️ Avoid PAINS substructures in hit-to-lead optimization.\n"
"⚠️ PAINS ≠ inactive. It means confirmation assays are needed.\n"
" Some approved drugs contain PAINS-flagged substructures."
),
"adme_brenk": (
"Structural alerts for reactive or unstable substructures.\n\n"
"📖 0 alerts : no reactive groups.\n"
" 1+ alerts: stability issues or metabolic liabilities.\n"
"⚙️ Review each alert individually.\n"
" Michael acceptors may be intentional for covalent inhibitors.\n"
"⚠️ BRENK alerts vary in severity — not all are disqualifying.\n"
" Prioritize by relevance to your assay conditions."
),
# ---- Batch (5) ---------------------------------------------------
"b_input_mode": (
"How to provide multiple ligands for batch docking.\n\n"
"📖 SMILES list : type directly, one per line.\n"
" Upload .smi : file with one SMILES [name] per line.\n"
" Structure files: .sdf/.mol2/.pdb files.\n"
"⚙️ SMILES fastest for known compounds.\n"
" .smi upload for large libraries (100+).\n"
"⚠️ All ligands share the same receptor, box, and parameters."
),
"b_smiles_text": (
"One ligand per line: SMILES [optional_name].\n\n"
"📖 Format example:\n"
" CCO Ethanol\n"
" c1ccccc1 Benzene\n"
" # this line is a comment\n"
"⚙️ Names: short, no spaces (use underscores).\n"
" No name = auto-numbered (lig_1, lig_2…).\n"
"⚠️ Very large molecules (> 100 heavy atoms) may fail.\n"
" Test problematic compounds individually."
),
"b_exh": (
"Search thoroughness per ligand (same as single-ligand mode).\n\n"
"📖 Lower = faster but potentially less accurate.\n"
"⚙️ 4–8 : initial screening of large libraries.\n"
" 16 : focused follow-up on promising hits.\n"
" 32 : final validation.\n"
"⚠️ Total time = N ligands × time per ligand.\n"
" 20 ligands × exh 16 ≈ 20–60 min total."
),
"b_nm": (
"Max binding poses per ligand in batch mode.\n\n"
"📖 Pose 1 affinity = used for ranking across the batch.\n"
" Extra poses available for inspecting selected hits.\n"
"⚙️ 5–9 : standard for batch screening.\n"
"⚠️ More poses → larger files, more memory.\n"
" For 50+ ligands, keep poses ≤ 9."
),
"b_do_redock": (
"Dock the reference co-crystal ligand as a validation control.\n\n"
"📖 Reference score = dashed red line in the batch score plot.\n"
" Ligands better than reference → prioritize for follow-up.\n"
"⚙️ Enable for any serious batch campaign.\n"
"⚠️ Adds one full docking run to total time.\n"
" Uses the same exhaustiveness as the batch."
),
# ---- Figure (5) --------------------------------------------------
"rtf_src": (
"Source for the 2D ligand-receptor interaction diagram.\n\n"
"📖 RDKit : fast, local, no network required.\n"
" PoseView v1 : proteins.plus REST API (server-side render).\n"
" PoseView2 : reference mode using PDB co-crystal ligand ID.\n"
"⚙️ Use RDKit for offline work or rapid iteration.\n"
" Use PoseView for publication-quality diagrams.\n"
"⚠️ PoseView requires internet access and may be slow."
),
"rtf_layout": (
"Layout of the multi-panel figure.\n\n"
"📖 2-panel : 3D view + 2D diagram side by side.\n"
" 4-panel : 3D view + 2D + ADME radar + score table.\n"
"⚙️ 2-panel for presentations. 4-panel for full analysis.\n"
"⚠️ 4-panel requires ADME to be calculated first."
),
"rtf_cutoff": (
"Distance cutoff for including residues in the figure diagram.\n\n"
"📖 Same meaning as the 2D diagram cutoff in Results section.\n"
"⚙️ 4.0–4.5 Å for most targets.\n"
"⚠️ Larger cutoff = more residues = slower rendering."
),
"rtf_labels": (
"Show residue labels in the figure's 3D panel.\n\n"
"📖 Labels appear as overlaid text on the protein in the PNG.\n"
"⚙️ Keep ON for analysis figures, OFF for presentation slides.\n"
"⚠️ Dense pockets with many residues produce overlapping labels."
),
"rtf_surf": (
"Render protein surface in the figure's 3D panel.\n\n"
"📖 SES surface shows the cavity shape around the ligand.\n"
"⚙️ Enable for shape complementarity visualization.\n"
"⚠️ Surface rendering adds GPU load — may be slow for large proteins."
),
}
# ---------------------------------------------------------------------------
# Utility helpers
# ---------------------------------------------------------------------------
def _h(key: str) -> str:
"""Return help string for a widget key, or empty string if not defined."""
return _H.get(key, "")
def _smiles_img_html(img_url: str, width: int = 160) -> str:
return (
f'<img src="{img_url}" width="{width}" '
f'style="border-radius:8px;border:1px solid #dde;background:white;" />'
)
def _affinity_color(aff: float) -> str:
if aff <= -9.0:
return "#1a7f37"
if aff <= -7.0:
return "#9a6700"
return "#cf222e"
def _lipinski_badge(pass_: bool) -> str:
color = "#1a7f37" if pass_ else "#cf222e"
label = "Pass" if pass_ else "Fail"
return (
f'<span style="background:{color};color:white;'
f'font-size:11px;font-weight:700;padding:2px 9px;'
f'border-radius:10px;">{label}</span>'
)
def _rule_badge(pass_: bool, label_pass: str = "Pass", label_fail: str = "Fail") -> str:
color = "#1a7f37" if pass_ else "#cf222e"
return (
f'<span style="background:{color};color:white;'
f'font-size:11px;font-weight:700;padding:2px 9px;'
f'border-radius:10px;">{"✓ " + label_pass if pass_ else "✗ " + label_fail}</span>'
)
# ---------------------------------------------------------------------------
# PubChem compound search (UI wrapper)
# ---------------------------------------------------------------------------
def _pubchem_search_widget(prefix: str = ""):
"""Inline PubChem search bar that auto-fills SMILES and name."""
key_q = prefix + "pub_search_query"
key_res = prefix + "pub_search_result"
if key_q not in st.session_state:
st.session_state[key_q] = ""
if key_res not in st.session_state:
st.session_state[key_res] = {}
with st.expander("🔍 Search compound by name (PubChem)", expanded=False):
col_a, col_b = st.columns([4, 1])
with col_a:
q = st.text_input(
"Compound name", value=st.session_state[key_q],
placeholder="e.g. erlotinib, ibuprofen",
key=key_q + "_inp",
label_visibility="collapsed",
)
with col_b:
search_clicked = st.button("Search", key=prefix + "btn_pubchem")
if search_clicked and q.strip():
with st.spinner("Searching PubChem…"):
result = search_pubchem(q.strip())
st.session_state[key_res] = result
st.session_state[key_q] = q.strip()
res = st.session_state.get(key_res, {})
if res.get("found"):
c1, c2 = st.columns([1, 3])
with c1:
if res.get("img_url"):
st.markdown(_smiles_img_html(res["img_url"], 120), unsafe_allow_html=True)
with c2:
st.markdown(f"**{res.get('iupac', '')}**")
st.caption(
f"Formula: {res.get('formula','')} "
f"MW: {res.get('mw',0):.1f} Da "
f"CID: {res.get('cid','')}"
)
if st.button("✓ Use this compound", key=prefix + "btn_use_pubchem"):
st.session_state[prefix + "smiles_in"] = res["smiles"]
st.session_state[prefix + "lig_name_in"] = (
res.get("iupac", "ligand").replace(" ", "_")[:20]
)
st.rerun()
elif res and not res.get("found"):
st.warning(res.get("error", "Not found."))
# ---------------------------------------------------------------------------
# RCSB protein search (UI wrapper)
# ---------------------------------------------------------------------------
def _rcsb_search_widget(prefix: str = ""):
key_q = prefix + "rec_search_query"
key_res = prefix + "rec_search_results"
key_sel = prefix + "rec_search_sel"
if key_q not in st.session_state: st.session_state[key_q] = ""
if key_res not in st.session_state: st.session_state[key_res] = []
if key_sel not in st.session_state: st.session_state[key_sel] = 0
with st.expander("🔍 Search RCSB by protein name", expanded=False):
col_a, col_b = st.columns([4, 1])
with col_a:
q = st.text_input(
"Protein name / keyword",
value=st.session_state[key_q],
placeholder="e.g. EGFR kinase, CDK2",
key=key_q + "_inp",
label_visibility="collapsed",
)
with col_b:
searched = st.button("Search", key=prefix + "btn_rcsb")
if searched and q.strip():
with st.spinner("Searching RCSB…"):
results = search_rcsb(q.strip(), top_n=25)
st.session_state[key_res] = results
st.session_state[key_q] = q.strip()
st.session_state[key_sel] = 0
results = st.session_state.get(key_res, [])
if not results:
return
st.caption(f"{len(results)} results — sorted by resolution")
_labels = []
for r in results:
res_s = f"{r['resolution']:.2f} Å" if r.get("resolution") else "—"
miss = "" if r.get("no_missing_residues") else " ⚠"
method = f" [{r.get('method','')[:4]}]" if r.get("method") else ""
_labels.append(
f"{r['pdb_id']} {res_s}{method}{miss} {r.get('title','')[:50]}"
)
sel_idx = st.radio(
"Select a structure",
options=range(len(_labels)),
format_func=lambda i: _labels[i],
key=key_sel + "_radio",
label_visibility="collapsed",
)
_picked = results[sel_idx]
# Row: Use PDB button + View on RCSB link
_btn_col, _link_col = st.columns([2, 1.2])
with _btn_col:
if st.button(
"✓ Use selected PDB ID",
key=prefix + "use_selected_pdb",
type="primary",
help=_h("pdb_id"),
):
st.session_state[prefix + "pdb_id"] = _picked["pdb_id"]
st.session_state[prefix + "pdb_token"] = _picked["pdb_id"]
st.rerun()
with _link_col:
_rcsb_url = f"https://www.rcsb.org/structure/{_picked['pdb_id']}"
st.markdown(
f'<a href="{_rcsb_url}" target="_blank" style="'
f'display:inline-flex;align-items:center;gap:6px;'
f'padding:7px 16px;border-radius:6px;'
f'background:#F6F8FA;border:1px solid #D0D7DE;'
f'color:#24292F;text-decoration:none;'
f'font-size:0.85rem;white-space:nowrap;">'
f'🔗 View on RCSB</a>',
unsafe_allow_html=True,
)
# Info
st.caption(
f"Resolution: {_picked.get('resolution', '—')} Å | "
f"Method: {_picked.get('method', '—')} | "
f"Protein: {_picked.get('protein_name', '—')[:60]}"
)
# ---------------------------------------------------------------------------
# RECEPTOR SECTION
# ---------------------------------------------------------------------------
def _receptor_section(prefix: str = ""):
st.subheader("Step 1 — Receptor")
# Structure source
src = st.radio(
"Structure source",
["Download PDB", "Upload file"],
key=prefix + "src_mode",
horizontal=True,
help=_h("src_mode"),
)
if src == "Download PDB":
_rcsb_search_widget(prefix)
c1, c2 = st.columns([2, 1])
with c1:
pdb_id = st.text_input(
"PDB ID",
value=st.session_state.get(prefix + "pdb_id", ""),
placeholder="e.g. 1M17",
key=prefix + "pdb_id",
help=_h("pdb_id"),
).strip().upper()
with c2:
fmt = st.radio(
"Format",
["CIF", "PDB"],
key=prefix + "rcsb_fmt",
horizontal=True,
help=_h("rcsb_fmt"),
)
else:
uploaded_rec = st.file_uploader(
"Upload receptor (.pdb or .cif)",
type=["pdb", "cif"],
key=prefix + "rec_upload",
)
# Grid center
center_mode = st.radio(
"Grid center",
["Auto-detect co-crystal ligand", "Manual XYZ", "ProDy selection"],
key=prefix + "center_mode",
horizontal=True,
help=_h("center_mode"),
)
if center_mode == "Manual XYZ":
c1, c2, c3 = st.columns(3)
with c1:
st.number_input("X", value=st.session_state.get(prefix+"cx", 0.0),
key=prefix+"cx", step=0.5, format="%.2f")
with c2:
st.number_input("Y", value=st.session_state.get(prefix+"cy", 0.0),
key=prefix+"cy", step=0.5, format="%.2f")
with c3:
st.number_input("Z", value=st.session_state.get(prefix+"cz", 0.0),
key=prefix+"cz", step=0.5, format="%.2f")
elif center_mode == "ProDy selection":
st.text_input(
"ProDy selection string",
placeholder="e.g. resid 702 and chain A",
key=prefix + "mda_sel",
help=_h("mda_sel"),
)
# Box size
with st.expander("🔲 Box size (Angstroms)", expanded=False):
c1, c2, c3 = st.columns(3)
with c1:
st.slider("X", 10, 40, st.session_state.get(prefix+"sx", 16), 2,
key=prefix+"sx", help=_h("box_size"))
with c2:
st.slider("Y", 10, 40, st.session_state.get(prefix+"sy", 16), 2,
key=prefix+"sy", help=_h("box_size"))
with c3:
st.slider("Z", 10, 40, st.session_state.get(prefix+"sz", 16), 2,
key=prefix+"sz", help=_h("box_size"))
sx = st.session_state.get(prefix+"sx", 16)
sy = st.session_state.get(prefix+"sy", 16)
sz = st.session_state.get(prefix+"sz", 16)
st.caption(f"Volume ≈ {sx*sy*sz:,} ų")
# Advanced options
with st.expander("⚙️ Advanced receptor options", expanded=False):
st.checkbox("Blind docking (expand box to whole protein)",
key=prefix+"blind_docking", help=_h("blind_docking"))
st.checkbox("Keep cofactors (ATP, FAD, heme…)",
key=prefix+"keep_cofactors", help=_h("keep_cofactors"))
st.checkbox("Keep metal ions (Zn, Mg, Ca…)",
key=prefix+"keep_metals", help=_h("keep_metals"))
st.checkbox("Prefer structures with no missing residues",
key=prefix+"rcsb_prefer_complete", help=_h("rcsb_prefer_complete"))
# Prepare button
if st.button("▶ Prepare Receptor", key=prefix+"btn_prep_rec", type="primary"):
with st.spinner("Preparing receptor…"):
_do_prepare_receptor(prefix)
# Status
if st.session_state.get(prefix+"rec_prepared"):
info = st.session_state.get(prefix+"rec_info", {})
st.success(
f"✓ Receptor ready | {info.get('n_atoms', '?')} atoms | "
f"Center: ({info.get('cx',0):.2f}, {info.get('cy',0):.2f}, {info.get('cz',0):.2f}) | "
f"Box: {info.get('sx',16)}×{info.get('sy',16)}×{info.get('sz',16)} Å"
)
if info.get("cocrystal_ligand_id"):
st.info(f"Co-crystal ligand detected: **{info['cocrystal_ligand_id']}**")
def _do_prepare_receptor(prefix: str = ""):
"""Run receptor preparation and store result in session state."""
wdir = _wdir()
info = {}
try:
src_mode = st.session_state.get(prefix+"src_mode", "Download PDB")
if src_mode == "Download PDB":
pdb_id = st.session_state.get(prefix+"pdb_id", "").strip().upper()