naagrh Cursor commited on
Commit
b8a800e
·
0 Parent(s):

AmberMDFlow for HF Space (no binaries)

Browse files

Co-authored-by: Cursor <cursoragent@cursor.com>

.dockerignore ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Git
2
+ .git
3
+ .gitignore
4
+
5
+ # Python
6
+ __pycache__
7
+ *.pyc
8
+ *.pyo
9
+ *.pyd
10
+ .Python
11
+ *.so
12
+ *.egg
13
+ *.egg-info
14
+ dist
15
+ build
16
+ .pytest_cache
17
+ .coverage
18
+ htmlcov
19
+
20
+ # Virtual environments
21
+ venv/
22
+ env/
23
+ ENV/
24
+
25
+ # IDE
26
+ .vscode/
27
+ .idea/
28
+ *.swp
29
+ *.swo
30
+ *~
31
+
32
+ # OS
33
+ .DS_Store
34
+ Thumbs.db
35
+
36
+ # Output and temporary files
37
+ output/
38
+ temp/
39
+ *.log
40
+ *.tmp
41
+
42
+ # Documentation
43
+ *.md
44
+ !README.md
45
+
46
+ # Docker
47
+ Dockerfile
48
+ docker-compose.yml
49
+ .dockerignore
50
+
51
+ # Other
52
+ *.bak
53
+ *.backup
54
+
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ venv/
25
+ env/
26
+ ENV/
27
+
28
+ # IDE
29
+ .vscode/
30
+ .idea/
31
+ *.swp
32
+ *.swo
33
+
34
+ # OS
35
+ .DS_Store
36
+ Thumbs.db
37
+
38
+ # Project specific
39
+ output/
40
+ *.log
41
+ *.tmp
42
+ temp/
43
+
44
+ # PDB files (optional - remove if you want to include example PDBs)
45
+ *.pdb
46
+ *.ent
47
+
48
+ # AMBER files
49
+ *.prmtop
50
+ *.inpcrd
51
+ *.rst
52
+ *.rst7
53
+ *.ncrst
54
+ *.mdcrd
55
+ *.nc
56
+ *.dcd
57
+ *.xtc
58
+ *.trr
59
+
60
+ # Binaries (HF Spaces rejects; logo can be hosted elsewhere)
61
+ AmberMDFlow.png
Dockerfile ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ # Install system dependencies
4
+ RUN apt-get update && apt-get install -y \
5
+ wget \
6
+ curl \
7
+ build-essential \
8
+ gcc \
9
+ g++ \
10
+ make \
11
+ libffi-dev \
12
+ libssl-dev \
13
+ libglib2.0-0 \
14
+ libxext6 \
15
+ libsm6 \
16
+ libxrender1 \
17
+ libgomp1 \
18
+ && rm -rf /var/lib/apt/lists/*
19
+
20
+ # Install Miniforge (conda-forge–based; no Anaconda ToS) and configure channels
21
+ RUN wget -q https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh && \
22
+ bash Miniforge3-Linux-x86_64.sh -b -p /opt/conda && \
23
+ rm Miniforge3-Linux-x86_64.sh
24
+
25
+ ENV PATH="/opt/conda/bin:${PATH}"
26
+
27
+ # Add bioconda; conda-forge is default for Miniforge. No Anaconda ToS needed.
28
+ RUN conda config --add channels bioconda && \
29
+ conda config --set channel_priority flexible
30
+
31
+ # Install mamba for faster package installation
32
+ RUN conda install -n base -c conda-forge mamba -y
33
+
34
+ # Install AMBER tools, PyMOL, AutoDock Vina 1.1.2, Open Babel, RDKit, and gemmi (for Meeko)
35
+ RUN mamba install -y python=3.11 \
36
+ conda-forge::ambertools conda-forge::pymol-open-source \
37
+ bioconda::autodock-vina conda-forge::openbabel conda-forge::rdkit conda-forge::gemmi
38
+
39
+ # Clean up conda/mamba cache to reduce image size
40
+ RUN conda clean -afy
41
+
42
+ # Install Python packages via pip (only packages not provided by conda or that need pip)
43
+ # Note: numpy, pandas, matplotlib are already installed by conda; don't override to avoid conflicts
44
+ RUN pip install --no-cache-dir \
45
+ flask==2.3.3 \
46
+ flask-cors==4.0.0 \
47
+ biopython \
48
+ seaborn \
49
+ mdanalysis \
50
+ gunicorn==21.2.0 \
51
+ requests \
52
+ meeko>=0.7.0 \
53
+ prody \
54
+ "numpy<2.0"
55
+
56
+ # Set working directory
57
+ WORKDIR /AmberMDFlow
58
+
59
+ # Copy the entire project
60
+ COPY . .
61
+
62
+ # Create necessary directories with proper permissions
63
+ RUN mkdir -p /AmberMDFlow/obsolete /AmberMDFlow/pdb /AmberMDFlow/temp /AmberMDFlow/output && \
64
+ chmod -R 777 /AmberMDFlow
65
+
66
+ # Make sure the ambermdflow package is on the Python path
67
+ ENV PYTHONPATH="${PYTHONPATH}:/AmberMDFlow"
68
+
69
+ # Expose the port
70
+ EXPOSE 7860
71
+
72
+ # Run the application
73
+ CMD ["python", "start_web_server.py"]
74
+
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Hemant Nagar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
MANIFEST.in ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Include package data
2
+ recursive-include ambermdflow/html *
3
+ recursive-include ambermdflow/css *
4
+ recursive-include ambermdflow/js *
5
+
6
+ # Include documentation
7
+ include README.md
8
+ include LICENSE
9
+
10
+ # Exclude development/test files
11
+ exclude .gitignore
12
+ exclude .dockerignore
13
+ exclude Dockerfile
14
+ prune Test
15
+ prune __pycache__
16
+ global-exclude *.pyc
17
+ global-exclude *.pyo
18
+ global-exclude .DS_Store
README.md ADDED
@@ -0,0 +1,331 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: AmberMDFlow
3
+ sdk: docker
4
+ app_port: 7860
5
+ ---
6
+
7
+ # AmberMDFlow
8
+
9
+ **AmberMDFlow** is a web-based pipeline for preparing structures, setting up molecular dynamics (MD) simulations with the AMBER force field. It integrates structure completion (ESMFold), preparation, force field parameterization, simulation file generation, and PLUMED-based biased MD in a single interface. This is the beta version.
10
+
11
+ ---
12
+
13
+ ## Note
14
+ - If you plan to dock ligands and have filled missing residues in the protein chain using ESMFold, you should also energy-minimize the structure.
15
+ - The **Fill Missing Residues** option works only for PDB files retrieved from the RCSB database, as it relies on the REMARK 465 records to identify missing residues.
16
+ - The public ESMFold API is used to predict protein structures from input sequences, and it supports sequences of up to 400 amino acids.
17
+
18
+ ---
19
+
20
+ ## Features
21
+
22
+ | Section | Description |
23
+ |--------|-------------|
24
+ | **Protein Loading** | Upload PDB files or fetch from RCSB PDB; 3D visualization with NGL |
25
+ | **Fill Missing Residues** | Detect missing residues (RCSB annotations), complete with ESMFold, optional trimming and energy minimization of predicted structure|
26
+ | **Structure Preparation** | Remove water/ions/H; add ACE/NME capping; chain and ligand selection; GAFF/GAFF2 parameterization |
27
+ | **Ligand Docking** | AutoDock Vina + Meeko; configurable search box; pose selection and use selected ligand pose to setup MD simulations |
28
+ | **Simulation Parameters** | Force fields (ff14SB, ff19SB), water models (TIP3P, SPCE), box size, temperature, pressure |
29
+ | **Simulation Steps** | Restrained minimization, minimization, NVT, NPT, production — each with configurable parameters |
30
+ | **Generate Files** | AMBER `.in` files, `prmtop`/`inpcrd`, PBS submission scripts |
31
+ | **PLUMED** | Collective variables (PLUMED v2.9), `plumed.dat` editor, and simulation file generation with PLUMED |
32
+
33
+ ---
34
+
35
+ ## Requirements for Custom PDB Files
36
+
37
+ For **custom PDB files** (uploaded or fetched), ensure:
38
+
39
+ | Requirement | Description |
40
+ |-------------|-------------|
41
+ | **Chain IDs** | Chain IDs (A,B,C..) must be clearly marked in the PDB file. |
42
+ | **Ligands as HETATM** | All ligands must be in **HETATM** records with name and IDs marked (LIG and A). |
43
+ | **Standard amino acids** | AmberMDFlow supports **standard amino acids** only. Non-standard residues and residues with PTMs are currently not supported in the pipeline. |
44
+
45
+ For RCSB structures, the pipeline parses the header and HETATM as provided; for your own PDBs, apply the above conventions.
46
+
47
+ ---
48
+
49
+ ## Quick Start
50
+
51
+ Try AmberMDFlow instantly on Hugging Face Spaces (no installation required):
52
+
53
+ **[https://huggingface.co/spaces/hemantn/AmberMDFlow](https://huggingface.co/spaces/hemantn/AmberMDFlow)**
54
+
55
+ ---
56
+
57
+ ## Installation
58
+
59
+ ### Prerequisites
60
+
61
+ AmberMDFlow requires scientific packages that are only available via **conda** (not PyPI). You must install these first:
62
+
63
+ | Package | Purpose |
64
+ |---------|---------|
65
+ | `ambertools` | AMBER MD tools (tleap, antechamber, sander) |
66
+ | `pymol-open-source` | Structure visualization and editing |
67
+ | `autodock-vina` | AutoDock Vina 1.1.2 molecular docking (from bioconda) |
68
+ | `openbabel` | Molecule format conversion |
69
+ | `rdkit` | Cheminformatics toolkit |
70
+ | `gemmi` | Structure file parsing (required by Meeko) |
71
+
72
+ ---
73
+
74
+ ### Option 1: pip install (recommended)
75
+
76
+ ```bash
77
+ # Step 1: Create conda environment with required tools
78
+ conda create -n ambermdflow python=3.11 -y
79
+ conda activate ambermdflow
80
+
81
+ # Step 2: Install conda-only dependencies
82
+ conda install -c conda-forge -c bioconda ambertools pymol-open-source autodock-vina openbabel rdkit gemmi -y
83
+
84
+ # Step 3: Install AmberMDFlow from Test PyPI
85
+ pip install --extra-index-url https://test.pypi.org/simple/ ambermdflow
86
+
87
+ # Step 4: Run the web app
88
+ ambermdflow
89
+ ```
90
+
91
+ Open your browser at **http://localhost:7860**
92
+
93
+ ---
94
+
95
+ ### Option 2: Docker (no conda/pip needed)
96
+ **build from source:**
97
+ ```bash
98
+ git clone https://github.com/nagarh/AmberMDFlow.git
99
+ cd AmberMDFlow
100
+ docker build -t ambermdflow .
101
+ docker run -p 7860:7860 ambermdflow
102
+ ```
103
+
104
+ Open your browser at **http://localhost:7860**
105
+
106
+ ---
107
+
108
+ ### Troubleshooting
109
+
110
+ | Issue | Solution |
111
+ |-------|----------|
112
+ | `ModuleNotFoundError: No module named 'gemmi'` | Run: `conda install -c conda-forge gemmi` |
113
+ | `vina: command not found` | Run: `conda install -c conda-forge vina` |
114
+ | Port 7860 already in use | Kill the process or edit `start_web_server.py` to use a different port |
115
+
116
+
117
+ ---
118
+
119
+ ## Usage
120
+
121
+ ### 1. Protein Loading
122
+
123
+ - **Upload**: Drag-and-drop or choose a `.pdb` file.
124
+ - **Fetch**: Enter a 4-character PDB ID (e.g. `1HPV`) to download from RCSB.
125
+
126
+ After loading, the **Protein Preview** shows: structure ID, atom count, chains, residues, water, ions, ligands, and HETATM count. Use the 3D viewer to inspect the structure.
127
+
128
+ ---
129
+
130
+ ### 2. Fill Missing Residues
131
+
132
+ - Click **Analyze Missing Residues** to detect gaps from RCSB metadata.
133
+ - **Select chains** to complete with ESMFold.
134
+ - **Trim residues** (optional): remove residues from N- or C-terminal edges; internal loops are always filled by ESMFold.
135
+ - **Energy minimization** (optional): if you enable ESMFold completion, you can minimize selected chains to resolve clashes before docking. Recommended if receptor preparation (Meeko) fails later.
136
+ - **Build Completed Structure** to run ESMFold and (if requested) minimization. Use **Preview Completed Structure** and **View Superimposed Structures** to compare original and completed chains.
137
+
138
+ > If you use ESMFold in this workflow, please cite [ESM Atlas](https://esmatlas.com/about).
139
+
140
+ ---
141
+
142
+ ### 3. Structure Preparation
143
+
144
+ - **Remove**: Water, ions, and hydrogens (options are pre-configured).
145
+ - **Add capping**: ACE (N-terminal) and NME (C-terminal).
146
+ - **Chains**: Select which protein chains to keep for force field generation.
147
+ - **Ligands**:
148
+ - **Preserve ligands** to keep them in the structure.
149
+ - **Select ligands to preserve** (e.g. `GOL-A-1`, `LIZ-A`). Unselected ligands are dropped.
150
+ - **Create separate ligand file** to export selected ligand(s) to a PDB.
151
+ - **Protonate** ligand using Open Babel.
152
+
153
+ Click **Prepare Structure**. The status panel reports original vs prepared atom counts, removed components, added capping, and preserved ligands. Use **View Prepared Structure** and **Download Prepared PDB** as needed.
154
+
155
+ **Ligand Docking** (nested in this tab):
156
+
157
+ - Select ligands to dock.
158
+ - Set the **search space** (center and size in X, Y, Z) with live 3D visualization.
159
+ - **Run Docking** (AutoDock Vina + Meeko). Progress and logs are shown in the docking panel.
160
+ - **Select poses** per ligand and **Use selected pose** to write the chosen pose into the structure for AMBER. You can switch modes (e.g. 1–9) and jump by clicking the mode labels.
161
+
162
+ ---
163
+
164
+ ### 4. Simulation Parameters
165
+
166
+ - **Force field**: ff14SB or ff19SB.
167
+ - **Water model**: TIP3P or SPCE.
168
+ - **Box size** (Å): padding for solvation.
169
+ - **Add ions**: to neutralize (and optionally reach a salt concentration).
170
+ - **Temperature** and **Pressure** (e.g. 300 K, 1 bar).
171
+ - **Time step** and **Cutoff** for non-bonded interactions.
172
+
173
+ If ligands were preserved, **Ligand force field** (GAFF/GAFF2) is configured here; net charge is computed before `antechamber` runs.
174
+
175
+ ---
176
+
177
+ ### 5. Simulation Steps
178
+
179
+ Enable/disable and set parameters for:
180
+
181
+ - **Restrained minimization** (steps, force constant)
182
+ - **Minimization** (steps, cutoff)
183
+ - **NVT heating** (steps, temperature)
184
+ - **NPT equilibration** (steps, temperature, pressure)
185
+ - **Production** (steps, temperature, pressure)
186
+
187
+ ---
188
+
189
+ ### 6. Generate Files
190
+
191
+ - **Generate All Files** to create AMBER inputs (`min_restrained.in`, `min.in`, `HeatNPT.in`, `mdin_equi.in`, `mdin_prod.in`), `tleap` scripts, `submit_job.pbs`, and (after `tleap`) `prmtop`/`inpcrd`.
192
+ - **Preview Files** to open and **edit** each file (e.g. `min.in`, `submit_job.pbs`) and **Save**; changes are written to the output directory.
193
+ - **Preview Solvated Protein** / **Download Solvated Protein** to inspect and download the solvated system.
194
+
195
+ For **PLUMED-based runs**, go to the **PLUMED** tab to configure CVs and `plumed.dat`, then use **Generate simulation files** there to produce inputs that include PLUMED.
196
+
197
+ ---
198
+
199
+ ### 7. PLUMED
200
+
201
+ - **Collective Variables**: search and select CVs from the PLUMED v2.9 set; view docs and add/edit lines in `plumed.dat`.
202
+ - **Custom PLUMED**: edit `plumed.dat` directly.
203
+ - **Generate simulation files**: create AMBER + PLUMED input files. Generated files can be **previewed, edited, and saved** as in the main **Generate Files** tab.
204
+
205
+ > PLUMED citation: [plumed.org/cite](https://www.plumed.org/cite).
206
+
207
+ ---
208
+
209
+ ## Pipeline Overview
210
+
211
+ ```
212
+ Protein Loading (upload/fetch)
213
+
214
+ Fill Missing Residues (detect → ESMFold → optional trim & minimize)
215
+
216
+ Structure Preparation (clean, cap, chains, ligands) → optional Docking (Vina, select pose)
217
+
218
+ Simulation Parameters (FF, water, box, T, P, etc.)
219
+
220
+ Simulation Steps (min, NVT, NPT, prod)
221
+
222
+ Generate Files (AMBER .in, tleap, prmtop/inpcrd, PBS)
223
+
224
+ [Optional] PLUMED (CVs, plumed.dat, generate PLUMED-enabled files)
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Output Layout
230
+
231
+ Generated files are written under `output/` (or the path set in the app), for example:
232
+
233
+ - `0_original_input.pdb` — raw input
234
+ - `1_protein_no_hydrogens.pdb` — cleaned, capped, chain/ligand selection applied
235
+ - `2_protein_with_caps.pdb`, `tleap_ready.pdb` — intermediates
236
+ - `4_ligands_corrected_*.pdb` — prepared ligands
237
+ - `protein.prmtop`, `protein.inpcrd` — after `tleap`
238
+ - `min_restrained.in`, `min.in`, `HeatNPT.in`, `mdin_equi.in`, `mdin_prod.in`, `submit_job.pbs`
239
+ - `output/docking/` — receptor, ligands, Vina configs, poses, logs
240
+ - `plumed.dat` — when using PLUMED
241
+
242
+ ---
243
+
244
+ ## Multi-user deployment (e.g. Hugging Face Spaces)
245
+
246
+ When multiple users use the app at the same time (e.g. on Hugging Face Spaces), each user gets an **isolated output folder** so one user’s files are not overwritten by another’s. The app assigns a session ID when the page loads; all API requests send this ID and generated files are stored under `output/<session_id>/`. No configuration is required—this works automatically in multi-user and single-user setups.
247
+
248
+ ---
249
+
250
+ ## Dependencies
251
+
252
+ | Category | Tools / libraries |
253
+ |----------|-------------------|
254
+ | **Python** | Flask, Flask-CORS, BioPython, NumPy, Pandas, Matplotlib, Seaborn, MDAnalysis, Requests, RDKit, SciPy |
255
+ | **AMBER** | AMBER Tools (tleap, antechamber, sander, ambpdb, etc.) |
256
+ | **Docking** | Meeko (`mk_prepare_ligand`, `mk_prepare_receptor`), AutoDock Vina, Open Babel |
257
+ | **Visualization** | PyMOL (scripted for H removal, structure editing), NGL (in-browser 3D) |
258
+ | **Structure completion** | ESMFold (via API or local, depending on deployment) |
259
+
260
+ ---
261
+
262
+ ## Project Structure
263
+
264
+ ```
265
+ AmberMDFlow/
266
+ ├── start_web_server.py # Entry point
267
+ ├── html/
268
+ │ ├── index.html # Main UI
269
+ │ └── plumed.html # PLUMED-focused view (if used)
270
+ ├── css/
271
+ │ ├── styles.css
272
+ │ └── plumed.css
273
+ ├── js/
274
+ │ ├── script.js # Main frontend logic
275
+ │ ├── plumed.js # PLUMED + docking UI
276
+ │ └── plumed_cv_docs.js # CV documentation
277
+ ├── python/
278
+ │ ├── app.py # Flask backend, API, file generation
279
+ │ ├── structure_preparation.py
280
+ │ ├── add_caps.py # ACE/NME capping
281
+ │ ├── Fill_missing_residues.py # ESMFold, trimming, minimization
282
+ │ ├── docking.py # Docking helpers
283
+ │ └── docking_utils.py
284
+ ├── output/ # Generated files (gitignored in dev)
285
+ ├── Dockerfile
286
+ └── README.md
287
+ ```
288
+
289
+ ---
290
+
291
+ ## Citation
292
+
293
+ If you use AmberMDFlow in your work, please cite:
294
+
295
+ ```bibtex
296
+ @software{AmberMDFlow,
297
+ title = {AmberMDFlow: Molecular Dynamics and Docking Pipeline},
298
+ author = {Nagar, Hemant},
299
+ year = {2025},
300
+ url = {https://github.com/nagarh/AmberMDFlow}
301
+ ```
302
+
303
+ **Related software to cite when used:**
304
+
305
+ - **AMBER**: [ambermd.org](https://ambermd.org)
306
+ - **PLUMED**: [plumed.org/cite](https://www.plumed.org/cite)
307
+ - **ESMFold / ESM Atlas**: [esmatlas.com/about](https://esmatlas.com/about)
308
+ - **AutoDock Vina**: [autodock-vina/cite](https://autodock-vina.readthedocs.io/en/latest/citations.html)
309
+ - **Meeko**: [github.com/forlilab/Meeko](https://github.com/forlilab/Meeko)
310
+ - **MDAnalysis**: [mdanalysis/cite](https://www.mdanalysis.org/pages/citations/)
311
+ - **NGL Viewer**: [nglviewer/cite](https://doi.org/10.1093/bioinformatics/bty419)
312
+ - **PyMOL**: [pymol/cite](https://www.pymol.org/support.html)
313
+
314
+ ---
315
+
316
+ ## Acknowledgments
317
+
318
+ - **Mohd Ibrahim** (Technical University of Munich) for the protein capping logic (`add_caps.py`).
319
+
320
+ ---
321
+
322
+ ## License
323
+
324
+ MIT License. See `LICENSE` for details.
325
+
326
+ ---
327
+
328
+ ## Contact
329
+
330
+ - **Author**: Hemant Nagar
331
+ - **Email**: hn533621@ohio.edu
ambermdflow/Fill_missing_residues.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from collections import defaultdict
3
+ from textwrap import wrap
4
+ import re
5
+
6
+
7
+ def get_pdb_id_from_pdb_file(pdb_path):
8
+ """
9
+ Extract the 4-character PDB ID from a PDB file.
10
+
11
+ By convention, PDB files have a line starting with 'HEADER' where
12
+ columns 63–66 contain the PDB ID code.
13
+
14
+ If that cannot be found, this function will raise a ValueError so
15
+ that the pipeline fails loudly instead of silently doing the wrong thing.
16
+ """
17
+ with open(pdb_path, "r") as fh:
18
+ for line in fh:
19
+ if line.startswith("HEADER") and len(line) >= 66:
20
+ pdb_id = line[62:66].strip()
21
+ if pdb_id:
22
+ return pdb_id.upper()
23
+
24
+ raise ValueError(
25
+ f"Could not determine PDB ID from file: {pdb_path}. "
26
+ "Expected a 'HEADER' record with ID in columns 63–66."
27
+ )
28
+
29
+ GRAPHQL_URL = "https://data.rcsb.org/graphql"
30
+
31
+ def detect_missing_residues(pdb_id):
32
+ url = f"https://files.rcsb.org/download/{pdb_id}.pdb"
33
+ response = requests.get(url)
34
+ response.raise_for_status()
35
+
36
+ missing_by_chain = defaultdict(list)
37
+
38
+ for line in response.text.splitlines():
39
+ if line.startswith("REMARK 465"):
40
+ parts = line.split()
41
+ if len(parts) >= 5 and parts[2].isalpha():
42
+ resname = parts[2]
43
+ chain = parts[3]
44
+
45
+ # Extract residue number (strip insertion code, handle negative numbers)
46
+ match = re.match(r"(-?\d+)", parts[4])
47
+ if match:
48
+ resnum = int(match.group(1))
49
+ missing_by_chain[chain].append((resname, resnum))
50
+
51
+ return dict(missing_by_chain)
52
+
53
+ def get_chain_sequences(pdb_id):
54
+ query = """
55
+ query ChainSequences($pdb_id: String!) {
56
+ entry(entry_id: $pdb_id) {
57
+ polymer_entities {
58
+ entity_poly {
59
+ pdbx_seq_one_letter_code_can
60
+ }
61
+ polymer_entity_instances {
62
+ rcsb_polymer_entity_instance_container_identifiers {
63
+ auth_asym_id
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ """
70
+
71
+ r = requests.post(
72
+ GRAPHQL_URL,
73
+ json={"query": query, "variables": {"pdb_id": pdb_id}}
74
+ )
75
+ r.raise_for_status()
76
+
77
+ chain_seqs = {}
78
+
79
+ for entity in r.json()["data"]["entry"]["polymer_entities"]:
80
+ seq = entity["entity_poly"]["pdbx_seq_one_letter_code_can"]
81
+ for inst in entity["polymer_entity_instances"]:
82
+ chain = inst[
83
+ "rcsb_polymer_entity_instance_container_identifiers"
84
+ ]["auth_asym_id"]
85
+ chain_seqs[chain] = seq
86
+
87
+ return chain_seqs
88
+
89
+ def trim_residues_from_edges(sequence, n_terminal_trim=0, c_terminal_trim=0):
90
+ """
91
+ Trim residues from the edges (N-terminal and C-terminal) of a sequence.
92
+ Only trims from the edges, not from loops in between.
93
+
94
+ Args:
95
+ sequence: str
96
+ The amino acid sequence to trim
97
+ n_terminal_trim: int
98
+ Number of residues to remove from the N-terminal (start)
99
+ c_terminal_trim: int
100
+ Number of residues to remove from the C-terminal (end)
101
+
102
+ Returns:
103
+ str: The trimmed sequence
104
+
105
+ Raises:
106
+ ValueError: If trim counts exceed sequence length or are negative
107
+ """
108
+ if n_terminal_trim < 0 or c_terminal_trim < 0:
109
+ raise ValueError("Trim counts must be non-negative")
110
+
111
+ if n_terminal_trim + c_terminal_trim >= len(sequence):
112
+ raise ValueError(
113
+ f"Total trim count ({n_terminal_trim + c_terminal_trim}) exceeds sequence length ({len(sequence)})"
114
+ )
115
+
116
+ # Trim from N-terminal (start) and C-terminal (end)
117
+ trimmed = sequence[n_terminal_trim:len(sequence) - c_terminal_trim]
118
+
119
+ return trimmed
120
+
121
+
122
+ def trim_chains_sequences(chains_with_sequences, trim_specs):
123
+ """
124
+ Apply trimming to multiple chain sequences based on specifications.
125
+
126
+ Args:
127
+ chains_with_sequences: dict
128
+ Dictionary mapping chain IDs to sequences
129
+ Example: {'A': 'MKTAYIAKQR...', 'B': 'MKTAYIAKQR...'}
130
+ trim_specs: dict
131
+ Dictionary mapping chain IDs to trim specifications
132
+ Each specification is a dict with 'n_terminal' and/or 'c_terminal' keys
133
+ Example: {'A': {'n_terminal': 5, 'c_terminal': 3}, 'B': {'n_terminal': 2}}
134
+
135
+ Returns:
136
+ dict: Dictionary mapping chain IDs to trimmed sequences
137
+ """
138
+ trimmed_chains = {}
139
+
140
+ for chain, sequence in chains_with_sequences.items():
141
+ if chain in trim_specs:
142
+ spec = trim_specs[chain]
143
+ n_term = spec.get('n_terminal', 0)
144
+ c_term = spec.get('c_terminal', 0)
145
+
146
+ try:
147
+ trimmed_seq = trim_residues_from_edges(sequence, n_term, c_term)
148
+ trimmed_chains[chain] = trimmed_seq
149
+ except ValueError as e:
150
+ raise ValueError(f"Error trimming chain {chain}: {str(e)}")
151
+ else:
152
+ # No trimming specified for this chain, keep original
153
+ trimmed_chains[chain] = sequence
154
+
155
+ return trimmed_chains
156
+
157
+
158
+ def write_fasta_for_missing_chains(pdb_id, chains_with_missing, output_dir=None):
159
+ """
160
+ Write FASTA file for chains with missing residues.
161
+
162
+ Args:
163
+ pdb_id: PDB identifier
164
+ chains_with_missing: Dictionary mapping chain IDs to sequences
165
+ output_dir: Optional output directory. If None, writes to current directory.
166
+ """
167
+ filename = f"{pdb_id}_chains_with_missing.fasta"
168
+
169
+ if output_dir:
170
+ from pathlib import Path
171
+ output_path = Path(output_dir) / filename
172
+ else:
173
+ output_path = filename
174
+
175
+ with open(output_path, "w") as f:
176
+ for chain, seq in chains_with_missing.items():
177
+ f.write(f">{pdb_id.upper()}_{chain}\n")
178
+ for line in wrap(seq, 60):
179
+ f.write(line + "\n")
180
+
181
+ print(f"Wrote FASTA: {output_path}")
182
+
183
+ def run_esmfold(sequence):
184
+ response = requests.post(
185
+ "https://api.esmatlas.com/foldSequence/v1/pdb/",
186
+ data=sequence,
187
+ timeout=300
188
+ )
189
+ response.raise_for_status()
190
+ return response.text
191
+
192
+
193
+ def merge_non_protein_atoms(original_pdb_path, protein_pdb_path, output_pdb_path, chains_to_replace):
194
+ """
195
+ Add non-protein atoms (water, ions, ligands) from original file to the completed protein structure.
196
+
197
+ Parameters:
198
+ -----------
199
+ original_pdb_path : str
200
+ Path to the original PDB file
201
+ protein_pdb_path : str
202
+ Path to the temporary protein-only PDB file
203
+ output_pdb_path : str
204
+ Path where the final merged PDB will be written
205
+ chains_to_replace : list[str]
206
+ List of chain IDs that were replaced by ESMFold (not used, kept for compatibility)
207
+ """
208
+ import os
209
+
210
+ # Extract non-protein atoms (HETATM records) from original PDB
211
+ non_protein_atoms = []
212
+
213
+ if not os.path.exists(original_pdb_path):
214
+ print(f"Warning: Original PDB file not found: {original_pdb_path}")
215
+ # Just copy the protein file if original doesn't exist
216
+ if os.path.exists(protein_pdb_path):
217
+ import shutil
218
+ shutil.copy2(protein_pdb_path, output_pdb_path)
219
+ return
220
+
221
+ # Read HETATM records from original PDB
222
+ with open(original_pdb_path, 'r') as f:
223
+ for line in f:
224
+ if line.startswith('HETATM'):
225
+ # Include all HETATM records (water, ions, ligands)
226
+ non_protein_atoms.append(line)
227
+
228
+ # Read the completed protein structure
229
+ if not os.path.exists(protein_pdb_path):
230
+ print(f"Error: Protein PDB file not found: {protein_pdb_path}")
231
+ return
232
+
233
+ # Write merged PDB file: protein structure + non-protein atoms
234
+ with open(output_pdb_path, 'w') as f:
235
+ # Write the completed protein structure (all lines except END)
236
+ with open(protein_pdb_path, 'r') as protein_file:
237
+ for line in protein_file:
238
+ if not line.startswith('END'):
239
+ f.write(line)
240
+
241
+ # Add non-protein atoms (water, ions, ligands) from original
242
+ for line in non_protein_atoms:
243
+ f.write(line)
244
+
245
+ # Write END record at the very end
246
+ f.write("END \n")
247
+
248
+ print(f"✅ Added {len(non_protein_atoms)} non-protein atoms to completed structure")
249
+
250
+
251
+ def rebuild_pdb_with_esmfold(
252
+ pdb_id,
253
+ chains_to_replace,
254
+ output_pdb=None,
255
+ original_pdb_path=None,
256
+ chains_use_minimized=None,
257
+ ):
258
+ """
259
+ pdb_id: str
260
+ Original crystal structure object name (e.g. '3hhr')
261
+
262
+ chains_to_replace: list[str]
263
+ Chains that were missing residues and replaced by ESMFold
264
+ Example: ['A', 'B', 'C']
265
+
266
+ output_pdb: str, optional
267
+ Output PDB filename.
268
+
269
+ original_pdb_path: str, optional
270
+ Path to the original PDB file that should be loaded into PyMOL
271
+ as the reference object named `pdb_id`. If None, defaults to
272
+ '../../output/0_original_input.pdb'.
273
+
274
+ chains_use_minimized: list[str], optional
275
+ For these chains, load the superimposed minimized PDB
276
+ ({pdb_id}_chain_{c}_esmfold_minimized_noH.pdb) instead of the
277
+ ESMFold PDB. The minimized structure is aligned to the original
278
+ the same way as ESMFold (CA-based superimposition).
279
+ """
280
+
281
+ from pymol import cmd
282
+
283
+ # -----------------------------
284
+ # 0. Clean up any existing objects with the same names
285
+ # -----------------------------
286
+ try:
287
+ # Delete existing objects if they exist
288
+ existing_objects = cmd.get_object_list()
289
+ if pdb_id in existing_objects:
290
+ cmd.delete(pdb_id)
291
+
292
+ # Delete any existing ESMFold objects for the chains we're processing
293
+ for chain in chains_to_replace:
294
+ esm_obj = f"{pdb_id}_chain_{chain}_esmfold"
295
+ if esm_obj in existing_objects:
296
+ cmd.delete(esm_obj)
297
+
298
+ # Delete final_model if it exists
299
+ if "final_model" in existing_objects:
300
+ cmd.delete("final_model")
301
+ except Exception as e:
302
+ print(f"Warning: Could not clean up existing objects: {e}")
303
+
304
+ # -----------------------------
305
+ # 1. Load original PDB into PyMOL
306
+ # -----------------------------
307
+ if original_pdb_path is None:
308
+ # Default to the pipeline output location
309
+ original_pdb_path = "../../output/0_original_input.pdb"
310
+
311
+ print(f"Loading original PDB from {original_pdb_path} as object '{pdb_id}'")
312
+ cmd.load(original_pdb_path, pdb_id)
313
+
314
+ if output_pdb is None:
315
+ output_pdb = f"{pdb_id}_rebuilt.pdb"
316
+
317
+ # -----------------------------
318
+ # 2. Align each ESMFold (or minimized) chain and fix chain IDs
319
+ # -----------------------------
320
+ for chain in chains_to_replace:
321
+ esm_obj = f"{pdb_id}_chain_{chain}_esmfold"
322
+
323
+ # For minimized chains, use the superimposed minimized noH PDB
324
+ # (minimization writes in a different frame; we align it to original here).
325
+ if chains_use_minimized and chain in chains_use_minimized:
326
+ esm_pdb_filename = f"{pdb_id}_chain_{chain}_esmfold_minimized_noH.pdb"
327
+ print(f"Loading minimized PDB {esm_pdb_filename} as object '{esm_obj}' (will superimpose to original)")
328
+ else:
329
+ esm_pdb_filename = f"{pdb_id}_chain_{chain}_esmfold.pdb"
330
+ print(f"Loading ESMFold PDB {esm_pdb_filename} as object '{esm_obj}'")
331
+ cmd.load(esm_pdb_filename, esm_obj)
332
+
333
+ # ESMFold outputs everything as chain A by default.
334
+ # Rename the chain in the loaded object to match the target chain ID.
335
+ print(f"Renaming chain A -> {chain} in {esm_obj}")
336
+ cmd.alter(esm_obj, f"chain='{chain}'")
337
+ cmd.sort(esm_obj) # Rebuild internal indices after alter
338
+
339
+ align_cmd = (
340
+ f"{esm_obj} and name CA",
341
+ f"{pdb_id} and chain {chain} and name CA"
342
+ )
343
+
344
+ print(f"Aligning {esm_obj} to {pdb_id} chain {chain}")
345
+ cmd.align(*align_cmd)
346
+
347
+ # -----------------------------
348
+ # 3. Build selection strings
349
+ # -----------------------------
350
+ chains_str = "+".join(chains_to_replace)
351
+
352
+ esm_objs_str = " or ".join(
353
+ f"{pdb_id}_chain_{chain}_esmfold"
354
+ for chain in chains_to_replace
355
+ )
356
+
357
+ selection = (
358
+ f"({pdb_id} and not chain {chains_str}) or "
359
+ f"({esm_objs_str})"
360
+ )
361
+
362
+ # -----------------------------
363
+ # 4. Create final model
364
+ # -----------------------------
365
+ cmd.select("final_model", selection)
366
+
367
+ # -----------------------------
368
+ # 5. Save rebuilt structure (protein only)
369
+ # -----------------------------
370
+ import os
371
+ temp_protein_pdb = output_pdb.replace('.pdb', '_protein_temp.pdb')
372
+ cmd.save(temp_protein_pdb, "final_model")
373
+
374
+ # -----------------------------
375
+ # 6. Add non-protein atoms from original PDB
376
+ # -----------------------------
377
+ print(f"Adding non-protein atoms from original file...")
378
+ # Convert paths to absolute paths if they're relative
379
+ abs_original = os.path.abspath(original_pdb_path) if original_pdb_path else None
380
+ abs_temp = os.path.abspath(temp_protein_pdb)
381
+ abs_output = os.path.abspath(output_pdb)
382
+ merge_non_protein_atoms(abs_original, abs_temp, abs_output, chains_to_replace)
383
+
384
+ # Clean up temporary protein file
385
+ try:
386
+ if os.path.exists(temp_protein_pdb):
387
+ os.remove(temp_protein_pdb)
388
+ except Exception as e:
389
+ print(f"Warning: Could not remove temporary file {temp_protein_pdb}: {e}")
390
+
391
+ # -----------------------------
392
+ # 7. Clean up temporary objects (keep final_model for potential reuse)
393
+ # -----------------------------
394
+ try:
395
+ # Delete the original and ESMFold objects, but keep final_model
396
+ cmd.delete(pdb_id)
397
+ for chain in chains_to_replace:
398
+ esm_obj = f"{pdb_id}_chain_{chain}_esmfold"
399
+ cmd.delete(esm_obj)
400
+ except Exception as e:
401
+ print(f"Warning: Could not clean up temporary objects: {e}")
402
+
403
+ print(f"✅ Final rebuilt structure saved as: {output_pdb}")
404
+
405
+
406
+ if __name__ == "__main__":
407
+ # Path to the original input PDB used by the pipeline
408
+ original_pdb_path = "../../output/0_original_input.pdb"
409
+
410
+ # Automatically infer the PDB ID from the original PDB file,
411
+ # instead of hard-coding it (e.g., '3hhr').
412
+ pdb_id = get_pdb_id_from_pdb_file(original_pdb_path)
413
+ print(f"Detected PDB ID from original file: {pdb_id}")
414
+
415
+ # 1) Find missing residues for this structure
416
+ missing = detect_missing_residues(pdb_id)
417
+ chain_sequences = get_chain_sequences(pdb_id)
418
+
419
+ chains_with_missing = {
420
+ chain: chain_sequences[chain]
421
+ for chain in missing
422
+ if chain in chain_sequences
423
+ }
424
+
425
+ # 2) Write FASTA for chains with missing residues
426
+ write_fasta_for_missing_chains(pdb_id, chains_with_missing)
427
+
428
+ # 3) Run ESMFold for each chain and save results
429
+ esmfold_results = {}
430
+ chains_to_replace = []
431
+
432
+ for chain, seq in chains_with_missing.items():
433
+ print(f"Running ESMFold for chain {chain}")
434
+ pdb_text = run_esmfold(seq)
435
+ esmfold_results[chain] = pdb_text
436
+ chains_to_replace.append(chain)
437
+ # Save each chain
438
+ with open(f"{pdb_id}_chain_{chain}_esmfold.pdb", "w") as f:
439
+ f.write(pdb_text)
440
+
441
+ # 4) Rebuild PDB in PyMOL using original structure and ESMFold chains
442
+ rebuild_pdb_with_esmfold(
443
+ pdb_id,
444
+ chains_to_replace,
445
+ original_pdb_path=original_pdb_path,
446
+ )
ambermdflow/__init__.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ AmberMDFlow: Web-based MD simulation pipeline with AMBER, ESMFold, docking, and PLUMED.
3
+
4
+ AmberMDFlow provides a complete workflow for:
5
+ - Protein structure loading and visualization
6
+ - Missing residue completion with ESMFold
7
+ - Structure preparation (cleaning, capping, chain/ligand selection)
8
+ - Ligand docking with AutoDock Vina + Meeko
9
+ - AMBER force field parameterization
10
+ - MD simulation file generation
11
+ - PLUMED collective variable configuration
12
+
13
+ Usage:
14
+ # Run the web interface
15
+ $ ambermdflow
16
+ # or
17
+ $ python -m ambermdflow
18
+
19
+ # Import in Python
20
+ from ambermdflow.app import app
21
+ from ambermdflow.structure_preparation import prepare_structure
22
+
23
+ Requirements:
24
+ - Python >= 3.10
25
+ - Conda packages: ambertools, pymol-open-source, vina, openbabel, rdkit, gemmi
26
+ - See README.md for full installation instructions
27
+
28
+ License: MIT
29
+ """
30
+
31
+ __version__ = "0.0.1"
32
+ __author__ = "Hemant Nagar"
33
+ __email__ = "hn533621@ohio.edu"
34
+ # Expose key components for programmatic use
35
+ from ambermdflow.app import app
36
+
37
+ __all__ = ["app", "__version__"]
ambermdflow/__main__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run the AmberMDFlow web server. Use: python -m ambermdflow or ambermdflow"""
2
+
3
+ from ambermdflow.app import app
4
+
5
+
6
+ def main():
7
+ app.run(debug=False, host="0.0.0.0", port=7860)
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()
ambermdflow/add_caps.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Written by Mohd Ibrahim
2
+ # Technical University of Munich
3
+ # Email: ibrahim.mohd@tum.de
4
+
5
+ import numpy as np
6
+ import MDAnalysis as mda
7
+ import argparse
8
+ import warnings
9
+ warnings.filterwarnings("ignore")
10
+
11
+ np.random.seed(42)
12
+
13
+ parser = argparse.ArgumentParser(
14
+ description="Add capping groups ACE and NME to protein termini. "
15
+ "Remove hydrogens before using this script")
16
+ parser.add_argument('-i', dest='in_file', type=str,
17
+ default='protein_noh.pdb', help='pdb file')
18
+ parser.add_argument('-o', dest='out_file', type=str,
19
+ default='protein_noh_cap.pdb', help='output file')
20
+
21
+ args = parser.parse_args()
22
+ in_file = args.in_file
23
+ out_file = args.out_file
24
+
25
+
26
+ def create_universe(n_atoms, name, resname, positions, resids, segid):
27
+ u_new = mda.Universe.empty(
28
+ n_atoms=n_atoms,
29
+ n_residues=n_atoms,
30
+ atom_resindex=np.arange(n_atoms),
31
+ residue_segindex=np.arange(n_atoms),
32
+ n_segments=n_atoms,
33
+ trajectory=True
34
+ )
35
+ u_new.add_TopologyAttr('name', name)
36
+ u_new.add_TopologyAttr('resid', resids)
37
+ u_new.add_TopologyAttr('resname', resname)
38
+ u_new.atoms.positions = positions
39
+ u_new.add_TopologyAttr('segid', n_atoms * [segid])
40
+ u_new.add_TopologyAttr('chainID', n_atoms * [segid])
41
+ return u_new
42
+
43
+
44
+ def get_nme_pos(end_residue):
45
+ if "OXT" in end_residue.names:
46
+ index = np.where(end_residue.names == "OXT")[0][0]
47
+ N_position = end_residue.positions[index]
48
+ index_c = np.where(end_residue.names == "C")[0][0]
49
+ carbon_position = end_residue.positions[index_c]
50
+ vector = N_position - carbon_position
51
+ vector /= np.sqrt(sum(vector**2))
52
+ C_position = N_position + vector * 1.36
53
+ return N_position, C_position
54
+ else:
55
+ index_o = np.where(end_residue.names == "O")[0][0]
56
+ index_ca = np.where(end_residue.names == "CA")[0][0]
57
+ mid_point = (end_residue.positions[index_o] +
58
+ end_residue.positions[index_ca]) / 2
59
+ index_c = np.where(end_residue.names == "C")[0][0]
60
+ vector = end_residue.positions[index_c] - mid_point
61
+ vector /= np.sqrt(sum(vector**2))
62
+ N_position = end_residue.positions[index_c] + 1.36 * vector
63
+ C_position = N_position + 1.36 * vector
64
+ return N_position, C_position
65
+
66
+
67
+ def get_ace_pos(end_residue):
68
+ index_ca = np.where(end_residue.names == "CA")[0][0]
69
+ index_n = np.where(end_residue.names == "N")[0][0]
70
+ vector = end_residue.positions[index_n] - end_residue.positions[index_ca]
71
+ vector /= np.sqrt(sum(vector**2))
72
+ C1_position = end_residue.positions[index_n] + 1.36 * vector
73
+
74
+ xa, ya, za = end_residue.positions[index_ca]
75
+ xg, yg, zg = C1_position
76
+
77
+ orientation = np.array([2 * np.random.rand() - 1,
78
+ 2 * np.random.rand() - 1,
79
+ 2 * np.random.rand() - 1])
80
+ nx, ny, nz = orientation / np.sqrt(sum(orientation**2))
81
+
82
+ x1 = xg - (xa - xg) / 2 + np.sqrt(3) * (ny * (za - zg) - nz * (ya - yg)) / 2
83
+ y1 = yg - (ya - yg) / 2 + np.sqrt(3) * (nz * (xa - xg) - nx * (za - zg)) / 2
84
+ z1 = zg - (za - zg) / 2 + np.sqrt(3) * (nx * (ya - yg) - ny * (xa - xg)) / 2
85
+
86
+ x2 = xg - (xa - xg) / 2 - np.sqrt(3) * (ny * (za - zg) - nz * (ya - yg)) / 2
87
+ y2 = yg - (ya - yg) / 2 - np.sqrt(3) * (nz * (xa - xg) - nx * (za - zg)) / 2
88
+ z2 = zg - (za - zg) / 2 - np.sqrt(3) * (nx * (ya - yg) - ny * (xa - xg)) / 2
89
+
90
+ C2_position = np.array([x1, y1, z1])
91
+ O_position = np.array([x2, y2, z2])
92
+
93
+ vector = C2_position - C1_position
94
+ vector /= np.sqrt(sum(vector**2))
95
+ C2_position = C1_position + 1.36 * vector
96
+
97
+ vector = O_position - C1_position
98
+ vector /= np.sqrt(sum(vector**2))
99
+ O_position = C1_position + 1.36 * vector
100
+
101
+ return C1_position, C2_position, O_position
102
+
103
+
104
+ # ----------- Main processing -----------
105
+ u = mda.Universe(in_file)
106
+ res_start = 0
107
+ segment_universes = []
108
+
109
+ for seg in u.segments:
110
+ chain = u.select_atoms(f"segid {seg.segid}")
111
+
112
+ # ACE
113
+ resid_c = chain.residues.resids[0]
114
+ end_residue = u.select_atoms(f"segid {seg.segid} and resid {resid_c}")
115
+ c1_pos, c2_pos, o_pos = get_ace_pos(end_residue)
116
+
117
+ # keep original mapping (C, CH3, O)
118
+ ace_names = ["C", "CH3", "O"]
119
+ ace_positions = [c1_pos, c2_pos, o_pos]
120
+ resid = chain.residues.resids[0]
121
+ ace_universe = create_universe(
122
+ n_atoms=len(ace_positions),
123
+ name=ace_names,
124
+ resname=len(ace_names) * ["ACE"],
125
+ positions=ace_positions,
126
+ resids=resid * np.ones(len(ace_names)),
127
+ segid=chain.segids[0]
128
+ )
129
+
130
+ # >>> Reorder rows only: CH3, C, O <<<
131
+ ace_universe = mda.Merge(
132
+ ace_universe.atoms.select_atoms("name CH3"),
133
+ ace_universe.atoms.select_atoms("name C"),
134
+ ace_universe.atoms.select_atoms("name O")
135
+ )
136
+
137
+ # NME
138
+ resid_c = chain.residues.resids[-1]
139
+ end_residue = u.select_atoms(f"segid {seg.segid} and resid {resid_c}")
140
+ nme_positions = get_nme_pos(end_residue)
141
+ nme_names = ["N", "C"]
142
+ resid = chain.residues.resids[-1] + 2
143
+ nme_universe = create_universe(
144
+ n_atoms=len(nme_names),
145
+ name=nme_names,
146
+ resname=len(nme_names) * ["NME"],
147
+ positions=nme_positions,
148
+ resids=resid * np.ones(len(nme_names)),
149
+ segid=chain.segids[0]
150
+ )
151
+
152
+ # Remove OXT if present
153
+ if "OXT" in end_residue.names:
154
+ index = np.where(end_residue.names == "OXT")[0][0]
155
+ OXT = end_residue[index]
156
+ Chain = u.select_atoms(f"segid {seg.segid} and not index {OXT.index}")
157
+ else:
158
+ Chain = u.select_atoms(f"segid {seg.segid}")
159
+
160
+ # Merge ACE, protein, NME
161
+ u_all = mda.Merge(ace_universe.atoms, Chain, nme_universe.atoms)
162
+
163
+ # Renumber residues
164
+ resids_ace = [res_start + 1] * 3
165
+ resids_pro = np.arange(resids_ace[0] + 1,
166
+ Chain.residues.n_residues + resids_ace[0] + 1)
167
+ resids_nme = [resids_pro[-1] + 1] * 2
168
+ u_all.atoms.residues.resids = np.concatenate(
169
+ [resids_ace, resids_pro, resids_nme]
170
+ )
171
+ res_start = u_all.atoms.residues.resids[-1]
172
+ segment_universes.append(u_all)
173
+
174
+ # Join and write output
175
+ all_uni = mda.Merge(*(seg.atoms for seg in segment_universes))
176
+ all_uni.atoms.write(out_file)
ambermdflow/app.py ADDED
The diff for this file is too large to render. See raw diff
 
ambermdflow/css/plumed.css ADDED
@@ -0,0 +1,1064 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* PLUMED Section Styles */
2
+
3
+ /* PLUMED Citation Note */
4
+ .plumed-citation-note {
5
+ background: #e3f2fd;
6
+ border: 2px solid #2196f3;
7
+ border-left: 5px solid #2196f3;
8
+ border-radius: 8px;
9
+ padding: 1rem 1.5rem;
10
+ margin-bottom: 1.5rem;
11
+ display: flex;
12
+ align-items: flex-start;
13
+ gap: 1rem;
14
+ box-shadow: 0 2px 4px rgba(33, 150, 243, 0.1);
15
+ }
16
+
17
+ .plumed-citation-note i {
18
+ color: #2196f3;
19
+ font-size: 1.5rem;
20
+ margin-top: 0.2rem;
21
+ flex-shrink: 0;
22
+ }
23
+
24
+ .citation-content {
25
+ flex: 1;
26
+ color: #1565c0;
27
+ line-height: 1.6;
28
+ }
29
+
30
+ .citation-content p {
31
+ margin: 0.5rem 0;
32
+ font-size: 0.95rem;
33
+ }
34
+
35
+ .citation-content p:first-child {
36
+ margin-top: 0;
37
+ }
38
+
39
+ .citation-content p:last-child {
40
+ margin-bottom: 0;
41
+ }
42
+
43
+ .citation-content strong {
44
+ color: #0d47a1;
45
+ font-weight: 600;
46
+ }
47
+
48
+ .citation-content a {
49
+ color: #1976d2;
50
+ text-decoration: none;
51
+ font-weight: 500;
52
+ transition: color 0.3s ease;
53
+ display: inline-flex;
54
+ align-items: center;
55
+ gap: 0.25rem;
56
+ }
57
+
58
+ .citation-content a:hover {
59
+ color: #0d47a1;
60
+ text-decoration: underline;
61
+ }
62
+
63
+ .citation-content a i {
64
+ font-size: 0.85rem;
65
+ color: inherit;
66
+ margin: 0;
67
+ }
68
+
69
+ .plumed-container {
70
+ display: flex;
71
+ gap: 2rem;
72
+ margin-top: 1.5rem;
73
+ min-height: 600px;
74
+ }
75
+
76
+ /* Left Sidebar */
77
+ .plumed-sidebar {
78
+ width: 300px;
79
+ background: #f8f9fa;
80
+ border-radius: 8px;
81
+ border: 1px solid #dee2e6;
82
+ display: flex;
83
+ flex-direction: column;
84
+ max-height: 800px;
85
+ overflow: hidden;
86
+ }
87
+
88
+ .sidebar-header {
89
+ padding: 1rem;
90
+ background: #2c3e50;
91
+ color: white;
92
+ border-bottom: 2px solid #3498db;
93
+ }
94
+
95
+ .sidebar-header h3 {
96
+ margin: 0 0 1rem 0;
97
+ font-size: 1.2rem;
98
+ font-weight: 600;
99
+ }
100
+
101
+ .sidebar-header h3 i {
102
+ margin-right: 0.5rem;
103
+ color: #3498db;
104
+ }
105
+
106
+ .search-box {
107
+ position: relative;
108
+ }
109
+
110
+ .search-input {
111
+ width: 100%;
112
+ padding: 0.5rem 2.5rem 0.5rem 0.75rem;
113
+ border: 1px solid #dee2e6;
114
+ border-radius: 4px;
115
+ font-size: 0.9rem;
116
+ transition: all 0.3s ease;
117
+ }
118
+
119
+ .search-input:focus {
120
+ outline: none;
121
+ border-color: #3498db;
122
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
123
+ }
124
+
125
+ .search-icon {
126
+ position: absolute;
127
+ right: 0.75rem;
128
+ top: 50%;
129
+ transform: translateY(-50%);
130
+ color: #7f8c8d;
131
+ pointer-events: none;
132
+ }
133
+
134
+ .cv-list {
135
+ flex: 1;
136
+ overflow-y: auto;
137
+ padding: 0.5rem;
138
+ }
139
+
140
+ .cv-item {
141
+ padding: 0.75rem 1rem;
142
+ margin-bottom: 0.5rem;
143
+ background: white;
144
+ border: 1px solid #dee2e6;
145
+ border-radius: 6px;
146
+ cursor: pointer;
147
+ transition: all 0.3s ease;
148
+ display: flex;
149
+ align-items: center;
150
+ justify-content: space-between;
151
+ }
152
+
153
+ .cv-item:hover {
154
+ background: #e3f2fd;
155
+ border-color: #3498db;
156
+ transform: translateX(5px);
157
+ }
158
+
159
+ .cv-item.active {
160
+ background: #3498db;
161
+ color: white;
162
+ border-color: #2980b9;
163
+ box-shadow: 0 2px 8px rgba(52, 152, 219, 0.3);
164
+ }
165
+
166
+ .cv-item-name {
167
+ font-weight: 600;
168
+ font-size: 0.95rem;
169
+ }
170
+
171
+ .cv-item-category {
172
+ font-size: 0.75rem;
173
+ opacity: 0.7;
174
+ margin-top: 0.25rem;
175
+ }
176
+
177
+ .cv-item.active .cv-item-category {
178
+ opacity: 0.9;
179
+ }
180
+
181
+ .cv-item-icon {
182
+ color: #3498db;
183
+ margin-left: 0.5rem;
184
+ }
185
+
186
+ .cv-item.active .cv-item-icon {
187
+ color: white;
188
+ }
189
+
190
+ /* Right Content Panel */
191
+ .plumed-content {
192
+ flex: 1;
193
+ background: white;
194
+ border-radius: 8px;
195
+ border: 1px solid #dee2e6;
196
+ padding: 1.5rem;
197
+ overflow-y: auto;
198
+ max-height: 800px;
199
+ }
200
+
201
+ .content-header {
202
+ display: flex;
203
+ justify-content: space-between;
204
+ align-items: center;
205
+ margin-bottom: 1.5rem;
206
+ padding-bottom: 1rem;
207
+ border-bottom: 2px solid #e1e8ed;
208
+ }
209
+
210
+ .content-header h3 {
211
+ margin: 0;
212
+ color: #2c3e50;
213
+ font-size: 1.5rem;
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 0.5rem;
217
+ }
218
+
219
+ .plumed-doc-link {
220
+ color: #3498db;
221
+ text-decoration: none;
222
+ font-size: 0.85em;
223
+ margin-left: 0.5rem;
224
+ transition: color 0.3s ease;
225
+ display: inline-flex;
226
+ align-items: center;
227
+ gap: 0.25rem;
228
+ }
229
+
230
+ .plumed-doc-link:hover {
231
+ color: #2980b9;
232
+ text-decoration: underline;
233
+ }
234
+
235
+ .plumed-doc-link i {
236
+ font-size: 0.75em;
237
+ }
238
+
239
+ .welcome-message {
240
+ text-align: center;
241
+ padding: 3rem 1rem;
242
+ color: #7f8c8d;
243
+ }
244
+
245
+ .welcome-message i {
246
+ color: #bdc3c7;
247
+ margin-bottom: 1rem;
248
+ }
249
+
250
+ .welcome-message h3 {
251
+ color: #2c3e50;
252
+ margin: 1rem 0;
253
+ }
254
+
255
+ /* Documentation Sections */
256
+ .cv-documentation {
257
+ margin-bottom: 2rem;
258
+ }
259
+
260
+ .doc-section {
261
+ margin-bottom: 2rem;
262
+ padding: 1.5rem;
263
+ background: #f8f9fa;
264
+ border-radius: 8px;
265
+ border-left: 4px solid #3498db;
266
+ }
267
+
268
+ .doc-section h4 {
269
+ color: #2c3e50;
270
+ margin-bottom: 1rem;
271
+ font-size: 1.2rem;
272
+ display: flex;
273
+ align-items: center;
274
+ }
275
+
276
+ .doc-section h4 {
277
+ display: flex;
278
+ align-items: center;
279
+ margin-bottom: 1rem;
280
+ }
281
+
282
+ .doc-section h4 i {
283
+ margin-right: 0.5rem;
284
+ color: #3498db;
285
+ }
286
+
287
+ /* Options heading with legend on the right - only apply space-between to headings with color-legend */
288
+ .doc-section h4 .color-legend {
289
+ margin-left: auto;
290
+ }
291
+
292
+ /* For browsers that support :has() */
293
+ @supports selector(:has(*)) {
294
+ .doc-section h4:has(.color-legend) {
295
+ justify-content: space-between;
296
+ flex-wrap: wrap;
297
+ gap: 1rem;
298
+ }
299
+ }
300
+
301
+ /* Fallback for browsers without :has() support - use a class */
302
+ .doc-section h4.options-heading-with-legend {
303
+ justify-content: space-between;
304
+ flex-wrap: wrap;
305
+ gap: 1rem;
306
+ }
307
+
308
+ .color-legend {
309
+ display: flex;
310
+ align-items: center;
311
+ gap: 1rem;
312
+ font-size: 0.85rem;
313
+ font-weight: normal;
314
+ color: #7f8c8d;
315
+ margin-left: auto;
316
+ }
317
+
318
+ .legend-item {
319
+ display: flex;
320
+ align-items: center;
321
+ gap: 0.5rem;
322
+ }
323
+
324
+ .legend-item code {
325
+ font-size: 0.8rem;
326
+ padding: 0.25rem 0.5rem;
327
+ }
328
+
329
+ .doc-content {
330
+ color: #555;
331
+ line-height: 1.8;
332
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
333
+ }
334
+
335
+ .description-paragraph {
336
+ margin-bottom: 1rem;
337
+ line-height: 1.8;
338
+ }
339
+
340
+ .description-list {
341
+ list-style: none;
342
+ padding-left: 0;
343
+ margin: 1rem 0;
344
+ }
345
+
346
+ .description-list li {
347
+ padding: 0.5rem 0 0.5rem 1.5rem;
348
+ position: relative;
349
+ line-height: 1.8;
350
+ margin-bottom: 0.5rem;
351
+ }
352
+
353
+ .description-list li::before {
354
+ content: '•';
355
+ position: absolute;
356
+ left: 0;
357
+ color: #3498db;
358
+ font-size: 1.2em;
359
+ font-weight: bold;
360
+ line-height: 1.4;
361
+ }
362
+
363
+ .description-list li:last-child {
364
+ margin-bottom: 0;
365
+ }
366
+
367
+ /* Ensure Unicode mathematical symbols render correctly */
368
+ .doc-content,
369
+ .math-formula {
370
+ font-variant-numeric: normal;
371
+ text-rendering: optimizeLegibility;
372
+ -webkit-font-feature-settings: "kern" 1;
373
+ font-feature-settings: "kern" 1;
374
+ }
375
+
376
+ .syntax-box,
377
+ .example-box {
378
+ background: #2c3e50;
379
+ color: #ecf0f1;
380
+ padding: 1rem;
381
+ border-radius: 6px;
382
+ overflow-x: auto;
383
+ font-family: 'Courier New', monospace;
384
+ font-size: 0.9rem;
385
+ line-height: 1.6;
386
+ margin: 0;
387
+ white-space: pre-wrap;
388
+ word-wrap: break-word;
389
+ }
390
+
391
+ .example-box {
392
+ background: #34495e;
393
+ border-left: 4px solid #3498db;
394
+ }
395
+
396
+ /* Editor Section */
397
+ .cv-editor-section {
398
+ margin-top: 2rem;
399
+ border-top: 2px solid #e1e8ed;
400
+ padding-top: 1.5rem;
401
+ }
402
+
403
+ .editor-header {
404
+ display: flex;
405
+ justify-content: space-between;
406
+ align-items: center;
407
+ margin-bottom: 1rem;
408
+ }
409
+
410
+ .editor-header h4 {
411
+ color: #2c3e50;
412
+ margin: 0;
413
+ font-size: 1.1rem;
414
+ }
415
+
416
+ .editor-header h4 i {
417
+ margin-right: 0.5rem;
418
+ color: #3498db;
419
+ }
420
+
421
+ .editor-actions {
422
+ display: flex;
423
+ gap: 0.5rem;
424
+ }
425
+
426
+ .cv-editor {
427
+ width: 100%;
428
+ min-height: 300px;
429
+ padding: 1rem;
430
+ border: 2px solid #dee2e6;
431
+ border-radius: 6px;
432
+ font-family: 'Courier New', monospace;
433
+ font-size: 0.9rem;
434
+ line-height: 1.6;
435
+ resize: vertical;
436
+ transition: border-color 0.3s ease;
437
+ }
438
+
439
+ .cv-editor:focus {
440
+ outline: none;
441
+ border-color: #3498db;
442
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
443
+ }
444
+
445
+ .editor-footer {
446
+ display: flex;
447
+ justify-content: space-between;
448
+ margin-top: 0.5rem;
449
+ font-size: 0.85rem;
450
+ color: #7f8c8d;
451
+ }
452
+
453
+ /* Saved Configurations */
454
+ .saved-configs {
455
+ margin-top: 2rem;
456
+ padding-top: 1.5rem;
457
+ border-top: 2px solid #e1e8ed;
458
+ }
459
+
460
+ .saved-configs h4 {
461
+ color: #2c3e50;
462
+ margin-bottom: 1rem;
463
+ font-size: 1.1rem;
464
+ }
465
+
466
+ .saved-configs h4 i {
467
+ margin-right: 0.5rem;
468
+ color: #3498db;
469
+ }
470
+
471
+ .config-item {
472
+ background: #f8f9fa;
473
+ border: 1px solid #dee2e6;
474
+ border-radius: 6px;
475
+ padding: 1rem;
476
+ margin-bottom: 0.75rem;
477
+ display: flex;
478
+ justify-content: space-between;
479
+ align-items: center;
480
+ transition: all 0.3s ease;
481
+ }
482
+
483
+ .config-item:hover {
484
+ background: #e3f2fd;
485
+ border-color: #3498db;
486
+ }
487
+
488
+ .config-item-name {
489
+ font-weight: 600;
490
+ color: #2c3e50;
491
+ }
492
+
493
+ .config-item-actions {
494
+ display: flex;
495
+ gap: 0.5rem;
496
+ }
497
+
498
+ .config-item-actions button {
499
+ padding: 0.25rem 0.75rem;
500
+ font-size: 0.85rem;
501
+ }
502
+
503
+ /* Section Description */
504
+ .section-description {
505
+ color: #7f8c8d;
506
+ margin-bottom: 1.5rem;
507
+ font-style: italic;
508
+ }
509
+
510
+ /* Glossary and Options Lists */
511
+ .glossary-content,
512
+ .options-list,
513
+ .components-list {
514
+ display: flex;
515
+ flex-direction: column;
516
+ gap: 1rem;
517
+ }
518
+
519
+ .glossary-item {
520
+ background: white;
521
+ padding: 1rem;
522
+ border-radius: 6px;
523
+ border-left: 3px solid #9b59b6;
524
+ margin-bottom: 0.5rem;
525
+ }
526
+
527
+ .glossary-item strong {
528
+ color: #2c3e50;
529
+ font-size: 1rem;
530
+ display: inline-block;
531
+ margin-right: 0.5rem;
532
+ }
533
+
534
+ .glossary-item p {
535
+ margin: 0.5rem 0 0 0;
536
+ color: #555;
537
+ line-height: 1.6;
538
+ }
539
+
540
+ .glossary-content > p {
541
+ font-weight: 600;
542
+ color: #2c3e50;
543
+ margin-bottom: 1rem;
544
+ }
545
+
546
+ /* Options Keywords - Horizontal Layout */
547
+ .options-keywords {
548
+ display: flex;
549
+ flex-wrap: wrap;
550
+ gap: 0.75rem;
551
+ align-items: center;
552
+ padding: 1rem;
553
+ background: #f8f9fa;
554
+ border-radius: 6px;
555
+ border: 1px solid #dee2e6;
556
+ }
557
+
558
+ .options-keywords code {
559
+ padding: 0.5rem 0.75rem;
560
+ border-radius: 4px;
561
+ font-family: 'Courier New', monospace;
562
+ font-size: 0.9rem;
563
+ font-weight: 600;
564
+ white-space: nowrap;
565
+ display: inline-block;
566
+ transition: all 0.2s ease;
567
+ }
568
+
569
+ .keyword-required {
570
+ background: #fee;
571
+ color: #c33;
572
+ border: 1px solid #fcc;
573
+ }
574
+
575
+ .keyword-required:hover {
576
+ background: #fdd;
577
+ border-color: #c33;
578
+ transform: translateY(-1px);
579
+ box-shadow: 0 2px 4px rgba(204, 51, 51, 0.2);
580
+ }
581
+
582
+ .keyword-optional {
583
+ background: #f5f5f5;
584
+ color: #666;
585
+ border: 1px solid #ddd;
586
+ }
587
+
588
+ .keyword-optional:hover {
589
+ background: #eee;
590
+ border-color: #999;
591
+ transform: translateY(-1px);
592
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
593
+ }
594
+
595
+ /* Components Lists */
596
+ .components-list {
597
+ display: flex;
598
+ flex-direction: column;
599
+ gap: 1rem;
600
+ }
601
+
602
+ .component-item {
603
+ background: white;
604
+ padding: 1rem;
605
+ border-radius: 6px;
606
+ border-left: 3px solid #3498db;
607
+ margin-bottom: 0.5rem;
608
+ }
609
+
610
+ .component-item strong {
611
+ color: #2c3e50;
612
+ font-size: 1rem;
613
+ display: inline-block;
614
+ margin-right: 0.5rem;
615
+ }
616
+
617
+ .component-item p {
618
+ margin: 0.5rem 0 0 0;
619
+ color: #555;
620
+ line-height: 1.6;
621
+ }
622
+
623
+ /* Notes List */
624
+ .notes-list {
625
+ list-style: none;
626
+ padding: 0;
627
+ }
628
+
629
+ .notes-list li {
630
+ padding: 0.75rem;
631
+ margin-bottom: 0.5rem;
632
+ background: #fff3cd;
633
+ border-left: 3px solid #ffc107;
634
+ border-radius: 4px;
635
+ color: #856404;
636
+ }
637
+
638
+ .notes-list li::before {
639
+ content: "ℹ️";
640
+ margin-right: 0.5rem;
641
+ }
642
+
643
+ /* Math Formulas */
644
+ .math-formula {
645
+ background: #f8f9fa;
646
+ padding: 0.75rem;
647
+ border-radius: 4px;
648
+ margin: 0.75rem 0;
649
+ font-family: 'Times New Roman', 'DejaVu Serif', serif;
650
+ font-size: 1rem;
651
+ border-left: 3px solid #3498db;
652
+ white-space: pre-wrap;
653
+ line-height: 1.8;
654
+ color: #2c3e50;
655
+ font-style: italic;
656
+ }
657
+
658
+ /* Mathematical formatting */
659
+ .math-fraction {
660
+ display: inline-flex;
661
+ flex-direction: column;
662
+ vertical-align: middle;
663
+ text-align: center;
664
+ margin: 0 0.3em;
665
+ line-height: 1;
666
+ font-size: 1.1em;
667
+ }
668
+
669
+ .math-numerator {
670
+ border-bottom: 1.5px solid currentColor;
671
+ padding: 0 0.3em 0.1em 0.3em;
672
+ line-height: 1.1;
673
+ }
674
+
675
+ .math-denominator {
676
+ padding: 0.1em 0.3em 0 0.3em;
677
+ line-height: 1.1;
678
+ }
679
+
680
+ .math-sqrt {
681
+ display: inline-block;
682
+ position: relative;
683
+ vertical-align: middle;
684
+ margin: 0 0.2em;
685
+ font-size: 1.1em;
686
+ }
687
+
688
+ .math-sqrt-symbol {
689
+ font-size: 1.3em;
690
+ vertical-align: middle;
691
+ margin-right: 0.1em;
692
+ line-height: 1;
693
+ }
694
+
695
+ .math-radicand {
696
+ display: inline-block;
697
+ padding: 0.1em 0.3em;
698
+ margin-left: 0.1em;
699
+ border-top: 1.5px solid currentColor;
700
+ vertical-align: middle;
701
+ }
702
+
703
+ .math-formula sup,
704
+ .math-formula sub,
705
+ .description-paragraph sup,
706
+ .description-paragraph sub,
707
+ .description-list li sup,
708
+ .description-list li sub {
709
+ font-size: 0.75em;
710
+ line-height: 0;
711
+ position: relative;
712
+ vertical-align: baseline;
713
+ font-weight: normal;
714
+ }
715
+
716
+ .math-formula sup,
717
+ .description-paragraph sup,
718
+ .description-list li sup {
719
+ top: -0.5em;
720
+ }
721
+
722
+ .math-formula sub,
723
+ .description-paragraph sub,
724
+ .description-list li sub {
725
+ bottom: -0.25em;
726
+ }
727
+
728
+ /* Ensure proper rendering of subscripts and superscripts in all contexts */
729
+ .doc-content sup,
730
+ .doc-content sub {
731
+ font-size: 0.75em;
732
+ line-height: 0;
733
+ position: relative;
734
+ vertical-align: baseline;
735
+ }
736
+
737
+ .doc-content sup {
738
+ top: -0.5em;
739
+ }
740
+
741
+ .doc-content sub {
742
+ bottom: -0.25em;
743
+ }
744
+
745
+ .doc-content sub {
746
+ font-size: 0.8em;
747
+ vertical-align: sub;
748
+ }
749
+
750
+ .doc-content code {
751
+ background: #f1f3f5;
752
+ padding: 0.2rem 0.4rem;
753
+ border-radius: 3px;
754
+ font-family: 'Courier New', monospace;
755
+ font-size: 0.9em;
756
+ }
757
+
758
+ /* Related CVs */
759
+ .related-cvs {
760
+ display: flex;
761
+ flex-wrap: wrap;
762
+ gap: 0.5rem;
763
+ }
764
+
765
+ .related-cv-badge {
766
+ display: inline-block;
767
+ padding: 0.5rem 1rem;
768
+ background: #e3f2fd;
769
+ color: #1976d2;
770
+ border-radius: 20px;
771
+ cursor: pointer;
772
+ transition: all 0.3s ease;
773
+ font-weight: 500;
774
+ border: 1px solid #90caf9;
775
+ }
776
+
777
+ .related-cv-badge:hover {
778
+ background: #1976d2;
779
+ color: white;
780
+ transform: translateY(-2px);
781
+ box-shadow: 0 2px 8px rgba(25, 118, 210, 0.3);
782
+ }
783
+
784
+ /* Responsive Design */
785
+ @media (max-width: 1024px) {
786
+ .plumed-container {
787
+ flex-direction: column;
788
+ }
789
+
790
+ .plumed-sidebar {
791
+ width: 100%;
792
+ max-height: 300px;
793
+ }
794
+ }
795
+
796
+ /* Scrollbar Styling */
797
+ .cv-list::-webkit-scrollbar,
798
+ .plumed-content::-webkit-scrollbar {
799
+ width: 8px;
800
+ }
801
+
802
+ .cv-list::-webkit-scrollbar-track,
803
+ .plumed-content::-webkit-scrollbar-track {
804
+ background: #f1f1f1;
805
+ border-radius: 4px;
806
+ }
807
+
808
+ .cv-list::-webkit-scrollbar-thumb,
809
+ .plumed-content::-webkit-scrollbar-thumb {
810
+ background: #bdc3c7;
811
+ border-radius: 4px;
812
+ }
813
+
814
+ .cv-list::-webkit-scrollbar-thumb:hover,
815
+ .plumed-content::-webkit-scrollbar-thumb:hover {
816
+ background: #95a5a6;
817
+ }
818
+
819
+ /* PLUMED Section Cards */
820
+ .plumed-section-card {
821
+ margin-bottom: 1.5rem;
822
+ }
823
+
824
+ .plumed-section-card:last-child {
825
+ margin-bottom: 0;
826
+ }
827
+
828
+ /* PLUMED Section Headers - Smaller font size */
829
+ .plumed-section-card h2 {
830
+ font-size: 1.4rem !important;
831
+ }
832
+
833
+ /* Collapsed state - hide content but keep header visible */
834
+ .plumed-section-card.collapsed .section-description,
835
+ .plumed-section-card.collapsed .plumed-container,
836
+ .plumed-section-card.collapsed .plumed-generate-section,
837
+ .plumed-section-card.collapsed .generate-simulation-files-section {
838
+ max-height: 0;
839
+ opacity: 0;
840
+ margin-top: 0;
841
+ margin-bottom: 0;
842
+ padding-top: 0;
843
+ padding-bottom: 0;
844
+ overflow: hidden;
845
+ transition: max-height 0.4s ease, opacity 0.3s ease, margin 0.3s ease, padding 0.3s ease;
846
+ }
847
+
848
+ /* Keep header visible even when collapsed */
849
+ .plumed-section-card.collapsed .plumed-toggle-header {
850
+ margin-bottom: 0;
851
+ }
852
+
853
+ /* Collapsible Header */
854
+ .plumed-toggle-header {
855
+ cursor: pointer;
856
+ user-select: none;
857
+ display: flex;
858
+ align-items: center;
859
+ justify-content: space-between;
860
+ transition: color 0.3s ease;
861
+ padding: 0.5rem;
862
+ margin: -0.5rem;
863
+ border-radius: 4px;
864
+ text-align: left !important;
865
+ }
866
+
867
+ .plumed-toggle-header > i:first-of-type {
868
+ margin-right: 0.5rem;
869
+ }
870
+
871
+ /* Ensure left alignment for custom PLUMED header */
872
+ #custom-plumed-card h2,
873
+ #custom-plumed-card .plumed-toggle-header {
874
+ text-align: left !important;
875
+ justify-content: flex-start !important;
876
+ }
877
+
878
+ #custom-plumed-card .plumed-toggle-header {
879
+ display: flex !important;
880
+ align-items: center;
881
+ }
882
+
883
+ #custom-plumed-card .plumed-toggle-header > *:first-child {
884
+ margin-right: 0.5rem;
885
+ }
886
+
887
+ #custom-plumed-card .plumed-toggle-header .toggle-icon {
888
+ margin-left: auto;
889
+ margin-right: 0;
890
+ }
891
+
892
+ /* Generate Simulation Files Section - Left Aligned */
893
+ #generate-simulation-files-card h2,
894
+ #generate-simulation-files-card .plumed-toggle-header {
895
+ text-align: left !important;
896
+ justify-content: flex-start !important;
897
+ }
898
+
899
+ #generate-simulation-files-card .plumed-toggle-header {
900
+ display: flex !important;
901
+ align-items: center;
902
+ }
903
+
904
+ #generate-simulation-files-card .plumed-toggle-header > *:first-child {
905
+ margin-right: 0.5rem;
906
+ }
907
+
908
+ #generate-simulation-files-card .plumed-toggle-header .toggle-icon {
909
+ margin-left: auto;
910
+ margin-right: 0;
911
+ }
912
+
913
+ .plumed-toggle-header:hover {
914
+ background: rgba(52, 152, 219, 0.1);
915
+ color: #3498db;
916
+ }
917
+
918
+ .toggle-icon {
919
+ transition: transform 0.3s ease;
920
+ font-size: 0.9em;
921
+ color: #7f8c8d;
922
+ }
923
+
924
+ .plumed-toggle-header.collapsed .toggle-icon {
925
+ transform: rotate(-90deg);
926
+ }
927
+
928
+ .plumed-container,
929
+ .plumed-generate-section {
930
+ transition: max-height 0.4s ease, opacity 0.3s ease, margin 0.3s ease;
931
+ overflow: hidden;
932
+ }
933
+
934
+ .section-description {
935
+ transition: max-height 0.4s ease, opacity 0.3s ease, margin 0.3s ease, padding 0.3s ease;
936
+ overflow: hidden;
937
+ }
938
+
939
+ /* Generate Files Section */
940
+ .plumed-generate-section {
941
+ margin-top: 1.5rem;
942
+ padding-top: 1.5rem;
943
+ }
944
+
945
+ .generate-content {
946
+ display: flex;
947
+ flex-direction: column;
948
+ gap: 1.5rem;
949
+ }
950
+
951
+ .generate-options {
952
+ display: flex;
953
+ gap: 1rem;
954
+ flex-wrap: wrap;
955
+ }
956
+
957
+ .generate-options .btn {
958
+ padding: 0.75rem 1.5rem;
959
+ font-size: 1rem;
960
+ display: flex;
961
+ align-items: center;
962
+ gap: 0.5rem;
963
+ }
964
+
965
+ .generated-file-preview {
966
+ background: #f8f9fa;
967
+ border: 1px solid #dee2e6;
968
+ border-radius: 8px;
969
+ padding: 1.5rem;
970
+ margin-top: 1rem;
971
+ }
972
+
973
+ .generated-file-preview h4 {
974
+ margin: 0 0 1rem 0;
975
+ color: #2c3e50;
976
+ font-size: 1.1rem;
977
+ }
978
+
979
+ .file-preview {
980
+ background: #2c3e50;
981
+ color: #ecf0f1;
982
+ padding: 1rem;
983
+ border-radius: 4px;
984
+ overflow-x: auto;
985
+ font-family: 'Courier New', monospace;
986
+ font-size: 0.9rem;
987
+ line-height: 1.6;
988
+ margin: 0;
989
+ max-height: 400px;
990
+ overflow-y: auto;
991
+ }
992
+
993
+ /* Custom PLUMED File Section */
994
+ .custom-plumed-section {
995
+ margin-top: 1.5rem;
996
+ transition: max-height 0.4s ease, opacity 0.3s ease, margin 0.3s ease, padding 0.3s ease;
997
+ overflow: hidden;
998
+ }
999
+
1000
+ /* Collapsed state for custom PLUMED section */
1001
+ #custom-plumed-card.collapsed .section-description,
1002
+ #custom-plumed-card.collapsed .custom-plumed-section {
1003
+ max-height: 0;
1004
+ opacity: 0;
1005
+ margin-top: 0;
1006
+ margin-bottom: 0;
1007
+ padding-top: 0;
1008
+ padding-bottom: 0;
1009
+ overflow: hidden;
1010
+ }
1011
+
1012
+ /* Keep header visible even when collapsed */
1013
+ #custom-plumed-card.collapsed .plumed-toggle-header {
1014
+ margin-bottom: 0;
1015
+ }
1016
+
1017
+ .custom-editor-header {
1018
+ display: flex;
1019
+ justify-content: space-between;
1020
+ align-items: center;
1021
+ margin-bottom: 1rem;
1022
+ padding-bottom: 0.75rem;
1023
+ border-bottom: 2px solid #dee2e6;
1024
+ }
1025
+
1026
+ .custom-editor-header h4 {
1027
+ margin: 0;
1028
+ color: #2c3e50;
1029
+ font-size: 1.1rem;
1030
+ display: flex;
1031
+ align-items: center;
1032
+ gap: 0.5rem;
1033
+ }
1034
+
1035
+ .custom-editor-header h4 i {
1036
+ color: #3498db;
1037
+ }
1038
+
1039
+ .custom-plumed-editor {
1040
+ width: 100%;
1041
+ min-height: 400px;
1042
+ padding: 1rem;
1043
+ border: 2px solid #dee2e6;
1044
+ border-radius: 8px;
1045
+ font-family: 'Courier New', monospace;
1046
+ font-size: 0.95rem;
1047
+ line-height: 1.6;
1048
+ resize: vertical;
1049
+ background: #ffffff;
1050
+ color: #2c3e50;
1051
+ transition: border-color 0.3s ease;
1052
+ }
1053
+
1054
+ .custom-plumed-editor:focus {
1055
+ outline: none;
1056
+ border-color: #3498db;
1057
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
1058
+ }
1059
+
1060
+ .custom-plumed-editor::placeholder {
1061
+ color: #95a5a6;
1062
+ font-style: italic;
1063
+ }
1064
+
ambermdflow/css/styles.css ADDED
@@ -0,0 +1,2029 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Reset and Base Styles */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ body {
9
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10
+ line-height: 1.6;
11
+ color: #333;
12
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
13
+ min-height: 100vh;
14
+ }
15
+
16
+ .container {
17
+ max-width: 1400px;
18
+ margin: 0 auto;
19
+ background: white;
20
+ min-height: 100vh;
21
+ box-shadow: 0 0 30px rgba(0, 0, 0, 0.1);
22
+ }
23
+
24
+ /* Header Styles */
25
+ .header {
26
+ background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
27
+ color: white;
28
+ padding: 2rem 0;
29
+ text-align: center;
30
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
31
+ }
32
+
33
+ .header-content h1 {
34
+ font-size: 2.5rem;
35
+ margin-bottom: 0.5rem;
36
+ font-weight: 300;
37
+ }
38
+
39
+ .header-content p {
40
+ font-size: 1.1rem;
41
+ opacity: 0.9;
42
+ }
43
+
44
+ .header i {
45
+ margin-right: 0.5rem;
46
+ color: #3498db;
47
+ }
48
+
49
+ /* Tab Navigation */
50
+ .tab-navigation {
51
+ display: flex;
52
+ background: #34495e;
53
+ border-bottom: 3px solid #3498db;
54
+ overflow-x: auto;
55
+ }
56
+
57
+ /* Step Navigation Controls */
58
+ .step-navigation {
59
+ display: flex;
60
+ justify-content: space-between;
61
+ align-items: center;
62
+ background: #ffffff;
63
+ padding: 20px 30px;
64
+ border-top: 1px solid #dee2e6;
65
+ box-shadow: 0 -2px 8px rgba(0,0,0,0.1);
66
+ position: sticky;
67
+ bottom: 0;
68
+ z-index: 100;
69
+ }
70
+
71
+ /* Checkbox with Button Layout */
72
+ .checkbox-with-button {
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 15px;
76
+ margin-bottom: 10px;
77
+ }
78
+
79
+ .checkbox-with-button .checkbox-container {
80
+ margin-bottom: 0;
81
+ flex: 1;
82
+ }
83
+
84
+ .btn-sm {
85
+ padding: 6px 12px;
86
+ font-size: 12px;
87
+ border-radius: 4px;
88
+ }
89
+
90
+ .btn-outline-primary {
91
+ color: #007bff;
92
+ border: 1px solid #007bff;
93
+ background: transparent;
94
+ }
95
+
96
+ .btn-outline-primary:hover:not(:disabled) {
97
+ color: white;
98
+ background: #007bff;
99
+ border-color: #007bff;
100
+ }
101
+
102
+ .btn-outline-primary:disabled {
103
+ color: #6c757d;
104
+ border-color: #6c757d;
105
+ background: transparent;
106
+ cursor: not-allowed;
107
+ }
108
+
109
+ .nav-btn {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 8px;
113
+ padding: 10px 20px;
114
+ border: 2px solid #007bff;
115
+ background: #007bff;
116
+ color: white;
117
+ border-radius: 25px;
118
+ font-weight: 600;
119
+ cursor: pointer;
120
+ transition: all 0.3s ease;
121
+ font-size: 14px;
122
+ }
123
+
124
+ .nav-btn:hover:not(:disabled) {
125
+ background: #0056b3;
126
+ border-color: #0056b3;
127
+ transform: translateY(-2px);
128
+ box-shadow: 0 4px 8px rgba(0,123,255,0.3);
129
+ }
130
+
131
+ .nav-btn:disabled {
132
+ background: #6c757d;
133
+ border-color: #6c757d;
134
+ cursor: not-allowed;
135
+ opacity: 0.6;
136
+ }
137
+
138
+ .step-indicator {
139
+ display: flex;
140
+ align-items: center;
141
+ gap: 5px;
142
+ font-weight: 600;
143
+ color: #495057;
144
+ font-size: 16px;
145
+ }
146
+
147
+ .step-indicator span {
148
+ background: #e9ecef;
149
+ padding: 8px 12px;
150
+ border-radius: 20px;
151
+ min-width: 30px;
152
+ text-align: center;
153
+ }
154
+
155
+ .step-indicator #current-step {
156
+ background: #007bff;
157
+ color: white;
158
+ }
159
+
160
+ .tab-button {
161
+ flex: 1;
162
+ background: none;
163
+ border: none;
164
+ color: white;
165
+ padding: 1rem 1.5rem;
166
+ cursor: pointer;
167
+ font-size: 1rem;
168
+ font-weight: 500;
169
+ transition: all 0.3s ease;
170
+ border-bottom: 3px solid transparent;
171
+ min-width: 200px;
172
+ }
173
+
174
+ .tab-button:hover {
175
+ background: rgba(255, 255, 255, 0.1);
176
+ transform: translateY(-2px);
177
+ }
178
+
179
+ .tab-button.active {
180
+ background: #3498db;
181
+ border-bottom-color: #e74c3c;
182
+ transform: translateY(-2px);
183
+ }
184
+
185
+ .tab-button i {
186
+ margin-right: 0.5rem;
187
+ font-size: 1.1rem;
188
+ }
189
+
190
+ /* Main Content */
191
+ .main-content {
192
+ padding: 2rem;
193
+ min-height: 600px;
194
+ }
195
+
196
+ .tab-content {
197
+ display: none;
198
+ animation: fadeIn 0.5s ease-in-out;
199
+ }
200
+
201
+ .tab-content.active {
202
+ display: block;
203
+ }
204
+
205
+ @keyframes fadeIn {
206
+ from { opacity: 0; transform: translateY(20px); }
207
+ to { opacity: 1; transform: translateY(0); }
208
+ }
209
+
210
+ /* Card Styles */
211
+ .card {
212
+ background: white;
213
+ border-radius: 12px;
214
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
215
+ padding: 2rem;
216
+ margin-bottom: 2rem;
217
+ border: 1px solid #e1e8ed;
218
+ }
219
+
220
+ .card h2 {
221
+ color: #2c3e50;
222
+ margin-bottom: 1.5rem;
223
+ font-size: 1.8rem;
224
+ font-weight: 600;
225
+ border-bottom: 2px solid #3498db;
226
+ padding-bottom: 0.5rem;
227
+ }
228
+
229
+ .card h2 i {
230
+ margin-right: 0.5rem;
231
+ color: #3498db;
232
+ }
233
+
234
+ /* File Generation Note */
235
+ .file-generation-note {
236
+ background: #fff3cd;
237
+ border: 2px solid #ffc107;
238
+ border-left: 5px solid #ffc107;
239
+ border-radius: 8px;
240
+ padding: 1rem 1.5rem;
241
+ margin-bottom: 1.5rem;
242
+ display: flex;
243
+ align-items: flex-start;
244
+ gap: 1rem;
245
+ box-shadow: 0 2px 4px rgba(255, 193, 7, 0.1);
246
+ }
247
+
248
+ .file-generation-note i {
249
+ color: #856404;
250
+ font-size: 1.5rem;
251
+ margin-top: 0.2rem;
252
+ flex-shrink: 0;
253
+ }
254
+
255
+ .file-generation-note .note-content {
256
+ flex: 1;
257
+ color: #856404;
258
+ line-height: 1.6;
259
+ }
260
+
261
+ .file-generation-note .note-content p {
262
+ margin: 0;
263
+ font-size: 0.95rem;
264
+ }
265
+
266
+ .file-generation-note .note-content strong {
267
+ color: #664d03;
268
+ font-weight: 600;
269
+ }
270
+
271
+ /* Protein Loading Styles */
272
+ .input-methods {
273
+ display: grid;
274
+ grid-template-columns: 1fr auto 1fr;
275
+ gap: 2rem;
276
+ align-items: center;
277
+ margin-bottom: 2rem;
278
+ }
279
+
280
+ .method-option {
281
+ background: #f8f9fa;
282
+ padding: 1.5rem;
283
+ border-radius: 8px;
284
+ border: 2px dashed #dee2e6;
285
+ transition: all 0.3s ease;
286
+ }
287
+
288
+ .method-option:hover {
289
+ border-color: #3498db;
290
+ background: #f0f8ff;
291
+ }
292
+
293
+ .method-option h3 {
294
+ color: #2c3e50;
295
+ margin-bottom: 1rem;
296
+ font-size: 1.2rem;
297
+ }
298
+
299
+ .method-option h3 i {
300
+ margin-right: 0.5rem;
301
+ color: #3498db;
302
+ }
303
+
304
+ .divider {
305
+ text-align: center;
306
+ font-weight: bold;
307
+ color: #7f8c8d;
308
+ font-size: 1.1rem;
309
+ }
310
+
311
+ .divider::before,
312
+ .divider::after {
313
+ content: '';
314
+ display: inline-block;
315
+ width: 50px;
316
+ height: 2px;
317
+ background: #bdc3c7;
318
+ vertical-align: middle;
319
+ margin: 0 1rem;
320
+ }
321
+
322
+ /* File Upload Area */
323
+ .file-upload-area {
324
+ border: 2px dashed #3498db;
325
+ border-radius: 8px;
326
+ padding: 2rem;
327
+ text-align: center;
328
+ cursor: pointer;
329
+ transition: all 0.3s ease;
330
+ background: #f8f9fa;
331
+ }
332
+
333
+ .file-upload-area:hover {
334
+ background: #e3f2fd;
335
+ border-color: #2980b9;
336
+ }
337
+
338
+ .file-upload-area i {
339
+ font-size: 3rem;
340
+ color: #3498db;
341
+ margin-bottom: 1rem;
342
+ display: block;
343
+ }
344
+
345
+ .file-upload-area p {
346
+ margin-bottom: 1rem;
347
+ color: #7f8c8d;
348
+ }
349
+
350
+ .file-info {
351
+ background: #d4edda;
352
+ border: 1px solid #c3e6cb;
353
+ border-radius: 4px;
354
+ padding: 1rem;
355
+ margin-top: 1rem;
356
+ }
357
+
358
+ .file-info p {
359
+ margin: 0.25rem 0;
360
+ color: #155724;
361
+ }
362
+
363
+ /* PDB Fetch */
364
+ .pdb-fetch {
365
+ margin-top: 1rem;
366
+ }
367
+
368
+ .input-group {
369
+ display: flex;
370
+ gap: 1rem;
371
+ align-items: end;
372
+ }
373
+
374
+ .input-group label {
375
+ font-weight: 600;
376
+ color: #2c3e50;
377
+ margin-bottom: 0.5rem;
378
+ display: block;
379
+ }
380
+
381
+ .input-group input {
382
+ flex: 1;
383
+ padding: 0.75rem;
384
+ border: 2px solid #dee2e6;
385
+ border-radius: 4px;
386
+ font-size: 1rem;
387
+ transition: border-color 0.3s ease;
388
+ }
389
+
390
+ .input-group input:focus {
391
+ outline: none;
392
+ border-color: #3498db;
393
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
394
+ }
395
+
396
+ /* Status Messages */
397
+ .status-message {
398
+ margin-top: 1rem;
399
+ padding: 0.75rem;
400
+ border-radius: 4px;
401
+ font-weight: 500;
402
+ }
403
+
404
+ .status-message.success {
405
+ background: #d4edda;
406
+ color: #155724;
407
+ border: 1px solid #c3e6cb;
408
+ }
409
+
410
+ .status-message.error {
411
+ background: #f8d7da;
412
+ color: #721c24;
413
+ border: 1px solid #f5c6cb;
414
+ }
415
+
416
+ .status-message.info {
417
+ background: #d1ecf1;
418
+ color: #0c5460;
419
+ border: 1px solid #bee5eb;
420
+ }
421
+
422
+ /* Protein Preview */
423
+ .protein-preview {
424
+ margin-top: 2rem;
425
+ background: #f8f9fa;
426
+ border-radius: 8px;
427
+ padding: 1.5rem;
428
+ border: 1px solid #dee2e6;
429
+ }
430
+
431
+ .preview-content {
432
+ display: grid;
433
+ grid-template-columns: 1fr 1fr;
434
+ gap: 2rem;
435
+ margin-top: 1rem;
436
+ }
437
+
438
+ /* Structure comparison container - force side by side and centered */
439
+ .structure-comparison-container {
440
+ display: flex !important;
441
+ flex-direction: row !important;
442
+ width: 100% !important;
443
+ max-width: 1400px !important;
444
+ gap: 20px !important;
445
+ margin: 0 auto !important;
446
+ justify-content: center !important;
447
+ }
448
+
449
+ .structure-comparison-container .comparison-viewer {
450
+ flex: 0 1 48% !important;
451
+ min-width: 450px !important;
452
+ max-width: 48% !important;
453
+ }
454
+
455
+ /* Override preview-content for comparison view */
456
+ #completed-structure-preview .preview-content {
457
+ display: flex !important;
458
+ justify-content: center !important;
459
+ width: 100% !important;
460
+ padding: 0 !important;
461
+ grid-template-columns: none !important;
462
+ }
463
+
464
+ .protein-info p {
465
+ margin: 0.5rem 0;
466
+ font-size: 1rem;
467
+ }
468
+
469
+ .protein-visualization {
470
+ background: white;
471
+ border-radius: 4px;
472
+ padding: 1rem;
473
+ border: 1px solid #dee2e6;
474
+ min-height: 300px;
475
+ position: relative;
476
+ }
477
+
478
+ /* Ensure NGL viewer controls overlay within both original and prepared viewers */
479
+ .molecule-viewer {
480
+ position: relative;
481
+ }
482
+
483
+ #ngl-viewer {
484
+ border-radius: 4px;
485
+ background: #f8f9fa;
486
+ border: 1px solid #dee2e6;
487
+ }
488
+
489
+ .viewer-controls {
490
+ position: absolute;
491
+ top: 10px;
492
+ right: 10px;
493
+ display: flex;
494
+ gap: 0.5rem;
495
+ z-index: 10;
496
+ }
497
+
498
+ .viewer-controls .btn {
499
+ padding: 0.25rem 0.5rem;
500
+ font-size: 0.8rem;
501
+ border-radius: 3px;
502
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
503
+ }
504
+
505
+ .viewer-controls .btn:hover {
506
+ transform: translateY(-1px);
507
+ box-shadow: 0 4px 8px rgba(0,0,0,0.15);
508
+ }
509
+
510
+ /* Superimposed structure preview: controls inside the viewer box (parent has position: relative) */
511
+ #superimposed-molecule-viewer .viewer-controls {
512
+ position: absolute;
513
+ top: 10px;
514
+ right: 10px;
515
+ z-index: 10;
516
+ }
517
+
518
+ /* Simulation Parameters */
519
+ .params-grid {
520
+ display: grid;
521
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
522
+ gap: 2rem;
523
+ }
524
+
525
+ .param-section {
526
+ background: #f8f9fa;
527
+ padding: 1.5rem;
528
+ border-radius: 8px;
529
+ border: 1px solid #dee2e6;
530
+ }
531
+
532
+ .param-section h3 {
533
+ color: #2c3e50;
534
+ margin-bottom: 1rem;
535
+ font-size: 1.2rem;
536
+ border-bottom: 1px solid #bdc3c7;
537
+ padding-bottom: 0.5rem;
538
+ }
539
+
540
+ .param-section h3 i {
541
+ margin-right: 0.5rem;
542
+ color: #3498db;
543
+ }
544
+
545
+ .form-group {
546
+ margin-bottom: 1rem;
547
+ }
548
+
549
+ .form-group label {
550
+ display: block;
551
+ margin-bottom: 0.5rem;
552
+ font-weight: 600;
553
+ color: #2c3e50;
554
+ }
555
+
556
+ .form-group input,
557
+ .form-group select {
558
+ width: 100%;
559
+ padding: 0.75rem;
560
+ border: 2px solid #dee2e6;
561
+ border-radius: 4px;
562
+ font-size: 1rem;
563
+ transition: border-color 0.3s ease;
564
+ }
565
+
566
+ .form-group input:focus,
567
+ .form-group select:focus {
568
+ outline: none;
569
+ border-color: #3498db;
570
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
571
+ }
572
+
573
+ /* Simulation Steps */
574
+ .steps-container {
575
+ display: flex;
576
+ flex-direction: column;
577
+ gap: 1.5rem;
578
+ }
579
+
580
+ .step-item {
581
+ background: #f8f9fa;
582
+ border-radius: 8px;
583
+ border: 1px solid #dee2e6;
584
+ overflow: hidden;
585
+ transition: all 0.3s ease;
586
+ }
587
+
588
+ .step-item:hover {
589
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
590
+ transform: translateY(-2px);
591
+ }
592
+
593
+ .step-header {
594
+ display: flex;
595
+ justify-content: space-between;
596
+ align-items: center;
597
+ padding: 1.5rem;
598
+ background: #34495e;
599
+ color: white;
600
+ cursor: pointer;
601
+ }
602
+
603
+ .step-header h3 {
604
+ margin: 0;
605
+ font-size: 1.2rem;
606
+ font-weight: 600;
607
+ }
608
+
609
+ .step-header h3 i {
610
+ margin-right: 0.5rem;
611
+ color: #3498db;
612
+ }
613
+
614
+ .step-content {
615
+ padding: 1.5rem;
616
+ background: white;
617
+ display: none;
618
+ }
619
+
620
+ .step-content.active {
621
+ display: block;
622
+ }
623
+
624
+ .form-row {
625
+ display: grid;
626
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
627
+ gap: 1rem;
628
+ }
629
+
630
+ /* Toggle Switch */
631
+ .switch {
632
+ position: relative;
633
+ display: inline-block;
634
+ width: 60px;
635
+ height: 34px;
636
+ }
637
+
638
+ .switch input {
639
+ opacity: 0;
640
+ width: 0;
641
+ height: 0;
642
+ }
643
+
644
+ .slider {
645
+ position: absolute;
646
+ cursor: pointer;
647
+ top: 0;
648
+ left: 0;
649
+ right: 0;
650
+ bottom: 0;
651
+ background-color: #ccc;
652
+ transition: .4s;
653
+ border-radius: 34px;
654
+ }
655
+
656
+ .slider:before {
657
+ position: absolute;
658
+ content: "";
659
+ height: 26px;
660
+ width: 26px;
661
+ left: 4px;
662
+ bottom: 4px;
663
+ background-color: white;
664
+ transition: .4s;
665
+ border-radius: 50%;
666
+ }
667
+
668
+ input:checked + .slider {
669
+ background-color: #3498db;
670
+ }
671
+
672
+ input:checked + .slider:before {
673
+ transform: translateX(26px);
674
+ }
675
+
676
+ /* File Generation */
677
+ .generation-controls {
678
+ display: flex;
679
+ gap: 1rem;
680
+ margin-bottom: 2rem;
681
+ flex-wrap: wrap;
682
+ }
683
+
684
+ .files-preview {
685
+ margin-bottom: 2rem;
686
+ }
687
+
688
+ .files-list {
689
+ display: grid;
690
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
691
+ gap: 1rem;
692
+ margin-top: 1rem;
693
+ }
694
+
695
+ .file-item {
696
+ background: #f8f9fa;
697
+ border: 1px solid #dee2e6;
698
+ border-radius: 8px;
699
+ padding: 1rem;
700
+ transition: all 0.3s ease;
701
+ }
702
+
703
+ .file-item:hover {
704
+ background: #e3f2fd;
705
+ border-color: #3498db;
706
+ transform: translateY(-2px);
707
+ }
708
+
709
+ .file-item h4 {
710
+ color: #2c3e50;
711
+ margin-bottom: 0.5rem;
712
+ font-size: 1.1rem;
713
+ }
714
+
715
+ .file-item p {
716
+ color: #7f8c8d;
717
+ font-size: 0.9rem;
718
+ margin: 0.25rem 0;
719
+ }
720
+
721
+ .download-section {
722
+ margin-bottom: 2rem;
723
+ }
724
+
725
+ .download-options {
726
+ display: flex;
727
+ gap: 1rem;
728
+ flex-wrap: wrap;
729
+ margin-top: 1rem;
730
+ }
731
+
732
+ .simulation-summary {
733
+ background: #f8f9fa;
734
+ border-radius: 8px;
735
+ padding: 1.5rem;
736
+ border: 1px solid #dee2e6;
737
+ }
738
+
739
+ .summary-content {
740
+ display: grid;
741
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
742
+ gap: 1rem;
743
+ margin-top: 1rem;
744
+ }
745
+
746
+ .summary-item {
747
+ background: white;
748
+ padding: 1rem;
749
+ border-radius: 4px;
750
+ border: 1px solid #dee2e6;
751
+ }
752
+
753
+ .summary-item h4 {
754
+ color: #2c3e50;
755
+ margin-bottom: 0.5rem;
756
+ font-size: 1rem;
757
+ }
758
+
759
+ .summary-item p {
760
+ color: #7f8c8d;
761
+ margin: 0.25rem 0;
762
+ }
763
+
764
+ /* Checkbox Group Styles */
765
+ .checkbox-group {
766
+ display: flex;
767
+ gap: 1rem;
768
+ margin-top: 0.5rem;
769
+ }
770
+
771
+ .checkbox-container {
772
+ display: flex;
773
+ align-items: center;
774
+ cursor: pointer;
775
+ font-size: 0.9rem;
776
+ color: #495057;
777
+ }
778
+
779
+ .checkbox-container input[type="checkbox"] {
780
+ margin-right: 0.5rem;
781
+ transform: scale(1.2);
782
+ }
783
+
784
+ .checkbox-container:hover {
785
+ color: #007bff;
786
+ }
787
+
788
+ /* Ion Controls */
789
+ .ion-controls {
790
+ display: flex;
791
+ gap: 0.5rem;
792
+ align-items: center;
793
+ }
794
+
795
+ .ion-controls select {
796
+ flex: 1;
797
+ }
798
+
799
+
800
+
801
+
802
+ #ligand-forcefield-section {
803
+ transition: all 0.3s ease;
804
+ }
805
+
806
+ #ligand-forcefield-section.disabled {
807
+ opacity: 0.5;
808
+ pointer-events: none;
809
+ }
810
+
811
+ /* Tooltip styling for better text wrapping */
812
+ .tooltip {
813
+ max-width: 300px;
814
+ }
815
+
816
+ .tooltip-inner {
817
+ text-align: left;
818
+ white-space: normal;
819
+ word-wrap: break-word;
820
+ }
821
+
822
+ /* Button Styles */
823
+ .btn {
824
+ display: inline-flex;
825
+ align-items: center;
826
+ padding: 0.75rem 1.5rem;
827
+ border: none;
828
+ border-radius: 6px;
829
+ font-size: 1rem;
830
+ font-weight: 600;
831
+ cursor: pointer;
832
+ transition: all 0.3s ease;
833
+ text-decoration: none;
834
+ gap: 0.5rem;
835
+ }
836
+
837
+ .btn:hover {
838
+ transform: translateY(-2px);
839
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
840
+ }
841
+
842
+ .btn:active {
843
+ transform: translateY(0);
844
+ }
845
+
846
+ .btn-primary {
847
+ background: linear-gradient(135deg, #3498db, #2980b9);
848
+ color: white;
849
+ }
850
+
851
+ .btn-primary:hover {
852
+ background: linear-gradient(135deg, #2980b9, #1f618d);
853
+ }
854
+
855
+ .btn-secondary {
856
+ background: linear-gradient(135deg, #95a5a6, #7f8c8d);
857
+ color: white;
858
+ }
859
+
860
+ .btn-secondary:hover {
861
+ background: linear-gradient(135deg, #7f8c8d, #6c7b7d);
862
+ }
863
+
864
+ .btn-success {
865
+ background: linear-gradient(135deg, #27ae60, #229954);
866
+ color: white;
867
+ }
868
+
869
+ .btn-success:hover {
870
+ background: linear-gradient(135deg, #229954, #1e8449);
871
+ }
872
+
873
+ .btn-info {
874
+ background: linear-gradient(135deg, #17a2b8, #138496);
875
+ color: white;
876
+ }
877
+
878
+ .btn-info:hover {
879
+ background: linear-gradient(135deg, #138496, #117a8b);
880
+ }
881
+
882
+ .btn i {
883
+ font-size: 1rem;
884
+ }
885
+
886
+ /* Footer */
887
+ .footer {
888
+ background: #2c3e50;
889
+ color: white;
890
+ text-align: center;
891
+ padding: 2rem;
892
+ margin-top: 2rem;
893
+ }
894
+
895
+ .footer p {
896
+ margin: 0;
897
+ opacity: 0.8;
898
+ }
899
+
900
+ /* Responsive Design */
901
+ @media (max-width: 768px) {
902
+ .container {
903
+ margin: 0;
904
+ box-shadow: none;
905
+ }
906
+
907
+ .header-content h1 {
908
+ font-size: 2rem;
909
+ }
910
+
911
+ .tab-navigation {
912
+ flex-direction: column;
913
+ }
914
+
915
+ .tab-button {
916
+ min-width: auto;
917
+ border-bottom: 1px solid #2c3e50;
918
+ }
919
+
920
+ .main-content {
921
+ padding: 1rem;
922
+ }
923
+
924
+ .input-methods {
925
+ grid-template-columns: 1fr;
926
+ }
927
+
928
+ .divider {
929
+ order: 2;
930
+ }
931
+
932
+ .preview-content {
933
+ grid-template-columns: 1fr;
934
+ }
935
+
936
+ .params-grid {
937
+ grid-template-columns: 1fr;
938
+ }
939
+
940
+ .form-row {
941
+ grid-template-columns: 1fr;
942
+ }
943
+
944
+ .generation-controls {
945
+ flex-direction: column;
946
+ }
947
+
948
+ .download-options {
949
+ flex-direction: column;
950
+ }
951
+
952
+ .summary-content {
953
+ grid-template-columns: 1fr;
954
+ }
955
+ }
956
+
957
+ @media (max-width: 480px) {
958
+ .header-content h1 {
959
+ font-size: 1.5rem;
960
+ }
961
+
962
+ .card {
963
+ padding: 1rem;
964
+ }
965
+
966
+ .card h2 {
967
+ font-size: 1.5rem;
968
+ }
969
+
970
+ .btn {
971
+ padding: 0.5rem 1rem;
972
+ font-size: 0.9rem;
973
+ }
974
+ }
975
+
976
+ /* Loading Animation */
977
+ .loading {
978
+ display: inline-block;
979
+ width: 20px;
980
+ height: 20px;
981
+ border: 3px solid #f3f3f3;
982
+ border-top: 3px solid #3498db;
983
+ border-radius: 50%;
984
+ animation: spin 1s linear infinite;
985
+ }
986
+
987
+ @keyframes spin {
988
+ 0% { transform: rotate(0deg); }
989
+ 100% { transform: rotate(360deg); }
990
+ }
991
+
992
+ /* Structure Preparation Styles */
993
+ .card-description {
994
+ color: #7f8c8d;
995
+ margin-bottom: 2rem;
996
+ font-style: italic;
997
+ }
998
+
999
+ .prep-sections {
1000
+ display: grid;
1001
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
1002
+ gap: 2rem;
1003
+ margin-bottom: 2rem;
1004
+ }
1005
+
1006
+ .prep-section {
1007
+ background: #f8f9fa;
1008
+ border-radius: 8px;
1009
+ padding: 1.5rem;
1010
+ border: 1px solid #dee2e6;
1011
+ }
1012
+
1013
+ .prep-section h3 {
1014
+ color: #2c3e50;
1015
+ margin-bottom: 1rem;
1016
+ font-size: 1.2rem;
1017
+ border-bottom: 1px solid #bdc3c7;
1018
+ padding-bottom: 0.5rem;
1019
+ }
1020
+
1021
+ .prep-section h3 i {
1022
+ margin-right: 0.5rem;
1023
+ color: #3498db;
1024
+ }
1025
+
1026
+ .prep-section-fullwidth {
1027
+ width: 100%;
1028
+ margin-top: 1rem;
1029
+ margin-bottom: 2rem;
1030
+ }
1031
+
1032
+ .prep-options {
1033
+ display: flex;
1034
+ flex-direction: column;
1035
+ gap: 1rem;
1036
+ }
1037
+
1038
+ .prep-option {
1039
+ background: white;
1040
+ border-radius: 6px;
1041
+ padding: 1rem;
1042
+ border: 1px solid #e1e8ed;
1043
+ transition: all 0.3s ease;
1044
+ }
1045
+
1046
+ .prep-option:hover {
1047
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
1048
+ transform: translateY(-1px);
1049
+ }
1050
+
1051
+ .checkbox-container {
1052
+ display: flex;
1053
+ align-items: center;
1054
+ cursor: pointer;
1055
+ font-weight: 600;
1056
+ color: #2c3e50;
1057
+ margin-bottom: 0.5rem;
1058
+ }
1059
+
1060
+ .checkbox-container input[type="checkbox"] {
1061
+ margin-right: 0.75rem;
1062
+ transform: scale(1.2);
1063
+ }
1064
+
1065
+ .option-description {
1066
+ color: #7f8c8d;
1067
+ font-size: 0.9rem;
1068
+ margin: 0;
1069
+ margin-left: 1.5rem;
1070
+ }
1071
+
1072
+ .prep-actions {
1073
+ display: flex;
1074
+ gap: 1rem;
1075
+ margin-bottom: 2rem;
1076
+ flex-wrap: wrap;
1077
+ }
1078
+
1079
+ .prep-status {
1080
+ background: #e8f5e8;
1081
+ border: 1px solid #c3e6cb;
1082
+ border-radius: 8px;
1083
+ padding: 1.5rem;
1084
+ margin-bottom: 2rem;
1085
+ }
1086
+
1087
+ .prep-status h3 {
1088
+ color: #155724;
1089
+ margin-bottom: 1rem;
1090
+ }
1091
+
1092
+ .status-content {
1093
+ color: #155724;
1094
+ }
1095
+
1096
+ .prepared-structure-preview {
1097
+ background: #f8f9fa;
1098
+ border-radius: 8px;
1099
+ padding: 1.5rem;
1100
+ border: 1px solid #dee2e6;
1101
+ margin-top: 2rem;
1102
+ width: 100%;
1103
+ box-sizing: border-box;
1104
+ }
1105
+
1106
+ #sequence-viewer-section {
1107
+ width: 100%;
1108
+ box-sizing: border-box;
1109
+ }
1110
+
1111
+ .prepared-structure-preview h3 {
1112
+ color: #2c3e50;
1113
+ margin-bottom: 1rem;
1114
+ font-size: 1.2rem;
1115
+ }
1116
+
1117
+ .structure-info p {
1118
+ margin: 0.5rem 0;
1119
+ font-size: 1rem;
1120
+ }
1121
+
1122
+ .structure-visualization {
1123
+ margin-top: 1rem;
1124
+ }
1125
+
1126
+ #prepared-ngl-viewer {
1127
+ border-radius: 4px;
1128
+ background: #f8f9fa;
1129
+ border: 1px solid #dee2e6;
1130
+ }
1131
+
1132
+ /* Custom Checkbox Styles */
1133
+ .checkbox-container input[type="checkbox"] {
1134
+ appearance: none;
1135
+ width: 20px;
1136
+ height: 20px;
1137
+ border: 2px solid #bdc3c7;
1138
+ border-radius: 4px;
1139
+ background: white;
1140
+ position: relative;
1141
+ cursor: pointer;
1142
+ transition: all 0.3s ease;
1143
+ }
1144
+
1145
+ .checkbox-container input[type="checkbox"]:checked {
1146
+ background: #3498db;
1147
+ border-color: #3498db;
1148
+ }
1149
+
1150
+ .checkbox-container input[type="checkbox"]:checked::after {
1151
+ content: '✓';
1152
+ position: absolute;
1153
+ top: 50%;
1154
+ left: 50%;
1155
+ transform: translate(-50%, -50%);
1156
+ color: white;
1157
+ font-weight: bold;
1158
+ font-size: 14px;
1159
+ }
1160
+
1161
+ .checkbox-container input[type="checkbox"]:hover {
1162
+ border-color: #3498db;
1163
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
1164
+ }
1165
+
1166
+ /* Responsive Design for Structure Prep */
1167
+ @media (max-width: 768px) {
1168
+ .prep-sections {
1169
+ grid-template-columns: 1fr;
1170
+ }
1171
+
1172
+ .prep-actions {
1173
+ flex-direction: column;
1174
+ }
1175
+
1176
+ .prep-option {
1177
+ padding: 0.75rem;
1178
+ }
1179
+
1180
+ .option-description {
1181
+ margin-left: 1.25rem;
1182
+ }
1183
+ }
1184
+
1185
+ /* Utility Classes */
1186
+ .text-center { text-align: center; }
1187
+ .text-left { text-align: left; }
1188
+ .text-right { text-align: right; }
1189
+ .mt-1 { margin-top: 0.5rem; }
1190
+ .mt-2 { margin-top: 1rem; }
1191
+ .mt-3 { margin-top: 1.5rem; }
1192
+ .mb-1 { margin-bottom: 0.5rem; }
1193
+ .mb-2 { margin-bottom: 1rem; }
1194
+ .mb-3 { margin-bottom: 1.5rem; }
1195
+ .p-1 { padding: 0.5rem; }
1196
+ .p-2 { padding: 1rem; }
1197
+ .p-3 { padding: 1.5rem; }
1198
+
1199
+ /* Missing Residues Horizontal Display */
1200
+ .missing-residues-horizontal {
1201
+ display: inline;
1202
+ color: #155724;
1203
+ font-weight: bold;
1204
+ font-size: 0.95rem;
1205
+ word-wrap: break-word;
1206
+ white-space: normal;
1207
+ line-height: 1.6;
1208
+ margin: 0;
1209
+ padding: 0;
1210
+ }
1211
+
1212
+ .chain-missing-info {
1213
+ margin-bottom: 1.5rem;
1214
+ }
1215
+
1216
+ .chain-missing-info h4 {
1217
+ color: #155724;
1218
+ margin-bottom: 0.5rem;
1219
+ }
1220
+
1221
+ /* Sequence Viewer Styles */
1222
+ .sequence-viewer-container {
1223
+ background: #ffffff;
1224
+ border: 1px solid #dee2e6;
1225
+ border-radius: 8px;
1226
+ padding: 1.5rem;
1227
+ max-height: 600px;
1228
+ overflow-y: auto;
1229
+ font-family: 'Courier New', monospace;
1230
+ width: 100%;
1231
+ box-sizing: border-box;
1232
+ }
1233
+
1234
+ .sequence-chain-container {
1235
+ margin-bottom: 2rem;
1236
+ border-bottom: 2px solid #e9ecef;
1237
+ padding-bottom: 1.5rem;
1238
+ }
1239
+
1240
+ .sequence-chain-container:last-child {
1241
+ border-bottom: none;
1242
+ margin-bottom: 0;
1243
+ }
1244
+
1245
+ .sequence-chain-header {
1246
+ margin-bottom: 1rem;
1247
+ }
1248
+
1249
+ .sequence-chain-header h4 {
1250
+ margin: 0;
1251
+ font-size: 1.1rem;
1252
+ font-weight: 600;
1253
+ }
1254
+
1255
+ .sequence-display {
1256
+ background: #f8f9fa;
1257
+ border: 1px solid #dee2e6;
1258
+ border-radius: 4px;
1259
+ padding: 1rem;
1260
+ font-size: 14px;
1261
+ line-height: 1.8;
1262
+ width: 100%;
1263
+ box-sizing: border-box;
1264
+ }
1265
+
1266
+ .sequence-line {
1267
+ display: flex;
1268
+ margin-bottom: 2px;
1269
+ white-space: nowrap;
1270
+ }
1271
+
1272
+ .sequence-line-number {
1273
+ color: #6c757d;
1274
+ margin-right: 1rem;
1275
+ min-width: 60px;
1276
+ text-align: right;
1277
+ font-size: 12px;
1278
+ user-select: none;
1279
+ }
1280
+
1281
+ .sequence-characters {
1282
+ letter-spacing: 2px;
1283
+ word-spacing: 0;
1284
+ flex: 1;
1285
+ overflow-x: auto;
1286
+ min-width: 0;
1287
+ }
1288
+
1289
+ .sequence-char {
1290
+ display: inline-block;
1291
+ padding: 2px 1px;
1292
+ transition: background-color 0.2s;
1293
+ }
1294
+
1295
+ .sequence-char:hover {
1296
+ background-color: rgba(0, 0, 0, 0.1);
1297
+ border-radius: 2px;
1298
+ }
1299
+
1300
+ /* Trim Residues Section */
1301
+ .trim-info-box {
1302
+ background: #e7f3ff;
1303
+ border: 1px solid #b3d9ff;
1304
+ border-radius: 6px;
1305
+ padding: 1rem;
1306
+ margin-top: 1rem;
1307
+ margin-bottom: 1rem;
1308
+ color: #004085;
1309
+ font-size: 0.9rem;
1310
+ line-height: 1.6;
1311
+ }
1312
+
1313
+ .trim-info-box i {
1314
+ color: #0066cc;
1315
+ margin-right: 0.5rem;
1316
+ }
1317
+
1318
+ .trim-info-box strong {
1319
+ color: #003366;
1320
+ }
1321
+
1322
+ .trim-residues-container {
1323
+ margin-top: 1rem;
1324
+ display: grid;
1325
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
1326
+ gap: 1rem;
1327
+ }
1328
+
1329
+ .trim-chain-controls {
1330
+ background: #f8f9fa;
1331
+ border: 1px solid #dee2e6;
1332
+ border-radius: 6px;
1333
+ padding: 1rem;
1334
+ }
1335
+
1336
+ .trim-chain-controls h5 {
1337
+ color: #2c3e50;
1338
+ margin-bottom: 0.75rem;
1339
+ font-size: 1rem;
1340
+ }
1341
+
1342
+ .trim-inputs {
1343
+ display: flex;
1344
+ gap: 1.5rem;
1345
+ align-items: center;
1346
+ flex-wrap: wrap;
1347
+ }
1348
+
1349
+ .trim-input-group {
1350
+ display: flex;
1351
+ align-items: center;
1352
+ gap: 0.5rem;
1353
+ }
1354
+
1355
+ .trim-input-group label {
1356
+ font-weight: 600;
1357
+ color: #495057;
1358
+ font-size: 0.9rem;
1359
+ min-width: 100px;
1360
+ }
1361
+
1362
+ .trim-limit {
1363
+ font-weight: normal;
1364
+ color: #6c757d;
1365
+ font-size: 0.85rem;
1366
+ font-style: italic;
1367
+ }
1368
+
1369
+ .trim-input-group input[type="number"] {
1370
+ width: 80px;
1371
+ padding: 0.5rem;
1372
+ border: 1px solid #ced4da;
1373
+ border-radius: 4px;
1374
+ font-size: 0.9rem;
1375
+ }
1376
+
1377
+ .trim-input-group input[type="number"]:focus {
1378
+ outline: none;
1379
+ border-color: #3498db;
1380
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
1381
+ }
1382
+
1383
+ .trim-info {
1384
+ font-size: 0.85rem;
1385
+ color: #6c757d;
1386
+ margin-top: 0.5rem;
1387
+ font-style: italic;
1388
+ }
1389
+
1390
+ /* Log Modal Styles */
1391
+ .log-modal {
1392
+ display: none;
1393
+ position: fixed;
1394
+ z-index: 10000;
1395
+ left: 0;
1396
+ top: 0;
1397
+ width: 100%;
1398
+ height: 100%;
1399
+ background-color: rgba(0, 0, 0, 0.5);
1400
+ animation: fadeIn 0.3s;
1401
+ }
1402
+
1403
+ @keyframes fadeIn {
1404
+ from { opacity: 0; }
1405
+ to { opacity: 1; }
1406
+ }
1407
+
1408
+ .log-modal-content {
1409
+ background-color: #fefefe;
1410
+ margin: 2% auto;
1411
+ padding: 0;
1412
+ border: 1px solid #888;
1413
+ border-radius: 8px;
1414
+ width: 90%;
1415
+ max-width: 900px;
1416
+ max-height: 90vh;
1417
+ display: flex;
1418
+ flex-direction: column;
1419
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
1420
+ animation: slideDown 0.3s;
1421
+ }
1422
+
1423
+ @keyframes slideDown {
1424
+ from {
1425
+ transform: translateY(-50px);
1426
+ opacity: 0;
1427
+ }
1428
+ to {
1429
+ transform: translateY(0);
1430
+ opacity: 1;
1431
+ }
1432
+ }
1433
+
1434
+ .log-modal-header {
1435
+ background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
1436
+ color: white;
1437
+ padding: 1rem 1.5rem;
1438
+ display: flex;
1439
+ justify-content: space-between;
1440
+ align-items: center;
1441
+ border-radius: 8px 8px 0 0;
1442
+ }
1443
+
1444
+ .log-modal-header h3 {
1445
+ margin: 0;
1446
+ font-size: 1.3rem;
1447
+ font-weight: 500;
1448
+ }
1449
+
1450
+ .log-modal-close {
1451
+ color: white;
1452
+ font-size: 2rem;
1453
+ font-weight: bold;
1454
+ background: none;
1455
+ border: none;
1456
+ cursor: pointer;
1457
+ padding: 0;
1458
+ width: 30px;
1459
+ height: 30px;
1460
+ display: flex;
1461
+ align-items: center;
1462
+ justify-content: center;
1463
+ border-radius: 50%;
1464
+ transition: background-color 0.2s;
1465
+ }
1466
+
1467
+ .log-modal-close:hover {
1468
+ background-color: rgba(255, 255, 255, 0.2);
1469
+ }
1470
+
1471
+ .log-container {
1472
+ flex: 1;
1473
+ overflow-y: auto;
1474
+ padding: 1rem;
1475
+ background-color: #1e1e1e;
1476
+ color: #d4d4d4;
1477
+ font-family: 'Courier New', monospace;
1478
+ font-size: 0.9rem;
1479
+ line-height: 1.6;
1480
+ max-height: calc(90vh - 80px);
1481
+ }
1482
+
1483
+ .log-content {
1484
+ min-height: 100%;
1485
+ }
1486
+
1487
+ .log-line {
1488
+ padding: 0.3rem 0;
1489
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1490
+ word-wrap: break-word;
1491
+ }
1492
+
1493
+ .log-line:last-child {
1494
+ border-bottom: none;
1495
+ }
1496
+
1497
+ .log-time {
1498
+ color: #858585;
1499
+ margin-right: 0.5rem;
1500
+ }
1501
+
1502
+ .log-icon {
1503
+ margin-right: 0.5rem;
1504
+ }
1505
+
1506
+ .log-message {
1507
+ color: #d4d4d4;
1508
+ }
1509
+
1510
+ .log-info .log-message {
1511
+ color: #d4d4d4;
1512
+ }
1513
+
1514
+ .log-success .log-message {
1515
+ color: #4ec9b0;
1516
+ }
1517
+
1518
+ .log-warning .log-message {
1519
+ color: #dcdcaa;
1520
+ }
1521
+
1522
+ .log-error .log-message {
1523
+ color: #f48771;
1524
+ }
1525
+
1526
+ .log-result {
1527
+ margin-top: 1.5rem;
1528
+ padding: 1rem;
1529
+ background-color: #252526;
1530
+ border-left: 4px solid #4ec9b0;
1531
+ border-radius: 4px;
1532
+ }
1533
+
1534
+ .log-result h4 {
1535
+ color: #4ec9b0;
1536
+ margin-bottom: 0.5rem;
1537
+ }
1538
+
1539
+ .log-result p {
1540
+ color: #d4d4d4;
1541
+ margin: 0.5rem 0;
1542
+ }
1543
+
1544
+ .log-result ul {
1545
+ margin: 0.5rem 0;
1546
+ padding-left: 1.5rem;
1547
+ color: #d4d4d4;
1548
+ }
1549
+
1550
+ .log-result li {
1551
+ margin: 0.5rem 0;
1552
+ }
1553
+
1554
+ /* File Editor Modal Styles */
1555
+ #file-content-modal {
1556
+ animation: fadeIn 0.3s;
1557
+ }
1558
+
1559
+ #file-content-modal > div {
1560
+ animation: slideDown 0.3s;
1561
+ }
1562
+
1563
+ #modal-content-edit {
1564
+ line-height: 1.6;
1565
+ tab-size: 4;
1566
+ -moz-tab-size: 4;
1567
+ }
1568
+
1569
+ #modal-content-edit:focus {
1570
+ outline: none;
1571
+ border-color: #0056b3;
1572
+ box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
1573
+ }
1574
+
1575
+ #save-status {
1576
+ font-weight: 500;
1577
+ animation: slideUp 0.3s;
1578
+ }
1579
+
1580
+ @keyframes slideUp {
1581
+ from {
1582
+ opacity: 0;
1583
+ transform: translateY(10px);
1584
+ }
1585
+ to {
1586
+ opacity: 1;
1587
+ transform: translateY(0);
1588
+ }
1589
+ }
1590
+
1591
+ #edit-file-btn:hover {
1592
+ background: #0056b3 !important;
1593
+ }
1594
+
1595
+ #save-file-btn:hover {
1596
+ background: #218838 !important;
1597
+ }
1598
+
1599
+ #cancel-edit-btn:hover {
1600
+ background: #5a6268 !important;
1601
+ }
1602
+
1603
+ /* Frozen/Disabled Checkboxes - Gray only the checkbox, keep text bold and normal */
1604
+ .checkbox-container input[type="checkbox"]:disabled {
1605
+ opacity: 0.5 !important;
1606
+ cursor: not-allowed !important;
1607
+ pointer-events: none !important;
1608
+ -webkit-appearance: none !important;
1609
+ appearance: none !important;
1610
+ }
1611
+
1612
+ .checkbox-container input[type="checkbox"]:disabled + .checkmark {
1613
+ opacity: 0.5 !important;
1614
+ }
1615
+
1616
+ /* Keep text labels bold and normal color when checkbox is disabled */
1617
+ .checkbox-container:has(input[type="checkbox"]:disabled) {
1618
+ cursor: not-allowed !important;
1619
+ pointer-events: none !important;
1620
+ opacity: 1 !important;
1621
+ color: #2c3e50 !important;
1622
+ font-weight: 600 !important;
1623
+ }
1624
+
1625
+ .checkbox-container:has(input[type="checkbox"]:disabled):hover {
1626
+ color: #2c3e50 !important;
1627
+ }
1628
+
1629
+ /* Prevent interaction on disabled checkbox inputs */
1630
+ .checkbox-container input[type="checkbox"]:disabled {
1631
+ cursor: not-allowed !important;
1632
+ pointer-events: none !important;
1633
+ opacity: 0.5 !important;
1634
+ }
1635
+
1636
+ /* Gray out the checkmark for disabled checkboxes */
1637
+ .checkbox-container input[type="checkbox"]:disabled:checked {
1638
+ background-color: #6c757d !important;
1639
+ border-color: #6c757d !important;
1640
+ }
1641
+
1642
+ .checkbox-container input[type="checkbox"]:disabled:checked::after {
1643
+ color: #ffffff !important;
1644
+ }
1645
+
1646
+ /* Frozen/Disabled Toggle Switches - Gray only the toggle, keep it hidden */
1647
+ .switch input[type="checkbox"]:disabled {
1648
+ opacity: 0 !important; /* Keep the input completely hidden */
1649
+ cursor: not-allowed !important;
1650
+ pointer-events: none !important;
1651
+ width: 0 !important;
1652
+ height: 0 !important;
1653
+ }
1654
+
1655
+ .switch input[type="checkbox"]:disabled + .slider {
1656
+ opacity: 0.5 !important;
1657
+ cursor: not-allowed !important;
1658
+ pointer-events: none !important;
1659
+ background-color: #95a5a6 !important; /* Gray color for disabled toggles */
1660
+ }
1661
+
1662
+ .switch input[type="checkbox"]:disabled:checked + .slider {
1663
+ background-color: #95a5a6 !important; /* Gray color even when checked */
1664
+ }
1665
+
1666
+ .switch input[type="checkbox"]:disabled:not(:checked) + .slider {
1667
+ background-color: #bdc3c7 !important; /* Lighter gray when unchecked */
1668
+ }
1669
+
1670
+ .switch:has(input[type="checkbox"]:disabled) {
1671
+ cursor: not-allowed !important;
1672
+ pointer-events: none !important;
1673
+ opacity: 1 !important; /* Keep switch container visible */
1674
+ }
1675
+
1676
+ .switch:has(input[type="checkbox"]:disabled) .slider {
1677
+ cursor: not-allowed !important;
1678
+ pointer-events: none !important;
1679
+ }
1680
+
1681
+ /* Ensure disabled switch input stays completely hidden */
1682
+ .switch input[type="checkbox"]:disabled {
1683
+ position: absolute !important;
1684
+ opacity: 0 !important;
1685
+ width: 0 !important;
1686
+ height: 0 !important;
1687
+ margin: 0 !important;
1688
+ padding: 0 !important;
1689
+ }
1690
+
1691
+ /* Docking Section Collapsible Styles */
1692
+ #docking-section.collapsed .section-description,
1693
+ #docking-section.collapsed .custom-plumed-section {
1694
+ max-height: 0;
1695
+ opacity: 0;
1696
+ margin-top: 0;
1697
+ margin-bottom: 0;
1698
+ padding-top: 0;
1699
+ padding-bottom: 0;
1700
+ overflow: hidden;
1701
+ transition: max-height 0.4s ease, opacity 0.3s ease, margin 0.3s ease, padding 0.3s ease;
1702
+ }
1703
+
1704
+ #docking-section.collapsed .plumed-toggle-header {
1705
+ margin-bottom: 0;
1706
+ }
1707
+
1708
+ .docking-box-row {
1709
+ display: flex;
1710
+ gap: 10px;
1711
+ margin-bottom: 10px;
1712
+ }
1713
+
1714
+ .docking-box-row .form-group {
1715
+ flex: 1;
1716
+ }
1717
+
1718
+ .docking-setup-entry {
1719
+ margin-bottom: 15px;
1720
+ padding: 10px;
1721
+ background: white;
1722
+ border-radius: 5px;
1723
+ border: 1px solid #dee2e6;
1724
+ }
1725
+
1726
+ .docking-setup-header {
1727
+ display: flex;
1728
+ justify-content: space-between;
1729
+ align-items: center;
1730
+ margin-bottom: 10px;
1731
+ }
1732
+
1733
+ .docking-setup-chains {
1734
+ font-size: 0.9em;
1735
+ color: #6c757d;
1736
+ }
1737
+
1738
+ /* Docking Ligand Selection Row Styles */
1739
+ .docking-ligand-row {
1740
+ transition: all 0.2s ease;
1741
+ }
1742
+
1743
+ .docking-ligand-row:hover {
1744
+ background: #e9ecef !important;
1745
+ border-color: #6f42c1 !important;
1746
+ }
1747
+
1748
+ .docking-ligand-row .checkbox-container {
1749
+ display: flex;
1750
+ align-items: center;
1751
+ gap: 8px;
1752
+ }
1753
+
1754
+ /* Docking Box Controls Compact Layout */
1755
+ #docking-setup-list {
1756
+ display: flex;
1757
+ flex-wrap: wrap;
1758
+ gap: 15px;
1759
+ }
1760
+
1761
+ .docking-setup-entry {
1762
+ transition: all 0.2s ease;
1763
+ }
1764
+
1765
+ .docking-setup-entry:hover {
1766
+ border-color: #6f42c1 !important;
1767
+ box-shadow: 0 2px 8px rgba(111, 66, 193, 0.15);
1768
+ }
1769
+
1770
+ .docking-setup-entry input[type="number"] {
1771
+ transition: border-color 0.2s ease;
1772
+ }
1773
+
1774
+ .docking-setup-entry input[type="number"]:focus {
1775
+ border-color: #6f42c1;
1776
+ outline: none;
1777
+ box-shadow: 0 0 0 2px rgba(111, 66, 193, 0.1);
1778
+ }
1779
+
1780
+ /* Small checkmarks for chain selection */
1781
+ .docking-ligand-row .checkmark {
1782
+ width: 16px !important;
1783
+ height: 16px !important;
1784
+ }
1785
+
1786
+ .docking-ligand-row .checkmark:after {
1787
+ left: 5px !important;
1788
+ top: 2px !important;
1789
+ width: 4px !important;
1790
+ height: 8px !important;
1791
+ }
1792
+
1793
+
1794
+ /* Docking Setup Collapsible Section */
1795
+ .docking-setup-collapsible {
1796
+ transition: all 0.3s ease;
1797
+ }
1798
+
1799
+ .docking-setup-header {
1800
+ user-select: none;
1801
+ transition: background 0.2s ease;
1802
+ }
1803
+
1804
+ .docking-setup-header:hover {
1805
+ background: linear-gradient(135deg, #5a31a8 0%, #7d4bc7 100%) !important;
1806
+ }
1807
+
1808
+ .docking-setup-content {
1809
+ transition: all 0.3s ease;
1810
+ }
1811
+
1812
+ #docking-setup-toggle-icon {
1813
+ transition: transform 0.3s ease;
1814
+ }
1815
+
1816
+ /* Docking Poses Viewer Styles */
1817
+ .docking-ligand-tabs {
1818
+ display: flex;
1819
+ gap: 8px;
1820
+ margin-bottom: 15px;
1821
+ flex-wrap: wrap;
1822
+ }
1823
+
1824
+ .docking-ligand-tab {
1825
+ padding: 8px 16px;
1826
+ border: 2px solid #dee2e6;
1827
+ border-radius: 6px;
1828
+ background: #f8f9fa;
1829
+ cursor: pointer;
1830
+ font-weight: 500;
1831
+ transition: all 0.2s ease;
1832
+ display: flex;
1833
+ align-items: center;
1834
+ gap: 8px;
1835
+ }
1836
+
1837
+ .docking-ligand-tab:hover {
1838
+ border-color: #6f42c1;
1839
+ background: #f3e8ff;
1840
+ }
1841
+
1842
+ .docking-ligand-tab.active {
1843
+ border-color: #6f42c1;
1844
+ background: linear-gradient(135deg, #6f42c1 0%, #9b59b6 100%);
1845
+ color: white;
1846
+ }
1847
+
1848
+ .docking-ligand-tab .ligand-color-dot {
1849
+ width: 12px;
1850
+ height: 12px;
1851
+ border-radius: 50%;
1852
+ display: inline-block;
1853
+ }
1854
+
1855
+ .docking-poses-viewer-wrapper {
1856
+ position: relative;
1857
+ margin-bottom: 15px;
1858
+ max-width: 700px;
1859
+ margin-left: auto;
1860
+ margin-right: auto;
1861
+ }
1862
+
1863
+ .docking-poses-viewer {
1864
+ width: 100%;
1865
+ max-width: 700px;
1866
+ height: 500px;
1867
+ background: #fff;
1868
+ border-radius: 5px;
1869
+ border: 2px solid #6f42c1;
1870
+ overflow: hidden;
1871
+ margin: 0 auto;
1872
+ }
1873
+
1874
+ .pose-nav-controls {
1875
+ position: absolute;
1876
+ bottom: 0;
1877
+ left: 0;
1878
+ right: 0;
1879
+ display: flex;
1880
+ justify-content: center;
1881
+ align-items: center;
1882
+ gap: 20px;
1883
+ padding: 15px;
1884
+ background: linear-gradient(to top, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0) 100%);
1885
+ border-radius: 0 0 5px 5px;
1886
+ }
1887
+
1888
+ .pose-nav-btn {
1889
+ width: 50px;
1890
+ height: 50px;
1891
+ border-radius: 50%;
1892
+ border: 2px solid #6f42c1;
1893
+ background: white;
1894
+ color: #6f42c1;
1895
+ cursor: pointer;
1896
+ transition: all 0.2s ease;
1897
+ display: flex;
1898
+ align-items: center;
1899
+ justify-content: center;
1900
+ font-size: 1.2rem;
1901
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
1902
+ }
1903
+
1904
+ .pose-nav-btn:hover:not(:disabled) {
1905
+ background: #6f42c1;
1906
+ border-color: #6f42c1;
1907
+ color: white;
1908
+ transform: scale(1.1);
1909
+ }
1910
+
1911
+ .pose-nav-btn:disabled {
1912
+ opacity: 0.3;
1913
+ cursor: not-allowed;
1914
+ }
1915
+
1916
+ .pose-info-display {
1917
+ text-align: center;
1918
+ min-width: 200px;
1919
+ }
1920
+
1921
+ .pose-mode-label {
1922
+ font-size: 1.1rem;
1923
+ font-weight: 600;
1924
+ color: #333;
1925
+ }
1926
+
1927
+ .pose-energy-label {
1928
+ font-size: 0.95rem;
1929
+ color: #28a745;
1930
+ font-weight: 500;
1931
+ }
1932
+
1933
+ .pose-color-legend {
1934
+ display: flex;
1935
+ gap: 20px;
1936
+ justify-content: center;
1937
+ margin-top: 8px;
1938
+ font-size: 0.85rem;
1939
+ }
1940
+
1941
+ .pose-color-legend span {
1942
+ display: flex;
1943
+ align-items: center;
1944
+ gap: 6px;
1945
+ color: #555;
1946
+ }
1947
+
1948
+ .legend-dot {
1949
+ width: 12px;
1950
+ height: 12px;
1951
+ border-radius: 50%;
1952
+ display: inline-block;
1953
+ }
1954
+
1955
+ .legend-dot.original {
1956
+ background: #00ff00;
1957
+ }
1958
+
1959
+ .legend-dot.docked {
1960
+ background: #ff6b6b;
1961
+ }
1962
+
1963
+ .docking-poses-selection {
1964
+ background: #f8f9fa;
1965
+ border-radius: 8px;
1966
+ padding: 15px;
1967
+ border: 1px solid #dee2e6;
1968
+ }
1969
+
1970
+ .docking-poses-selection h5 {
1971
+ margin-bottom: 10px;
1972
+ color: #495057;
1973
+ font-size: 0.95rem;
1974
+ }
1975
+
1976
+ .pose-selection-row {
1977
+ display: flex;
1978
+ align-items: center;
1979
+ gap: 15px;
1980
+ padding: 10px;
1981
+ background: white;
1982
+ border-radius: 6px;
1983
+ margin-bottom: 8px;
1984
+ border: 1px solid #e9ecef;
1985
+ }
1986
+
1987
+ .pose-selection-row:last-child {
1988
+ margin-bottom: 0;
1989
+ }
1990
+
1991
+ .pose-selection-label {
1992
+ font-weight: 500;
1993
+ min-width: 100px;
1994
+ }
1995
+
1996
+ .pose-selection-options {
1997
+ display: flex;
1998
+ gap: 15px;
1999
+ flex-wrap: wrap;
2000
+ }
2001
+
2002
+ .pose-selection-option {
2003
+ display: flex;
2004
+ align-items: center;
2005
+ gap: 6px;
2006
+ cursor: pointer;
2007
+ padding: 4px 8px;
2008
+ border-radius: 4px;
2009
+ transition: background 0.2s ease;
2010
+ }
2011
+
2012
+ .pose-selection-option:hover {
2013
+ background: #e9ecef;
2014
+ }
2015
+
2016
+ .pose-selection-option input[type="radio"] {
2017
+ accent-color: #6f42c1;
2018
+ }
2019
+
2020
+ .pose-selection-option.selected {
2021
+ background: #f3e8ff;
2022
+ border: 1px solid #6f42c1;
2023
+ }
2024
+
2025
+ .pose-selection-energy {
2026
+ font-size: 0.85rem;
2027
+ color: #28a745;
2028
+ font-weight: 500;
2029
+ }
ambermdflow/docking.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Step 1 obabel -i pdb ../4_ligands_corrected_1.pdb -o sdf -O ligand.sdf
2
+
3
+ # Step 2 Run tleap on 1_protein_no_hydrogens.pdb to protonate and add hydrogen to the pdb file
4
+ # leap file content:
5
+ # source leaprc.protein.ff14SB
6
+ # protein = loadpdb 1_protein_no_hydrogens.pdb
7
+ # savepdb protein protein.pdb
8
+ # quit
9
+
10
+ # Step 3 pdb4amber -i receptor.pdb -o receptor_fixed.pdb run this command on protein to add element names
11
+
12
+ # Step 4 mk_prepare_ligand.py -i ligand.sdf -o ligand.pdbqt run this command on ligand to get pdbqt file for selected ligand
13
+
14
+ # Step 4 mk_prepare_receptor.py -i receptor.pdb -o receptor -p run this command on protein to get pdbqt file for selected protein chain
15
+
16
+ # Now we are ready to run the docking
17
+
18
+ # find the center of the ligand
19
+ # run this script
20
+ #from MDAnalysis import Universe
21
+ #import numpy as np
22
+ #
23
+ #u = Universe("../output/4_ligands_corrected_1.pdb")
24
+ #
25
+ ## replace 'LIG' with your ligand residue name
26
+ #ligand = u.select_atoms("all")
27
+ #coords = ligand.positions
28
+ #
29
+ ## compute center of ligand
30
+ #center = coords.mean(axis=0)
31
+ #print("Center of ligand:", center)
32
+
33
+ #then run this vina script
34
+ #vina \
35
+ # --receptor receptor_ready.pdbqt \
36
+ # --ligand ligand_1.pdbqt \
37
+ # --center_x 34.3124 \
38
+ # --center_y 4.95463 \
39
+ # --center_z 1.774217 \
40
+ # --size_x 18 \
41
+ # --size_y 18 \
42
+ # --size_z 18 \
43
+ # --out ligand_1_docked.pdbqt \
44
+ # --log ligand_1_docked.log
45
+
46
+ #vina_split --input ligand_1_docked.pdbqt --ligand ligand_1_mode
47
+
48
+ #Now we need to turn back pdbqt file to pdb file for ligand
49
+ #run this command to do that obabel ligand_1_mode1.pdbqt -O ligand_1_mode1.pdb -p 7.4
50
+ #now we need to add remaining hydrogens in it using pymol. pymol command is h_add ligand_1_mode1.pdb
51
+
52
+ #Now we need to make sure the residue name is correct like the name in the original ligand and then
53
+ #we need to rename the atoms names to give this ligand to antechamber like C1, N1, .. like the way '4_ligands_corrected_1.pdb' formated
54
+ #Now this ligand is ready to be used by antechambe to generate force field parameters
ambermdflow/docking_utils.py ADDED
@@ -0,0 +1,639 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Docking Utilities for AmberMDFlow
4
+
5
+ This module contains all the Python functions needed for the docking workflow:
6
+ 1. Compute ligand center
7
+ 2. Prepare receptor (tleap + pdb4amber + meeko)
8
+ 3. Prepare ligand (obabel + meeko)
9
+ 4. Run Vina docking
10
+ 5. Split docked poses (vina_split)
11
+ 6. Convert poses to PDB (obabel)
12
+ 7. Sanitize docked poses for use in MD workflow
13
+
14
+ Usage:
15
+ from docking_utils import (
16
+ compute_ligand_center,
17
+ prepare_receptor,
18
+ prepare_ligand,
19
+ run_vina_docking,
20
+ split_docked_poses,
21
+ convert_pdbqt_to_pdb,
22
+ sanitize_docked_pose
23
+ )
24
+ """
25
+
26
+ import subprocess
27
+ from pathlib import Path
28
+ import logging
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ def compute_ligand_center(pdb_path: str) -> tuple:
34
+ """
35
+ Compute the geometric center of all atoms in a ligand PDB file.
36
+
37
+ Args:
38
+ pdb_path: Path to the ligand PDB file
39
+
40
+ Returns:
41
+ Tuple of (x, y, z) center coordinates
42
+ """
43
+ try:
44
+ import MDAnalysis as mda
45
+ import numpy as np
46
+ except ImportError as e:
47
+ raise RuntimeError(
48
+ "MDAnalysis and NumPy are required. Install with: "
49
+ "conda install -c conda-forge mdanalysis numpy"
50
+ ) from e
51
+
52
+ pdb_path = Path(pdb_path)
53
+ if not pdb_path.exists():
54
+ raise FileNotFoundError(f"Ligand file not found: {pdb_path}")
55
+
56
+ u = mda.Universe(str(pdb_path))
57
+ if u.atoms.n_atoms == 0:
58
+ raise ValueError(f"No atoms found in ligand file {pdb_path}")
59
+
60
+ coords = u.atoms.positions.astype(float)
61
+ center = coords.mean(axis=0)
62
+
63
+ logger.info(f"Ligand center for {pdb_path.name}: ({center[0]:.3f}, {center[1]:.3f}, {center[2]:.3f})")
64
+ return float(center[0]), float(center[1]), float(center[2])
65
+
66
+
67
+ def prepare_receptor(protein_pdb: str, output_dir: str) -> tuple:
68
+ """
69
+ Prepare receptor for docking:
70
+ 1. Run tleap to add hydrogens
71
+ 2. Run pdb4amber to fix element names
72
+ 3. Run mk_prepare_receptor.py to create PDBQT
73
+
74
+ Args:
75
+ protein_pdb: Path to protein PDB file (typically 1_protein_no_hydrogens.pdb)
76
+ output_dir: Directory to store output files
77
+
78
+ Returns:
79
+ Tuple of (receptor_fixed_pdb_path, receptor_pdbqt_path)
80
+ """
81
+ protein_pdb = Path(protein_pdb).resolve()
82
+ output_dir = Path(output_dir)
83
+ output_dir.mkdir(parents=True, exist_ok=True)
84
+
85
+ if not protein_pdb.exists():
86
+ raise FileNotFoundError(f"Protein PDB not found: {protein_pdb}")
87
+
88
+ # Step 1: tleap - add hydrogens
89
+ tleap_in = output_dir / "prepare_receptor.in"
90
+ receptor_pdb = output_dir / "receptor.pdb"
91
+
92
+ if not receptor_pdb.exists():
93
+ logger.info("Step 1: Running tleap to add hydrogens to protein...")
94
+ with open(tleap_in, "w") as f:
95
+ f.write("source leaprc.protein.ff14SB\n")
96
+ f.write(f"protein = loadpdb {protein_pdb}\n")
97
+ f.write("savepdb protein receptor.pdb\n")
98
+ f.write("quit\n")
99
+
100
+ result = subprocess.run(
101
+ ["tleap", "-f", tleap_in.name],
102
+ cwd=output_dir,
103
+ capture_output=True,
104
+ text=True,
105
+ )
106
+ if result.returncode != 0 or not receptor_pdb.exists():
107
+ raise RuntimeError(
108
+ f"tleap failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
109
+ )
110
+ logger.info(f" Created: {receptor_pdb}")
111
+
112
+ # Step 2: pdb4amber - fix element names
113
+ receptor_fixed = output_dir / "receptor_fixed.pdb"
114
+
115
+ if not receptor_fixed.exists():
116
+ logger.info("Step 2: Running pdb4amber to add element names...")
117
+ result = subprocess.run(
118
+ ["pdb4amber", "-i", str(receptor_pdb), "-o", str(receptor_fixed)],
119
+ capture_output=True,
120
+ text=True,
121
+ )
122
+ if result.returncode != 0 or not receptor_fixed.exists():
123
+ raise RuntimeError(
124
+ f"pdb4amber failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
125
+ )
126
+ logger.info(f" Created: {receptor_fixed}")
127
+
128
+ # Step 3: Meeko receptor preparation
129
+ receptor_pdbqt = output_dir / "receptor.pdbqt"
130
+
131
+ if not receptor_pdbqt.exists():
132
+ logger.info("Step 3: Running mk_prepare_receptor.py to create PDBQT...")
133
+ result = subprocess.run(
134
+ ["mk_prepare_receptor.py", "-i", str(receptor_fixed), "-o", "receptor", "-p"],
135
+ cwd=output_dir,
136
+ capture_output=True,
137
+ text=True,
138
+ )
139
+ if result.returncode != 0 or not receptor_pdbqt.exists():
140
+ raise RuntimeError(
141
+ f"mk_prepare_receptor.py failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
142
+ )
143
+ logger.info(f" Created: {receptor_pdbqt}")
144
+
145
+ return str(receptor_fixed), str(receptor_pdbqt)
146
+
147
+
148
+ def prepare_ligand(ligand_pdb: str, output_dir: str, ligand_index: int = 1) -> str:
149
+ """
150
+ Prepare ligand for docking:
151
+ 1. Convert PDB to SDF using obabel
152
+ 2. Convert SDF to PDBQT using mk_prepare_ligand.py
153
+
154
+ Args:
155
+ ligand_pdb: Path to ligand PDB file
156
+ output_dir: Directory to store output files
157
+ ligand_index: Index number for naming output files
158
+
159
+ Returns:
160
+ Path to ligand PDBQT file
161
+ """
162
+ ligand_pdb = Path(ligand_pdb)
163
+ output_dir = Path(output_dir)
164
+ output_dir.mkdir(parents=True, exist_ok=True)
165
+
166
+ if not ligand_pdb.exists():
167
+ raise FileNotFoundError(f"Ligand PDB not found: {ligand_pdb}")
168
+
169
+ # Step 1: obabel PDB -> SDF
170
+ sdf_path = output_dir / f"ligand_{ligand_index}.sdf"
171
+
172
+ logger.info(f"Step 1: Converting ligand {ligand_index} PDB to SDF...")
173
+ result = subprocess.run(
174
+ ["obabel", "-i", "pdb", str(ligand_pdb), "-o", "sdf", "-O", str(sdf_path)],
175
+ capture_output=True,
176
+ text=True,
177
+ )
178
+ if result.returncode != 0 or not sdf_path.exists():
179
+ raise RuntimeError(
180
+ f"obabel failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
181
+ )
182
+ logger.info(f" Created: {sdf_path}")
183
+
184
+ # Step 2: Meeko ligand preparation -> PDBQT
185
+ pdbqt_path = output_dir / f"ligand_{ligand_index}.pdbqt"
186
+
187
+ logger.info(f"Step 2: Converting ligand {ligand_index} SDF to PDBQT...")
188
+ result = subprocess.run(
189
+ ["mk_prepare_ligand.py", "-i", str(sdf_path), "-o", str(pdbqt_path)],
190
+ capture_output=True,
191
+ text=True,
192
+ )
193
+ if result.returncode != 0 or not pdbqt_path.exists():
194
+ raise RuntimeError(
195
+ f"mk_prepare_ligand.py failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
196
+ )
197
+ logger.info(f" Created: {pdbqt_path}")
198
+
199
+ return str(pdbqt_path)
200
+
201
+
202
+ def run_vina_docking(
203
+ receptor_pdbqt: str,
204
+ ligand_pdbqt: str,
205
+ center_x: float,
206
+ center_y: float,
207
+ center_z: float,
208
+ size_x: float = 18.0,
209
+ size_y: float = 18.0,
210
+ size_z: float = 18.0,
211
+ output_dir: str = None,
212
+ ligand_index: int = 1,
213
+ exhaustiveness: int = 8,
214
+ num_modes: int = 9,
215
+ ) -> tuple:
216
+ """
217
+ Run AutoDock Vina docking.
218
+
219
+ Args:
220
+ receptor_pdbqt: Path to receptor PDBQT file
221
+ ligand_pdbqt: Path to ligand PDBQT file
222
+ center_x, center_y, center_z: Box center coordinates (Angstroms)
223
+ size_x, size_y, size_z: Box dimensions (Angstroms)
224
+ output_dir: Directory for output files (default: same as ligand)
225
+ ligand_index: Index for naming output files
226
+ exhaustiveness: Search exhaustiveness (default: 8)
227
+ num_modes: Maximum number of binding modes (default: 9)
228
+
229
+ Returns:
230
+ Tuple of (docked_pdbqt_path, log_file_path)
231
+ """
232
+ ligand_pdbqt = Path(ligand_pdbqt)
233
+ output_dir = Path(output_dir) if output_dir else ligand_pdbqt.parent
234
+
235
+ docked_pdbqt = output_dir / f"ligand_{ligand_index}_docked.pdbqt"
236
+ log_file = output_dir / f"ligand_{ligand_index}_docked.log"
237
+
238
+ logger.info(f"Running Vina docking for ligand {ligand_index}...")
239
+ logger.info(f" Center: ({center_x:.3f}, {center_y:.3f}, {center_z:.3f})")
240
+ logger.info(f" Size: ({size_x:.1f}, {size_y:.1f}, {size_z:.1f})")
241
+
242
+ cmd = [
243
+ "vina",
244
+ "--receptor", str(receptor_pdbqt),
245
+ "--ligand", str(ligand_pdbqt),
246
+ "--center_x", str(center_x),
247
+ "--center_y", str(center_y),
248
+ "--center_z", str(center_z),
249
+ "--size_x", str(size_x),
250
+ "--size_y", str(size_y),
251
+ "--size_z", str(size_z),
252
+ "--out", str(docked_pdbqt),
253
+ "--log", str(log_file),
254
+ "--exhaustiveness", str(exhaustiveness),
255
+ "--num_modes", str(num_modes),
256
+ ]
257
+
258
+ result = subprocess.run(cmd, capture_output=True, text=True)
259
+
260
+ if result.returncode != 0 or not docked_pdbqt.exists():
261
+ raise RuntimeError(
262
+ f"Vina docking failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
263
+ )
264
+
265
+ logger.info(f" Created: {docked_pdbqt}")
266
+ logger.info(f" Log: {log_file}")
267
+
268
+ return str(docked_pdbqt), str(log_file)
269
+
270
+
271
+ def parse_vina_log(log_path: str) -> list:
272
+ """
273
+ Parse Vina log file to extract binding energies for each mode.
274
+
275
+ Args:
276
+ log_path: Path to Vina log file
277
+
278
+ Returns:
279
+ List of dicts with 'mode', 'affinity', 'rmsd_lb', 'rmsd_ub' for each pose
280
+ """
281
+ log_path = Path(log_path)
282
+ if not log_path.exists():
283
+ return []
284
+
285
+ energies = []
286
+ in_results = False
287
+
288
+ with open(log_path, "r") as f:
289
+ for line in f:
290
+ line = line.strip()
291
+ if "-----+------------+----------+----------" in line:
292
+ in_results = True
293
+ continue
294
+ if in_results and line and line[0].isdigit():
295
+ parts = line.split()
296
+ if len(parts) >= 4:
297
+ try:
298
+ energies.append({
299
+ 'mode': int(parts[0]),
300
+ 'affinity': float(parts[1]),
301
+ 'rmsd_lb': float(parts[2]),
302
+ 'rmsd_ub': float(parts[3]),
303
+ })
304
+ except (ValueError, IndexError):
305
+ continue
306
+ elif in_results and not line:
307
+ break
308
+
309
+ return energies
310
+
311
+
312
+ def split_docked_poses(docked_pdbqt: str, output_prefix: str = None) -> list:
313
+ """
314
+ Split docked PDBQT into individual pose files using vina_split.
315
+
316
+ Args:
317
+ docked_pdbqt: Path to docked PDBQT file with multiple poses
318
+ output_prefix: Prefix for output files (default: derived from input)
319
+
320
+ Returns:
321
+ List of paths to individual pose PDBQT files
322
+ """
323
+ docked_pdbqt = Path(docked_pdbqt)
324
+ if not docked_pdbqt.exists():
325
+ raise FileNotFoundError(f"Docked PDBQT not found: {docked_pdbqt}")
326
+
327
+ output_dir = docked_pdbqt.parent
328
+ if output_prefix is None:
329
+ output_prefix = docked_pdbqt.stem.replace("_docked", "_mode")
330
+
331
+ logger.info(f"Splitting docked poses from {docked_pdbqt.name}...")
332
+
333
+ result = subprocess.run(
334
+ ["vina_split", "--input", str(docked_pdbqt), "--ligand", output_prefix],
335
+ cwd=output_dir,
336
+ capture_output=True,
337
+ text=True,
338
+ )
339
+
340
+ if result.returncode != 0:
341
+ raise RuntimeError(
342
+ f"vina_split failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
343
+ )
344
+
345
+ # Find all generated mode files
346
+ pose_files = sorted(output_dir.glob(f"{output_prefix}*.pdbqt"))
347
+ logger.info(f" Split into {len(pose_files)} pose files")
348
+
349
+ return [str(f) for f in pose_files]
350
+
351
+
352
+ def convert_pdbqt_to_pdb(pdbqt_path: str, ph: float = 7.4) -> str:
353
+ """
354
+ Convert PDBQT file to PDB using obabel.
355
+
356
+ Args:
357
+ pdbqt_path: Path to PDBQT file
358
+ ph: pH for protonation (default: 7.4)
359
+
360
+ Returns:
361
+ Path to output PDB file
362
+ """
363
+ pdbqt_path = Path(pdbqt_path)
364
+ if not pdbqt_path.exists():
365
+ raise FileNotFoundError(f"PDBQT file not found: {pdbqt_path}")
366
+
367
+ pdb_path = pdbqt_path.with_suffix(".pdb")
368
+
369
+ logger.info(f"Converting {pdbqt_path.name} to PDB...")
370
+
371
+ result = subprocess.run(
372
+ ["obabel", str(pdbqt_path), "-O", str(pdb_path), "-p", str(ph)],
373
+ capture_output=True,
374
+ text=True,
375
+ )
376
+
377
+ if result.returncode != 0 or not pdb_path.exists():
378
+ raise RuntimeError(
379
+ f"obabel conversion failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
380
+ )
381
+
382
+ logger.info(f" Created: {pdb_path}")
383
+ return str(pdb_path)
384
+
385
+
386
+ def sanitize_docked_pose(original_ligand: str, pose_pdb: str) -> str:
387
+ """
388
+ Sanitize a docked pose PDB to match the original ligand format:
389
+ - Restore residue name, chain ID, and residue number from original
390
+ - Convert ATOM to HETATM
391
+ - Rename atoms to match original format (C1, N1, etc.)
392
+ - Remove CONECT/MASTER records
393
+
394
+ Args:
395
+ original_ligand: Path to original ligand PDB file
396
+ pose_pdb: Path to docked pose PDB file
397
+
398
+ Returns:
399
+ Path to sanitized pose PDB (same as pose_pdb, modified in place)
400
+ """
401
+ original_ligand = Path(original_ligand)
402
+ pose_pdb = Path(pose_pdb)
403
+
404
+ if not original_ligand.exists():
405
+ raise FileNotFoundError(f"Original ligand not found: {original_ligand}")
406
+ if not pose_pdb.exists():
407
+ raise FileNotFoundError(f"Pose PDB not found: {pose_pdb}")
408
+
409
+ # Extract residue info from original ligand
410
+ resname = "LIG"
411
+ chain = "X"
412
+ resnum = 1
413
+
414
+ with open(original_ligand, "r") as f:
415
+ for line in f:
416
+ if line.startswith(("ATOM", "HETATM")):
417
+ resname = line[17:20].strip() or "LIG"
418
+ chain = line[21] if len(line) > 21 and line[21].strip() else "X"
419
+ try:
420
+ resnum = int(line[22:26].strip())
421
+ except ValueError:
422
+ resnum = 1
423
+ break
424
+
425
+ logger.info(f"Sanitizing pose with resname={resname}, chain={chain}, resnum={resnum}")
426
+
427
+ # Process pose PDB
428
+ new_lines = []
429
+ atom_counter = 0
430
+ element_counts = {}
431
+
432
+ with open(pose_pdb, "r") as f:
433
+ for line in f:
434
+ if line.startswith(("CONECT", "MASTER")):
435
+ continue
436
+ if line.startswith(("ATOM", "HETATM")):
437
+ atom_counter += 1
438
+
439
+ # Extract element from line or atom name
440
+ element = line[76:78].strip() if len(line) > 77 else ""
441
+ if not element:
442
+ # Try to get from atom name
443
+ atom_name = line[12:16].strip()
444
+ element = ''.join(c for c in atom_name if c.isalpha())[:2]
445
+ if len(element) > 1:
446
+ element = element[0].upper() + element[1].lower()
447
+
448
+ if not element:
449
+ element = "C" # Default fallback
450
+
451
+ # Generate new atom name (C1, C2, N1, etc.)
452
+ element_counts[element] = element_counts.get(element, 0) + 1
453
+ new_atom_name = f"{element}{element_counts[element]}"
454
+ new_atom_name = f"{new_atom_name:<4}" # Left-justified, 4 chars
455
+
456
+ # Build new line as HETATM
457
+ new_line = (
458
+ f"HETATM{atom_counter:5d} {new_atom_name}"
459
+ f"{resname:>3s} {chain}{resnum:4d} "
460
+ f"{line[30:54]}" # Coordinates
461
+ f"{line[54:66] if len(line) > 54 else ' 1.00 0.00'}" # Occupancy, B-factor
462
+ f" {element:>2s}\n"
463
+ )
464
+ new_lines.append(new_line)
465
+ elif line.startswith("END"):
466
+ new_lines.append("END\n")
467
+
468
+ # Write sanitized file
469
+ with open(pose_pdb, "w") as f:
470
+ f.writelines(new_lines)
471
+
472
+ logger.info(f" Sanitized: {pose_pdb}")
473
+ return str(pose_pdb)
474
+
475
+
476
+ def run_full_docking_workflow(
477
+ protein_pdb: str,
478
+ ligand_pdbs: list,
479
+ output_dir: str,
480
+ box_configs: dict = None,
481
+ ) -> dict:
482
+ """
483
+ Run the complete docking workflow for multiple ligands.
484
+
485
+ Args:
486
+ protein_pdb: Path to protein PDB file (1_protein_no_hydrogens.pdb)
487
+ ligand_pdbs: List of paths to ligand PDB files
488
+ output_dir: Base output directory for docking results
489
+ box_configs: Optional dict of {ligand_index: {'center': (x,y,z), 'size': (sx,sy,sz)}}
490
+
491
+ Returns:
492
+ Dict with results for each ligand including poses and energies
493
+ """
494
+ output_dir = Path(output_dir)
495
+ output_dir.mkdir(parents=True, exist_ok=True)
496
+ box_configs = box_configs or {}
497
+
498
+ results = {
499
+ 'success': True,
500
+ 'ligands': [],
501
+ 'warnings': [],
502
+ 'errors': [],
503
+ }
504
+
505
+ # Step 1: Prepare receptor (only once for all ligands)
506
+ logger.info("=" * 60)
507
+ logger.info("STEP 1: Preparing receptor for docking")
508
+ logger.info("=" * 60)
509
+
510
+ try:
511
+ receptor_fixed, receptor_pdbqt = prepare_receptor(protein_pdb, str(output_dir))
512
+ except Exception as e:
513
+ results['success'] = False
514
+ results['errors'].append(f"Receptor preparation failed: {str(e)}")
515
+ return results
516
+
517
+ # Step 2: Process each ligand
518
+ for idx, ligand_pdb in enumerate(ligand_pdbs, start=1):
519
+ ligand_pdb = Path(ligand_pdb)
520
+ logger.info("")
521
+ logger.info("=" * 60)
522
+ logger.info(f"STEP 2.{idx}: Processing ligand {idx}: {ligand_pdb.name}")
523
+ logger.info("=" * 60)
524
+
525
+ lig_dir = output_dir / f"ligand_{idx}"
526
+ lig_dir.mkdir(parents=True, exist_ok=True)
527
+
528
+ ligand_result = {
529
+ 'index': idx,
530
+ 'original_file': str(ligand_pdb),
531
+ 'poses': [],
532
+ 'energies': [],
533
+ 'success': True,
534
+ }
535
+
536
+ try:
537
+ # Copy original ligand for reference
538
+ original_copy = lig_dir / "original_ligand.pdb"
539
+ if not original_copy.exists():
540
+ original_copy.write_text(ligand_pdb.read_text())
541
+
542
+ # Prepare ligand PDBQT
543
+ ligand_pdbqt = prepare_ligand(str(ligand_pdb), str(lig_dir), idx)
544
+
545
+ # Get box configuration
546
+ cfg = box_configs.get(idx, {})
547
+ center = cfg.get('center')
548
+ size = cfg.get('size', (18.0, 18.0, 18.0))
549
+
550
+ if center is None:
551
+ # Compute center from ligand
552
+ cx, cy, cz = compute_ligand_center(str(ligand_pdb))
553
+ else:
554
+ cx, cy, cz = center
555
+
556
+ sx, sy, sz = size
557
+
558
+ # Run Vina docking
559
+ docked_pdbqt, log_file = run_vina_docking(
560
+ receptor_pdbqt, ligand_pdbqt,
561
+ cx, cy, cz, sx, sy, sz,
562
+ str(lig_dir), idx
563
+ )
564
+
565
+ # Parse binding energies
566
+ energies = parse_vina_log(log_file)
567
+ ligand_result['energies'] = energies
568
+
569
+ # Split poses
570
+ pose_pdbqts = split_docked_poses(docked_pdbqt)
571
+
572
+ # Convert each pose to PDB and sanitize
573
+ for pose_pdbqt in pose_pdbqts:
574
+ pose_pdb = convert_pdbqt_to_pdb(pose_pdbqt)
575
+ sanitize_docked_pose(str(original_copy), pose_pdb)
576
+ ligand_result['poses'].append(pose_pdb)
577
+
578
+ except Exception as e:
579
+ ligand_result['success'] = False
580
+ ligand_result['error'] = str(e)
581
+ results['errors'].append(f"Ligand {idx}: {str(e)}")
582
+ logger.error(f"Error processing ligand {idx}: {e}")
583
+
584
+ results['ligands'].append(ligand_result)
585
+
586
+ # Check overall success
587
+ results['success'] = all(lig['success'] for lig in results['ligands'])
588
+
589
+ logger.info("")
590
+ logger.info("=" * 60)
591
+ logger.info("DOCKING WORKFLOW COMPLETE")
592
+ logger.info("=" * 60)
593
+
594
+ return results
595
+
596
+
597
+ # Example usage / CLI interface
598
+ if __name__ == "__main__":
599
+ import argparse
600
+
601
+ logging.basicConfig(level=logging.INFO, format='%(message)s')
602
+
603
+ parser = argparse.ArgumentParser(description="Run AutoDock Vina docking workflow")
604
+ parser.add_argument("--protein", required=True, help="Path to protein PDB file")
605
+ parser.add_argument("--ligands", nargs="+", required=True, help="Paths to ligand PDB files")
606
+ parser.add_argument("--output", required=True, help="Output directory")
607
+ parser.add_argument("--center", nargs=3, type=float, help="Box center (x y z)")
608
+ parser.add_argument("--size", nargs=3, type=float, default=[18, 18, 18], help="Box size (x y z)")
609
+
610
+ args = parser.parse_args()
611
+
612
+ box_configs = {}
613
+ if args.center:
614
+ for i in range(1, len(args.ligands) + 1):
615
+ box_configs[i] = {
616
+ 'center': tuple(args.center),
617
+ 'size': tuple(args.size),
618
+ }
619
+
620
+ results = run_full_docking_workflow(
621
+ args.protein,
622
+ args.ligands,
623
+ args.output,
624
+ box_configs
625
+ )
626
+
627
+ print("\n" + "=" * 60)
628
+ print("RESULTS SUMMARY")
629
+ print("=" * 60)
630
+ print(f"Overall success: {results['success']}")
631
+ for lig in results['ligands']:
632
+ print(f"\nLigand {lig['index']}:")
633
+ print(f" Success: {lig['success']}")
634
+ if lig['success']:
635
+ print(f" Poses generated: {len(lig['poses'])}")
636
+ if lig['energies']:
637
+ print(f" Best binding energy: {lig['energies'][0]['affinity']} kcal/mol")
638
+ else:
639
+ print(f" Error: {lig.get('error', 'Unknown')}")
ambermdflow/html/index.html ADDED
@@ -0,0 +1,1145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>MD Simulation Pipeline</title>
7
+ <link rel="stylesheet" href="../css/styles.css">
8
+ <link rel="stylesheet" href="../css/plumed.css">
9
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
10
+ <!-- THREE.js (needed for docking box visualization) - using r95 to match NGL's bundled version -->
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r95/three.min.js"></script>
12
+ <!-- NGL 3D Molecular Viewer -->
13
+ <script src="https://unpkg.com/ngl@2.0.0-dev.35/dist/ngl.js"></script>
14
+ </head>
15
+ <body>
16
+ <div class="container">
17
+ <!-- Header -->
18
+ <header class="header">
19
+ <div class="header-content">
20
+ <h1><i class="fas fa-atom"></i> MD Simulation Pipeline</h1>
21
+ <p>Molecular Dynamics Simulation Setup and File Generation</p>
22
+ </div>
23
+ </header>
24
+
25
+ <!-- Navigation Tabs -->
26
+ <nav class="tab-navigation">
27
+ <button class="tab-button active" data-tab="protein-loading">
28
+ <i class="fas fa-upload"></i> Protein Loading
29
+ </button>
30
+ <button class="tab-button" data-tab="fill-missing">
31
+ <i class="fas fa-puzzle-piece"></i> Fill Missing Residues
32
+ </button>
33
+ <button class="tab-button" data-tab="structure-prep">
34
+ <i class="fas fa-tools"></i> Structure Preparation
35
+ </button>
36
+ <button class="tab-button" data-tab="simulation-params">
37
+ <i class="fas fa-cogs"></i> Simulation Parameters
38
+ </button>
39
+ <button class="tab-button" data-tab="simulation-steps">
40
+ <i class="fas fa-list-ol"></i> Simulation Steps
41
+ </button>
42
+ <button class="tab-button" data-tab="file-generation">
43
+ <i class="fas fa-file-download"></i> Generate Files
44
+ </button>
45
+ <button class="tab-button" data-tab="plumed">
46
+ <i class="fas fa-chart-line"></i> PLUMED
47
+ </button>
48
+ </nav>
49
+
50
+ <!-- Main Content -->
51
+ <main class="main-content">
52
+ <!-- Protein Loading Tab -->
53
+ <div id="protein-loading" class="tab-content active">
54
+ <div class="card">
55
+ <h2><i class="fas fa-dna"></i> Protein Structure Input</h2>
56
+
57
+ <div class="input-methods">
58
+ <div class="method-option">
59
+ <h3><i class="fas fa-file-upload"></i> Upload PDB File</h3>
60
+ <div class="file-upload-area" id="file-upload-area">
61
+ <i class="fas fa-cloud-upload-alt"></i>
62
+ <p>Drag and drop your PDB file here or click to browse</p>
63
+ <input type="file" id="pdb-file" accept=".pdb,.ent" style="display: none;">
64
+ <button type="button" class="btn btn-secondary" id="choose-file-btn">
65
+ Choose File
66
+ </button>
67
+ </div>
68
+ <div id="file-info" class="file-info" style="display: none;">
69
+ <p><strong>Selected file:</strong> <span id="file-name"></span></p>
70
+ <p><strong>Size:</strong> <span id="file-size"></span></p>
71
+ </div>
72
+ </div>
73
+
74
+ <div class="divider">
75
+ <span>OR</span>
76
+ </div>
77
+
78
+ <div class="method-option">
79
+ <h3><i class="fas fa-database"></i> Fetch from PDB</h3>
80
+ <div class="pdb-fetch">
81
+ <div class="input-group">
82
+ <label for="pdb-id">PDB ID:</label>
83
+ <input type="text" id="pdb-id" placeholder="e.g., 1CRN, 1HTM" maxlength="4">
84
+ <button type="button" class="btn btn-primary" id="fetch-pdb">
85
+ <i class="fas fa-download"></i> Fetch
86
+ </button>
87
+ </div>
88
+ <div id="pdb-status" class="status-message"></div>
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ <div class="protein-preview" id="protein-preview" style="display: none;">
94
+ <h3><i class="fas fa-eye"></i> Protein Preview</h3>
95
+ <div class="preview-content">
96
+ <div class="protein-info">
97
+ <p><strong>Structure ID:</strong> <span id="structure-id"></span></p>
98
+ <p><strong>Number of atoms (Protein):</strong> <span id="atom-count"></span></p>
99
+ <p><strong>Chains:</strong> <span id="chain-info"></span></p>
100
+ <p><strong>Residues:</strong> <span id="residue-count"></span></p>
101
+ <p><strong>Water molecules:</strong> <span id="water-count"></span></p>
102
+ <p><strong>Ions:</strong> <span id="ion-count"></span></p>
103
+ <p><strong>Ligands:</strong> <span id="ligand-info"></span></p>
104
+ <p><strong>HETATM entries:</strong> <span id="hetatm-count"></span></p>
105
+ </div>
106
+ <div class="protein-visualization">
107
+ <div id="molecule-viewer" class="molecule-viewer">
108
+ <!-- 3D visualization will be added here -->
109
+ <div id="ngl-viewer" style="width: 100%; height: 100%; min-height: 300px;"></div>
110
+ <div id="viewer-controls" class="viewer-controls" style="display: none;">
111
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.resetView()">
112
+ <i class="fas fa-home"></i> Reset View
113
+ </button>
114
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleRepresentation()">
115
+ <i class="fas fa-eye"></i> <span id="style-text">Mixed View</span>
116
+ </button>
117
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleSpin()">
118
+ <i class="fas fa-sync"></i> Spin
119
+ </button>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </div>
126
+ </div>
127
+
128
+ <!-- Fill Missing Residues Tab -->
129
+ <div id="fill-missing" class="tab-content">
130
+ <div class="card">
131
+ <h2><i class="fas fa-puzzle-piece"></i> Fill Missing Residues</h2>
132
+ <p class="card-description">
133
+ Detect missing residues in the experimental structure using RCSB annotations and complete them
134
+ with ESMFold. You can choose which chains to include in the completion.
135
+ </p>
136
+ <p class="form-text text-muted" style="margin-top: 6px; font-size: 0.9em;">
137
+ <i class="fas fa-book"></i> If you use this workflow in your research, please cite: <a href="https://esmatlas.com/about" target="_blank" rel="noopener noreferrer">ESM Atlas</a>
138
+ </p>
139
+
140
+ <div class="prep-sections">
141
+ <div class="prep-section">
142
+ <h3><i class="fas fa-search"></i> Detect Missing Residues</h3>
143
+ <button class="btn btn-primary" id="detect-missing-residues">
144
+ <i class="fas fa-search"></i> Analyze Missing Residues
145
+ </button>
146
+ <div id="missing-status" class="status-message" style="margin-top: 10px;"></div>
147
+ </div>
148
+
149
+ <div class="prep-section" id="missing-chains-section" style="display: none;">
150
+ <h3><i class="fas fa-link"></i> Select Chains for Completion</h3>
151
+ <p class="option-description">
152
+ Chains listed below have missing residues. Select which chains you want to rebuild with ESMFold.
153
+ </p>
154
+ <div id="missing-chains-list" class="multi-checkbox-group">
155
+ <!-- Missing chains checkboxes will be rendered here -->
156
+ </div>
157
+ <small class="form-help">
158
+ At least one chain must be selected to build a completed structure.
159
+ </small>
160
+ </div>
161
+ </div>
162
+
163
+ <div class="prep-section prep-section-fullwidth" id="trim-residues-section" style="display: none;">
164
+ <h3><i class="fas fa-cut"></i> Trim Residues from Edges (Optional)</h3>
165
+ <p class="option-description">
166
+ Optionally trim residues from the N-terminal and/or C-terminal edges of selected chains.
167
+ This only removes residues from the edges, not from loops in between.
168
+ </p>
169
+ <div class="trim-info-box" id="trim-info-box-content">
170
+ <i class="fas fa-info-circle"></i>
171
+ <strong>Note:</strong> Only missing residues at the <strong>N-terminal edge</strong> (beginning of sequence) and <strong>C-terminal edge</strong> (end of sequence) can be trimmed.
172
+ Missing residues in internal loops (discontinuities in the middle of the sequence) cannot be trimmed using this tool and will be filled by ESMFold.
173
+ </div>
174
+ <div id="trim-residues-list" class="trim-residues-container">
175
+ <!-- Trim controls for each chain will be rendered here -->
176
+ </div>
177
+ <button class="btn btn-secondary" id="apply-trim" style="margin-top: 15px;">
178
+ <i class="fas fa-check"></i> Apply Trimming
179
+ </button>
180
+ <div id="trim-status" class="status-message" style="margin-top: 10px; display: none;"></div>
181
+ </div>
182
+
183
+ <!-- Chain Minimization Option -->
184
+ <div class="prep-section" id="chain-minimization-section" style="display: none; margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 5px; border: 1px solid #dee2e6;">
185
+ <h3><i class="fas fa-compress-arrows-alt"></i> Energy Minimization (Optional)</h3>
186
+ <div class="form-check" style="display: flex; align-items: center; gap: 10px; margin-top: 10px;">
187
+ <input class="form-check-input" type="checkbox" id="minimize-chains-checkbox" style="width: 20px; height: 20px; cursor: pointer;">
188
+ <label class="form-check-label" for="minimize-chains-checkbox" style="cursor: pointer; font-weight: 500; flex: 1;">
189
+ <strong>Energy minimize ESMFold-generated chains</strong>
190
+ </label>
191
+ </div>
192
+ <small class="form-text text-muted" style="display: block; margin-top: 8px; margin-left: 30px;">
193
+ <i class="fas fa-info-circle"></i> Recommended: Minimization resolve structural clashes
194
+ </small>
195
+ <div id="minimization-chains-list" style="display: none; margin-top: 15px; margin-left: 30px;">
196
+ <strong>Select chains to minimize:</strong>
197
+ <div id="minimization-chains-checkboxes" class="multi-checkbox-group" style="margin-top: 10px;">
198
+ <!-- Chain checkboxes will be populated here -->
199
+ </div>
200
+ </div>
201
+ </div>
202
+
203
+ <div class="prep-actions">
204
+ <button class="btn btn-primary" id="build-complete-structure" disabled>
205
+ <i class="fas fa-magic"></i> Build Completed Structure
206
+ </button>
207
+ <button class="btn btn-secondary" id="preview-completed-structure" disabled>
208
+ <i class="fas fa-eye"></i> Preview Completed Structure
209
+ </button>
210
+ <button class="btn btn-secondary" id="preview-superimposed-structure" disabled>
211
+ <i class="fas fa-layer-group"></i> View Superimposed Structures
212
+ </button>
213
+ <button class="btn btn-info" id="download-completed-structure" disabled>
214
+ <i class="fas fa-download"></i> Download Completed PDB
215
+ </button>
216
+ </div>
217
+
218
+ <div class="prep-status" id="missing-summary" style="display: none;">
219
+ <h3><i class="fas fa-info-circle"></i> Missing Residues Summary</h3>
220
+ <div class="status-content" id="missing-summary-content">
221
+ <!-- Missing residues summary will be displayed here -->
222
+ </div>
223
+ </div>
224
+
225
+ <div class="prep-actions" id="sequence-viewer-actions" style="display: none; margin-top: 1rem;">
226
+ <button class="btn btn-secondary" id="view-protein-sequences">
227
+ <i class="fas fa-dna"></i> View Protein Sequences
228
+ </button>
229
+ </div>
230
+
231
+ <div class="prepared-structure-preview" id="sequence-viewer-section" style="display: none;">
232
+ <h3><i class="fas fa-dna"></i> Protein Sequence Viewer</h3>
233
+ <p class="card-description" style="margin-bottom: 15px;">
234
+ View protein sequences for all chains. Chain colors match the structure visualization. Missing residues are shown in grey.
235
+ </p>
236
+ <div id="sequence-viewer-content" class="sequence-viewer-container">
237
+ <!-- Sequence viewer will be rendered here -->
238
+ </div>
239
+ </div>
240
+
241
+ <div class="prepared-structure-preview" id="completed-structure-preview" style="display: none;">
242
+ <h3><i class="fas fa-eye"></i> Structure Comparison Preview</h3>
243
+ <p class="card-description" style="margin-bottom: 15px;">
244
+ Compare the completed structure (right) with the original crystal structure (left) to see the added missing residues.
245
+ </p>
246
+ <div class="preview-content" style="display: flex; justify-content: center; width: 100%; padding: 0;">
247
+ <div class="structure-comparison-container" style="display: flex; gap: 20px; flex-direction: row; width: 100%; max-width: 1400px; align-items: flex-start; justify-content: center;">
248
+ <!-- Original Structure Viewer -->
249
+ <div class="comparison-viewer" style="flex: 0 1 48%; min-width: 450px; max-width: 48%;">
250
+ <h4 style="text-align: center; margin-bottom: 10px; font-size: 14px;">
251
+ <i class="fas fa-dna"></i> Original Crystal Structure
252
+ </h4>
253
+ <div id="original-molecule-viewer" class="molecule-viewer" style="border: 2px solid #007bff; border-radius: 5px; width: 100%; height: 500px; position: relative;">
254
+ <div id="original-ngl-viewer" style="width: 100%; height: 100%; position: absolute; top: 0; left: 0;"></div>
255
+ <div id="original-viewer-controls" class="viewer-controls" style="display: none; justify-content: center; position: relative; z-index: 10;">
256
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.resetOriginalView()">
257
+ <i class="fas fa-home"></i> Reset
258
+ </button>
259
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleOriginalRepresentation()">
260
+ <i class="fas fa-eye"></i> <span id="original-style-text">Mixed</span>
261
+ </button>
262
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleOriginalSpin()">
263
+ <i class="fas fa-sync"></i> Spin
264
+ </button>
265
+ </div>
266
+ </div>
267
+ </div>
268
+
269
+ <!-- Completed Structure Viewer -->
270
+ <div class="comparison-viewer" style="flex: 0 1 48%; min-width: 450px; max-width: 48%;">
271
+ <h4 style="text-align: center; margin-bottom: 10px; font-size: 14px;">
272
+ <i class="fas fa-puzzle-piece"></i> Completed Structure
273
+ </h4>
274
+ <div id="completed-molecule-viewer" class="molecule-viewer" style="border: 2px solid #28a745; border-radius: 5px; width: 100%; height: 500px; position: relative;">
275
+ <div id="completed-ngl-viewer" style="width: 100%; height: 100%; position: absolute; top: 0; left: 0;"></div>
276
+ <div id="completed-viewer-controls" class="viewer-controls" style="display: none; justify-content: center; position: relative; z-index: 10;">
277
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.resetCompletedView()">
278
+ <i class="fas fa-home"></i> Reset
279
+ </button>
280
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleCompletedRepresentation()">
281
+ <i class="fas fa-eye"></i> <span id="completed-style-text">Mixed</span>
282
+ </button>
283
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleCompletedSpin()">
284
+ <i class="fas fa-sync"></i> Spin
285
+ </button>
286
+ </div>
287
+ </div>
288
+ </div>
289
+ </div>
290
+ </div>
291
+ </div>
292
+
293
+ <div class="prepared-structure-preview" id="superimposed-structure-preview" style="display: none;">
294
+ <h3><i class="fas fa-layer-group"></i> Superimposed Structure View</h3>
295
+ <p class="card-description" style="margin-bottom: 15px;">
296
+ View both the original crystal structure (original colors) and completed structure (different chain colors) superimposed in the same viewer to see which residues were filled.
297
+ </p>
298
+ <div class="preview-content" style="display: flex; justify-content: center; width: 100%; padding: 0;">
299
+ <div class="structure-comparison-container" style="width: 100%; max-width: 1400px;">
300
+ <!-- Superimposed Structure Viewer -->
301
+ <div class="comparison-viewer" style="width: 100%;">
302
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
303
+ <div>
304
+ <span style="color: #007bff; font-weight: 600;"><i class="fas fa-dna"></i> Original Crystal Structure</span>
305
+ <span style="margin: 0 10px;">|</span>
306
+ <span style="color: #28a745; font-weight: 600;"><i class="fas fa-puzzle-piece"></i> Completed Structure</span>
307
+ </div>
308
+ </div>
309
+ <div id="superimposed-molecule-viewer" class="molecule-viewer" style="border: 2px solid #007bff; border-radius: 5px; width: 100%; height: 600px; position: relative;">
310
+ <div id="superimposed-ngl-viewer" style="width: 100%; height: 100%; position: absolute; top: 0; left: 0;"></div>
311
+ <div id="superimposed-viewer-controls" class="viewer-controls" style="display: none; gap: 10px;">
312
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.resetSuperimposedView()">
313
+ <i class="fas fa-home"></i> Reset
314
+ </button>
315
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleSuperimposedRepresentation()">
316
+ <i class="fas fa-eye"></i> <span id="superimposed-style-text">Cartoon</span>
317
+ </button>
318
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleSuperimposedSpin()">
319
+ <i class="fas fa-sync"></i> Spin
320
+ </button>
321
+ </div>
322
+ </div>
323
+ </div>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ </div>
328
+ </div>
329
+
330
+ <!-- Structure Preparation Tab -->
331
+ <div id="structure-prep" class="tab-content">
332
+ <div class="card">
333
+ <h2><i class="fas fa-tools"></i> Structure Preparation for AMBER</h2>
334
+ <p class="card-description">Prepare the protein structure for AMBER force field generation by cleaning and modifying the PDB file.</p>
335
+
336
+ <div class="prep-sections">
337
+ <div class="prep-section">
338
+ <h3><i class="fas fa-trash"></i> Remove Components</h3>
339
+ <div class="prep-options">
340
+ <div class="prep-option">
341
+ <label class="checkbox-container">
342
+ <input type="checkbox" id="remove-water" checked disabled>
343
+ <span class="checkmark"></span>
344
+ Remove water molecules
345
+ </label>
346
+ <p class="option-description">Remove all water molecules (HOH, WAT, TIP3, etc.) from the structure</p>
347
+ </div>
348
+
349
+ <div class="prep-option">
350
+ <label class="checkbox-container">
351
+ <input type="checkbox" id="remove-ions" checked disabled>
352
+ <span class="checkmark"></span>
353
+ Remove ions
354
+ </label>
355
+ <p class="option-description">Remove all ions (Na+, Cl-, K+, Mg2+, etc.) from the structure</p>
356
+ </div>
357
+
358
+ <div class="prep-option">
359
+ <label class="checkbox-container">
360
+ <input type="checkbox" id="remove-hydrogens" checked disabled>
361
+ <span class="checkmark"></span>
362
+ Remove hydrogen atoms
363
+ </label>
364
+ <p class="option-description">Remove all hydrogen atoms from the protein structure</p>
365
+ </div>
366
+ </div>
367
+ </div>
368
+
369
+ <div class="prep-section">
370
+ <h3><i class="fas fa-plus-circle"></i> Add Capping Groups and Select Protein Chains</h3>
371
+ <div class="prep-options">
372
+ <div class="prep-option">
373
+ <label class="checkbox-container">
374
+ <input type="checkbox" id="add-nme" checked>
375
+ <span class="checkmark"></span>
376
+ Add NME group (C-terminal)
377
+ </label>
378
+ <p class="option-description">Add N-methyl amide (NME) group to C-terminal residues</p>
379
+ </div>
380
+
381
+ <div class="prep-option">
382
+ <label class="checkbox-container">
383
+ <input type="checkbox" id="add-ace" checked>
384
+ <span class="checkmark"></span>
385
+ Add ACE group (N-terminal)
386
+ </label>
387
+ <p class="option-description">Add acetyl (ACE) group to N-terminal residues</p>
388
+ </div>
389
+
390
+ <div class="form-group">
391
+ <label>Preserve Chains for FF Generation:</label>
392
+ <div id="chain-selection" class="multi-checkbox-group">
393
+ <!-- Chain checkboxes will be rendered here -->
394
+ </div>
395
+ <small class="form-help">Select one or more protein chains to include in preparation</small>
396
+ </div>
397
+ </div>
398
+ </div>
399
+
400
+ <div class="prep-section">
401
+ <h3><i class="fas fa-pills"></i> Ligand Handling</h3>
402
+ <div class="prep-options">
403
+ <div class="prep-option">
404
+ <label class="checkbox-container">
405
+ <input type="checkbox" id="preserve-ligands">
406
+ <span class="checkmark"></span>
407
+ Preserve ligands
408
+ </label>
409
+ <p class="option-description">Keep ligands in the structure and append them at the end</p>
410
+ </div>
411
+
412
+ <div class="prep-option">
413
+ <div class="checkbox-with-button">
414
+ <label class="checkbox-container">
415
+ <input type="checkbox" id="separate-ligands">
416
+ <span class="checkmark"></span>
417
+ Create separate ligand file
418
+ </label>
419
+ <button class="btn btn-sm btn-outline-primary" id="download-ligand" disabled>
420
+ <i class="fas fa-download"></i>
421
+ </button>
422
+ </div>
423
+ <p class="option-description">Extract ligands to a separate PDB file for individual processing</p>
424
+ </div>
425
+
426
+ <div class="form-group">
427
+ <label>Select Ligands to Preserve</label>
428
+ <div id="ligand-selection" class="multi-checkbox-group">
429
+ <!-- Ligand checkboxes will be rendered here -->
430
+ </div>
431
+ <small class="form-help">Tick ligands to include. Unselected ligands will be ignored.</small>
432
+ </div>
433
+ </div>
434
+ </div>
435
+
436
+ </div>
437
+
438
+ <div class="prep-actions">
439
+ <button class="btn btn-primary" id="prepare-structure">
440
+ <i class="fas fa-magic"></i> Prepare Structure
441
+ </button>
442
+ <button class="btn btn-secondary" id="preview-prepared">
443
+ <i class="fas fa-eye"></i> Preview Prepared Structure
444
+ </button>
445
+ <button class="btn btn-info" id="download-prepared">
446
+ <i class="fas fa-download"></i> Download Prepared PDB
447
+ </button>
448
+ </div>
449
+
450
+ <div class="prep-status" id="prep-status" style="display: none;">
451
+ <h3><i class="fas fa-info-circle"></i> Preparation Status</h3>
452
+ <div class="status-content" id="prep-status-content">
453
+ <!-- Status information will be displayed here -->
454
+ </div>
455
+ </div>
456
+
457
+ <div class="prepared-structure-preview" id="prepared-structure-preview" style="display: none;">
458
+ <h3><i class="fas fa-eye"></i> Prepared Structure Preview</h3>
459
+ <div class="preview-content">
460
+ <div class="structure-info">
461
+ <p><strong>Original atoms</strong> <span style="font-size:0.9em; color:#6c757d;">(protein without H, before capping):</span> <span id="original-atoms"></span></p>
462
+ <p><strong>Prepared atoms</strong> <span style="font-size:0.9em; color:#6c757d;">(protein without H, after capping):</span> <span id="prepared-atoms"></span></p>
463
+ <p><strong>Removed components:</strong> <span id="removed-components"></span></p>
464
+ <p><strong>Added capping groups:</strong> <span id="added-capping"></span></p>
465
+ <p><strong>Ligands preserved:</strong> <span id="preserved-ligands"></span></p>
466
+ </div>
467
+ <div class="structure-visualization">
468
+ <div id="prepared-molecule-viewer" class="molecule-viewer">
469
+ <div id="prepared-ngl-viewer" style="width: 100%; height: 100%; min-height: 300px;"></div>
470
+ <div id="prepared-viewer-controls" class="viewer-controls" style="display: none;">
471
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.resetPreparedView()">
472
+ <i class="fas fa-home"></i> Reset View
473
+ </button>
474
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.togglePreparedRepresentation()">
475
+ <i class="fas fa-eye"></i> <span id="prepared-style-text">Mixed View</span>
476
+ </button>
477
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.togglePreparedSpin()">
478
+ <i class="fas fa-sync"></i> Spin
479
+ </button>
480
+ </div>
481
+ </div>
482
+ </div>
483
+ </div>
484
+ </div>
485
+
486
+ <!-- Docking Section (visible only when ligands are preserved and present) -->
487
+ <div class="card plumed-section-card" id="docking-section" style="display: none; margin-top: 20px;">
488
+ <h2 class="plumed-toggle-header" id="docking-toggle-header">
489
+ <i class="fas fa-vial"></i> Ligand Docking
490
+ <i class="fas fa-chevron-down toggle-icon" id="docking-toggle-icon"></i>
491
+ </h2>
492
+ <p class="section-description">
493
+ Configure docking for preserved ligands using AutoDock Vina and Meeko. Select which ligands to dock,
494
+ define the Vina bounding box with live visualization, then run docking and choose poses.
495
+ </p>
496
+ <div class="custom-plumed-section" id="docking-content-section">
497
+ <!-- Ligand Selection -->
498
+ <div class="form-group" style="margin-bottom: 20px;">
499
+ <label><i class="fas fa-pills"></i> Select Ligands to Dock</label>
500
+ <p class="option-description" style="margin-bottom: 10px;">
501
+ Choose which preserved ligands should be included in docking calculations.
502
+ </p>
503
+ <div id="docking-ligand-selection" class="multi-checkbox-group">
504
+ <!-- Ligand checkboxes will be rendered here -->
505
+ </div>
506
+ </div>
507
+
508
+ <!-- Collapsible: Docking Setup (Visualization + Box Dimensions) -->
509
+ <div class="docking-setup-collapsible" style="margin-top: 10px; border: 1px solid #dee2e6; border-radius: 8px; overflow: hidden;">
510
+ <div class="docking-setup-header" id="docking-setup-toggle" style="background: linear-gradient(135deg, #6f42c1 0%, #8e5dd4 100%); color: white; padding: 12px 15px; cursor: pointer; display: flex; justify-content: space-between; align-items: center;">
511
+ <span><i class="fas fa-cube"></i> Docking Search Space Setup</span>
512
+ <i class="fas fa-chevron-up" id="docking-setup-toggle-icon" style="transition: transform 0.3s ease;"></i>
513
+ </div>
514
+ <div class="docking-setup-content" id="docking-setup-content" style="padding: 15px; background: white;">
515
+ <!-- Docking Search Space Visualization -->
516
+ <div class="prepared-structure-preview" id="docking-structure-preview">
517
+ <h4 style="margin-top: 0;"><i class="fas fa-eye"></i> Search Space Visualization</h4>
518
+ <p class="card-description" style="margin-bottom: 10px;">
519
+ The protein–ligand system is shown below. For each selected ligand, a bounding box (10×10×10 Å by default)
520
+ represents the Vina search space. Adjust box dimensions below to update the visualization live.
521
+ </p>
522
+ <!-- NGL Viewer - Matching section 2 aspect ratio -->
523
+ <div id="docking-ngl-viewer" class="molecule-viewer" style="border: 2px solid #6f42c1; border-radius: 5px; width: 100%; max-width: 700px; height: 500px; position: relative; margin: 0 auto;">
524
+ <!-- Docking NGL visualization will be added here -->
525
+ </div>
526
+ </div>
527
+
528
+ <!-- Box Dimensions - Below Visualization -->
529
+ <div id="docking-box-controls" style="margin-top: 15px; background: #f8f9fa; padding: 15px; border-radius: 5px; border: 1px solid #dee2e6;">
530
+ <h5 style="margin-top: 0; margin-bottom: 15px;"><i class="fas fa-sliders-h"></i> Box Dimensions for Selected Ligands</h5>
531
+ <div id="docking-setup-list" style="display: flex; flex-wrap: wrap; gap: 15px;">
532
+ <!-- Per-ligand box controls will be rendered here in a horizontal layout -->
533
+ </div>
534
+ </div>
535
+
536
+ <div class="prep-actions" style="margin-top: 15px;">
537
+ <button class="btn btn-primary" id="run-docking">
538
+ <i class="fas fa-vial"></i> Run Docking with Above Settings
539
+ </button>
540
+ </div>
541
+ </div>
542
+ </div>
543
+ <div id="docking-status" class="status-message" style="display: none; margin-top: 10px;"></div>
544
+
545
+ <div id="docking-poses-container" style="display: none; margin-top: 20px;">
546
+ <h4><i class="fas fa-project-diagram"></i> Visualize Binding Poses</h4>
547
+ <p class="option-description">
548
+ Browse through docked poses for each ligand. Use the navigation arrows to view different binding modes.
549
+ Select your preferred pose for the simulation.
550
+ </p>
551
+
552
+ <!-- Ligand selector tabs -->
553
+ <div id="docking-ligand-tabs" class="docking-ligand-tabs">
554
+ <!-- Ligand tabs will be rendered here -->
555
+ </div>
556
+
557
+ <!-- 3D Viewer with pose navigation -->
558
+ <div class="docking-poses-viewer-wrapper">
559
+ <div id="docking-poses-viewer" class="docking-poses-viewer"></div>
560
+
561
+ <!-- Pose navigation controls overlay -->
562
+ <div class="pose-nav-controls">
563
+ <button type="button" class="pose-nav-btn pose-nav-prev" id="pose-prev-btn" title="Previous Pose">
564
+ <i class="fas fa-chevron-left"></i>
565
+ </button>
566
+ <div class="pose-info-display">
567
+ <div class="pose-mode-label" id="pose-mode-label">Original Ligand</div>
568
+ <div class="pose-energy-label" id="pose-energy-label"></div>
569
+ <div class="pose-color-legend">
570
+ <span><span class="legend-dot original"></span> Original (green)</span>
571
+ <span><span class="legend-dot docked"></span> Docked (coral)</span>
572
+ </div>
573
+ </div>
574
+ <button type="button" class="pose-nav-btn pose-nav-next" id="pose-next-btn" title="Next Pose">
575
+ <i class="fas fa-chevron-right"></i>
576
+ </button>
577
+ </div>
578
+ </div>
579
+
580
+ <!-- Pose selection summary -->
581
+ <div id="docking-poses-list" class="docking-poses-selection">
582
+ <!-- Radio buttons for final selection will be rendered here -->
583
+ </div>
584
+
585
+ <div class="prep-actions" style="margin-top: 15px;">
586
+ <button class="btn btn-success" id="apply-docking-poses">
587
+ <i class="fas fa-check"></i> Use Selected Pose
588
+ </button>
589
+ </div>
590
+ </div>
591
+ </div>
592
+ </div>
593
+ </div>
594
+ </div>
595
+
596
+ <!-- Simulation Parameters Tab -->
597
+ <div id="simulation-params" class="tab-content">
598
+ <div class="card">
599
+ <h2><i class="fas fa-sliders-h"></i> Simulation Parameters</h2>
600
+
601
+ <div class="params-grid">
602
+ <div class="param-section">
603
+ <h3><i class="fas fa-cube"></i> System Setup</h3>
604
+ <div class="form-group">
605
+ <label for="box-type">Box Type:</label>
606
+ <select id="box-type">
607
+ <option value="cuboid">Cuboid</option>
608
+ </select>
609
+ </div>
610
+ <div class="form-group">
611
+ <label for="box-size">
612
+ Distance (Å):
613
+ <i class="fas fa-info-circle"
614
+ style="color: #007bff; margin-left: 5px; cursor: help;"
615
+ data-toggle="tooltip"
616
+ data-placement="top"
617
+ data-html="true"
618
+ title="The minimum distance between any atom originally present in solute and the edge of the periodic box is given by the distance parameter.">
619
+ </i>
620
+ </label>
621
+ <input type="number" id="box-size" value="10" step="1" min="5">
622
+ </div>
623
+ </div>
624
+
625
+ <div class="param-section" id="ligand-forcefield-section" style="display: none;">
626
+ <h3><i class="fas fa-atom"></i> Ligand Force Field</h3>
627
+ <div class="form-group">
628
+ <label for="ligand-forcefield">Ligand Force Field:</label>
629
+ <select id="ligand-forcefield">
630
+ <option value="gaff2">gaff2</option>
631
+ <option value="gaff">gaff</option>
632
+ </select>
633
+ </div>
634
+ <div class="form-group">
635
+ <button type="button" class="btn btn-primary" onclick="mdPipeline.generateLigandFF(event)">
636
+ <i class="fas fa-cogs"></i> Generate FF for Ligand
637
+ </button>
638
+ </div>
639
+ </div>
640
+
641
+ <div class="param-section">
642
+ <h3><i class="fas fa-flask"></i> Force Field & Water Model</h3>
643
+ <div class="form-group">
644
+ <label for="force-field">Protein Force Field:</label>
645
+ <select id="force-field">
646
+ <option value="ff14SB">ff14SB</option>
647
+ <option value="ff19SB">ff19SB</option>
648
+ </select>
649
+ </div>
650
+ <div class="form-group">
651
+ <label for="water-model">Water Model:</label>
652
+ <select id="water-model">
653
+ <option value="tip3p">TIP3P</option>
654
+ <option value="spce">SPCE</option>
655
+ </select>
656
+ </div>
657
+ <div class="form-group">
658
+ <label for="add-ions">Add Ions:</label>
659
+ <div class="ion-controls">
660
+ <select id="add-ions">
661
+ <option value="None">None</option>
662
+ <option value="Na+">Na+</option>
663
+ <option value="Cl-">Cl-</option>
664
+ </select>
665
+ <button type="button" class="btn btn-sm btn-outline-primary" onclick="mdPipeline.calculateNetCharge(event)">
666
+ <i class="fas fa-calculator"></i> Net Charge
667
+ </button>
668
+ </div>
669
+ </div>
670
+ </div>
671
+
672
+ <div class="param-section">
673
+ <h3><i class="fas fa-thermometer-half"></i> Temperature & Pressure</h3>
674
+ <div class="form-group">
675
+ <label for="temperature">Temperature (K):</label>
676
+ <input type="number" id="temperature" value="300" step="5" min="200" max="400">
677
+ </div>
678
+ <div class="form-group">
679
+ <label for="pressure">Pressure (atm):</label>
680
+ <input type="number" id="pressure" value="1.0" step="0.1" min="0.1">
681
+ </div>
682
+ <div class="form-group">
683
+ <label for="coupling-type">Thermostat:</label>
684
+ <select id="coupling-type">
685
+ <option value="langevin">Langevin</option>
686
+ </select>
687
+ </div>
688
+ </div>
689
+
690
+ <div class="param-section">
691
+ <h3><i class="fas fa-clock"></i> Integration Parameters</h3>
692
+ <div class="form-group">
693
+ <label for="timestep">Time Step (ps):</label>
694
+ <input type="number" id="timestep" value="0.002" step="0.001" min="0.001" max="0.005">
695
+ </div>
696
+ <div class="form-group">
697
+ <label for="cutoff">Cutoff Distance (Ang):</label>
698
+ <input type="number" id="cutoff" value="10.0" step="1" min="8" max="20">
699
+ </div>
700
+ <div class="form-group">
701
+ <label for="electrostatic">Electrostatic:</label>
702
+ <select id="electrostatic">
703
+ <option value="pme">PME</option>
704
+ </select>
705
+ </div>
706
+ </div>
707
+
708
+ </div>
709
+ </div>
710
+ </div>
711
+
712
+ <!-- Simulation Steps Tab -->
713
+ <div id="simulation-steps" class="tab-content">
714
+ <div class="card">
715
+ <h2><i class="fas fa-list-ol"></i> Simulation Steps Configuration</h2>
716
+
717
+ <div class="steps-container">
718
+ <div class="step-item">
719
+ <div class="step-header">
720
+ <h3><i class="fas fa-lock"></i> Restrained Minimization</h3>
721
+ <label class="switch">
722
+ <input type="checkbox" id="enable-restrained-min" checked disabled>
723
+ <span class="slider"></span>
724
+ </label>
725
+ </div>
726
+ <div class="step-content" id="restrained-min-content">
727
+ <div class="form-row">
728
+ <div class="form-group">
729
+ <label for="restrained-steps">Steps:</label>
730
+ <input type="number" id="restrained-steps" value="10000" step="100" min="100">
731
+ </div>
732
+ <div class="form-group">
733
+ <label for="restrained-force">Force Constant (kJ/mol/Ų):</label>
734
+ <input type="number" id="restrained-force" value="10" step="1" min="1">
735
+ </div>
736
+ </div>
737
+ </div>
738
+ </div>
739
+
740
+ <div class="step-item">
741
+ <div class="step-header">
742
+ <h3><i class="fas fa-compress"></i> Minimization</h3>
743
+ <label class="switch">
744
+ <input type="checkbox" id="enable-minimization" checked disabled>
745
+ <span class="slider"></span>
746
+ </label>
747
+ </div>
748
+ <div class="step-content" id="minimization-content">
749
+ <div class="form-row">
750
+ <div class="form-group">
751
+ <label for="min-steps">Steps:</label>
752
+ <input type="number" id="min-steps" value="20000" step="500" min="1000">
753
+ </div>
754
+ <div class="form-group">
755
+ <label for="min-algorithm">Algorithm:</label>
756
+ <select id="min-algorithm">
757
+ <option value="cg">Conjugate Gradient</option>
758
+ </select>
759
+ </div>
760
+ </div>
761
+ </div>
762
+ </div>
763
+
764
+ <div class="step-item">
765
+ <div class="step-header">
766
+ <h3><i class="fas fa-fire"></i> NPT Heating</h3>
767
+ <label class="switch">
768
+ <input type="checkbox" id="enable-nvt" checked disabled>
769
+ <span class="slider"></span>
770
+ </label>
771
+ </div>
772
+ <div class="step-content" id="nvt-content">
773
+ <div class="form-row">
774
+ <div class="form-group">
775
+ <label for="nvt-steps">Steps:</label>
776
+ <input type="number" id="nvt-steps" value="50000" step="5000" min="10000">
777
+ </div>
778
+ <div class="form-group">
779
+ <label for="nvt-temp">Target Temperature (K):</label>
780
+ <input type="number" id="nvt-temp" value="300" step="5" min="200">
781
+ </div>
782
+ </div>
783
+ </div>
784
+ </div>
785
+
786
+ <div class="step-item">
787
+ <div class="step-header">
788
+ <h3><i class="fas fa-compress-arrows-alt"></i> NPT Equilibration</h3>
789
+ <label class="switch">
790
+ <input type="checkbox" id="enable-npt" checked disabled>
791
+ <span class="slider"></span>
792
+ </label>
793
+ </div>
794
+ <div class="step-content" id="npt-content">
795
+ <div class="form-row">
796
+ <div class="form-group">
797
+ <label for="npt-steps">Steps:</label>
798
+ <input type="number" id="npt-steps" value="100000" step="10000" min="20000">
799
+ </div>
800
+ <div class="form-group">
801
+ <label for="npt-temp">Temperature (K):</label>
802
+ <input type="number" id="npt-temp" value="300" step="5" min="200">
803
+ </div>
804
+ <div class="form-group">
805
+ <label for="npt-pressure">Pressure (atm):</label>
806
+ <input type="number" id="npt-pressure" value="1.0" step="0.1" min="0.1">
807
+ </div>
808
+ </div>
809
+ </div>
810
+ </div>
811
+
812
+ <div class="step-item">
813
+ <div class="step-header">
814
+ <h3><i class="fas fa-play"></i> Production Run(NPT)</h3>
815
+ <label class="switch">
816
+ <input type="checkbox" id="enable-production" checked disabled>
817
+ <span class="slider"></span>
818
+ </label>
819
+ </div>
820
+ <div class="step-content" id="production-content">
821
+ <div class="form-row">
822
+ <div class="form-group">
823
+ <label for="prod-steps">Steps:</label>
824
+ <input type="number" id="prod-steps" value="1000000" step="100000" min="100000">
825
+ </div>
826
+ <div class="form-group">
827
+ <label for="prod-temp">Temperature (K):</label>
828
+ <input type="number" id="prod-temp" value="300" step="5" min="200">
829
+ </div>
830
+ <div class="form-group">
831
+ <label for="prod-pressure">Pressure (atm):</label>
832
+ <input type="number" id="prod-pressure" value="1.0" step="0.1" min="0.1">
833
+ </div>
834
+ </div>
835
+ </div>
836
+ </div>
837
+ </div>
838
+ </div>
839
+ </div>
840
+
841
+ <!-- File Generation Tab -->
842
+ <div id="file-generation" class="tab-content">
843
+ <!-- Guidance Note Card -->
844
+ <div class="plumed-citation-note">
845
+ <i class="fas fa-info-circle"></i>
846
+ <div class="citation-content">
847
+ <p>Click on <strong>Generate All Files</strong>. If you want to run <strong>biased simulations</strong> using PLUMED, proceed to the <strong>next section (PLUMED)</strong> to configure collective variables. Otherwise, download all files here and you're all set!</p>
848
+ </div>
849
+ </div>
850
+
851
+ <div class="card">
852
+ <h2><i class="fas fa-file-download"></i> Generate Simulation Files</h2>
853
+
854
+ <div class="generation-controls">
855
+ <button class="btn btn-primary" id="generate-files">
856
+ <i class="fas fa-magic"></i> Generate All Files
857
+ </button>
858
+ <button class="btn btn-secondary" id="preview-files">
859
+ <i class="fas fa-eye"></i> Preview Files
860
+ </button>
861
+ <button class="btn btn-info" id="preview-solvated">
862
+ <i class="fas fa-tint"></i> Preview Solvated Protein
863
+ </button>
864
+ <button class="btn btn-success" id="download-solvated">
865
+ <i class="fas fa-download"></i> Download Solvated Protein
866
+ </button>
867
+ </div>
868
+
869
+ <div class="files-preview" id="files-preview" style="display: none;">
870
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
871
+ <h3 style="margin: 0;"><i class="fas fa-files"></i> Generated Files</h3>
872
+ <button class="btn btn-primary" id="add-simulation-file" style="margin-left: 10px;">
873
+ <i class="fas fa-plus"></i> Add Simulation File
874
+ </button>
875
+ </div>
876
+ <div class="files-list" id="files-list">
877
+ <!-- Generated files will be listed here -->
878
+ </div>
879
+ </div>
880
+
881
+ <div class="download-section" id="download-section" style="display: none;">
882
+ <h3><i class="fas fa-download"></i> Download Files</h3>
883
+ <div class="download-options">
884
+ <button class="btn btn-success" id="download-zip">
885
+ <i class="fas fa-file-archive"></i> Download All as ZIP
886
+ </button>
887
+
888
+ </div>
889
+ </div>
890
+
891
+ <div class="simulation-summary" id="simulation-summary" style="display: none;">
892
+ <h3><i class="fas fa-chart-line"></i> Simulation Summary</h3>
893
+ <div class="summary-content" id="summary-content">
894
+ <!-- Simulation summary will be displayed here -->
895
+ </div>
896
+ </div>
897
+ </div>
898
+ </div>
899
+
900
+ <!-- PLUMED Section -->
901
+ <div id="plumed" class="tab-content">
902
+ <!-- PLUMED Citation Note -->
903
+ <div class="plumed-citation-note">
904
+ <i class="fas fa-info-circle"></i>
905
+ <div class="citation-content">
906
+ <p><strong>Note:</strong> All CVs are taken from <a href="https://www.plumed.org/doc-v2.9/user-doc/html/index.html" target="_blank" rel="noopener noreferrer"><strong>PLUMED v2.9</strong> <i class="fas fa-external-link-alt"></i></a> documentation.</p>
907
+ <p>If you use PLUMED in your research, please cite it. <a href="https://www.plumed.org/cite" target="_blank" rel="noopener noreferrer">Citation information <i class="fas fa-external-link-alt"></i></a></p>
908
+ </div>
909
+ </div>
910
+
911
+ <!-- PLUMED Collective Variables Section -->
912
+ <div class="card plumed-section-card">
913
+ <h2>
914
+ <i class="fas fa-chart-line"></i> PLUMED Collective Variables
915
+ </h2>
916
+ <p class="section-description">
917
+ Configure Collective Variables (CVs) for biased MD simulations. Select a CV from the sidebar to view documentation and examples.
918
+ </p>
919
+
920
+ <div class="plumed-container" id="plumed-container">
921
+ <!-- Left Sidebar: CV List -->
922
+ <div class="plumed-sidebar">
923
+ <div class="sidebar-header">
924
+ <h3><i class="fas fa-list"></i> Collective Variables</h3>
925
+ <div class="search-box">
926
+ <input type="text" id="cv-search" placeholder="Search CVs..." class="search-input">
927
+ <i class="fas fa-search search-icon"></i>
928
+ </div>
929
+ </div>
930
+ <div class="cv-list" id="cv-list">
931
+ <!-- CV items will be populated by JavaScript -->
932
+ </div>
933
+ </div>
934
+
935
+ <!-- Right Panel: Documentation and Editor -->
936
+ <div class="plumed-content">
937
+ <div class="content-header" id="content-header" style="display: none;">
938
+ <h3 id="cv-title"></h3>
939
+ </div>
940
+
941
+ <div class="content-body" id="content-body">
942
+ <div class="welcome-message" id="welcome-message">
943
+ <i class="fas fa-hand-pointer fa-3x"></i>
944
+ <h3>Select a Collective Variable</h3>
945
+ <p>Choose a CV from the left sidebar to view its documentation and configure it for your simulation.</p>
946
+ </div>
947
+
948
+ <!-- Documentation Section -->
949
+ <div class="cv-documentation" id="cv-documentation" style="display: none;">
950
+ <div class="doc-section" id="cv-module-section" style="display: none;">
951
+ <h4><i class="fas fa-puzzle-piece"></i> Module</h4>
952
+ <div class="doc-content" id="cv-module"></div>
953
+ </div>
954
+
955
+ <div class="doc-section">
956
+ <h4><i class="fas fa-info-circle"></i> Description</h4>
957
+ <div class="doc-content" id="cv-description"></div>
958
+ </div>
959
+
960
+ <div class="doc-section">
961
+ <h4><i class="fas fa-code"></i> Syntax</h4>
962
+ <div class="doc-content">
963
+ <pre class="syntax-box" id="cv-syntax"></pre>
964
+ </div>
965
+ </div>
966
+
967
+ <div class="doc-section" id="cv-glossary-section" style="display: none;">
968
+ <h4><i class="fas fa-book"></i> Glossary of keywords and components</h4>
969
+ <div class="doc-content" id="cv-glossary"></div>
970
+ </div>
971
+
972
+ <div class="doc-section" id="cv-options-section" style="display: none;">
973
+ <h4><i class="fas fa-list-ul"></i> Options</h4>
974
+ <div class="doc-content" id="cv-options"></div>
975
+ </div>
976
+
977
+ <div class="doc-section" id="cv-components-section" style="display: none;">
978
+ <h4><i class="fas fa-cogs"></i> Components</h4>
979
+ <div class="doc-content" id="cv-components"></div>
980
+ </div>
981
+
982
+ <div class="doc-section">
983
+ <h4><i class="fas fa-book"></i> Examples</h4>
984
+ <div class="doc-content">
985
+ <pre class="example-box" id="cv-example"></pre>
986
+ </div>
987
+ </div>
988
+
989
+ <div class="doc-section" id="cv-notes-section" style="display: none;">
990
+ <h4><i class="fas fa-sticky-note"></i> Notes</h4>
991
+ <div class="doc-content" id="cv-notes"></div>
992
+ </div>
993
+
994
+ <div class="doc-section" id="cv-related-section" style="display: none;">
995
+ <h4><i class="fas fa-link"></i> Related Collective Variables</h4>
996
+ <div class="doc-content" id="cv-related"></div>
997
+ </div>
998
+ </div>
999
+
1000
+ <!-- Editable Editor Section -->
1001
+ <div class="cv-editor-section" id="cv-editor-section" style="display: none;">
1002
+ <div class="editor-header">
1003
+ <h4><i class="fas fa-edit"></i> Your Configuration</h4>
1004
+ <div class="editor-actions">
1005
+ <button class="btn btn-sm btn-info" id="copy-config">
1006
+ <i class="fas fa-copy"></i> Copy
1007
+ </button>
1008
+ <button class="btn btn-sm btn-primary" id="view-pdb">
1009
+ <i class="fas fa-eye"></i> View PDB
1010
+ </button>
1011
+ <button class="btn btn-sm btn-secondary" id="reset-cv">
1012
+ <i class="fas fa-redo"></i> Reset to Example
1013
+ </button>
1014
+ <button class="btn btn-sm btn-success" id="save-config">
1015
+ <i class="fas fa-save"></i> Save
1016
+ </button>
1017
+ </div>
1018
+ </div>
1019
+ <textarea id="cv-editor" class="cv-editor" placeholder="Enter your PLUMED configuration here..."></textarea>
1020
+ <div class="editor-footer">
1021
+ <span class="char-count"><span id="char-count">0</span> characters</span>
1022
+ <span class="line-count"><span id="line-count">0</span> lines</span>
1023
+ </div>
1024
+ </div>
1025
+
1026
+ <!-- Saved Configurations -->
1027
+ <div class="saved-configs" id="saved-configs" style="display: none;">
1028
+ <h4><i class="fas fa-bookmark"></i> Saved Configurations</h4>
1029
+ <div class="configs-list" id="configs-list">
1030
+ <!-- Saved configs will be shown here -->
1031
+ </div>
1032
+ </div>
1033
+ </div>
1034
+ </div>
1035
+ </div>
1036
+ </div>
1037
+
1038
+ <!-- Custom PLUMED File Section -->
1039
+ <div class="card plumed-section-card" id="custom-plumed-card">
1040
+ <h2 class="plumed-toggle-header" id="custom-plumed-toggle-header">
1041
+ <i class="fas fa-file-code"></i> Write Custom PLUMED File
1042
+ <i class="fas fa-chevron-down toggle-icon" id="custom-plumed-toggle-icon"></i>
1043
+ </h2>
1044
+ <p class="section-description">
1045
+ Write your custom PLUMED configuration from scratch. This editor allows you to create a complete PLUMED input file.
1046
+ </p>
1047
+
1048
+ <div class="custom-plumed-section" id="custom-plumed-section">
1049
+ <div class="custom-editor-header">
1050
+ <h4><i class="fas fa-edit"></i> Custom PLUMED Configuration</h4>
1051
+ <div class="editor-actions">
1052
+ <button class="btn btn-sm btn-info" id="copy-custom-plumed">
1053
+ <i class="fas fa-copy"></i> Copy
1054
+ </button>
1055
+ <button class="btn btn-sm btn-primary" id="view-pdb-custom">
1056
+ <i class="fas fa-eye"></i> View PDB
1057
+ </button>
1058
+ <button class="btn btn-sm btn-success" id="download-custom-plumed">
1059
+ <i class="fas fa-save"></i> Save
1060
+ </button>
1061
+ <button class="btn btn-sm btn-secondary" id="clear-custom-plumed">
1062
+ <i class="fas fa-trash"></i> Clear
1063
+ </button>
1064
+ </div>
1065
+ </div>
1066
+ <textarea id="custom-plumed-editor" class="custom-plumed-editor" placeholder="Write your PLUMED configuration here...&#10;&#10;Example:&#10;# PLUMED input file&#10;d1: DISTANCE ATOMS=1,2&#10;PRINT ARG=d1 FILE=colvar.dat"></textarea>
1067
+ <div class="editor-footer">
1068
+ <span class="char-count"><span id="custom-char-count">0</span> characters</span>
1069
+ <span class="line-count"><span id="custom-line-count">0</span> lines</span>
1070
+ </div>
1071
+ </div>
1072
+ </div>
1073
+
1074
+ <!-- Generate Simulation Files Section -->
1075
+ <div class="card plumed-section-card" id="generate-simulation-files-card">
1076
+ <h2 class="plumed-toggle-header" id="generate-simulation-files-toggle-header">
1077
+ <i class="fas fa-cogs"></i> Generate Simulation Files
1078
+ <i class="fas fa-chevron-down toggle-icon" id="generate-simulation-files-toggle-icon"></i>
1079
+ </h2>
1080
+ <p class="section-description">
1081
+ Generate and download simulation files for your PLUMED setup.
1082
+ </p>
1083
+
1084
+ <div class="generate-simulation-files-section" id="generate-simulation-files-section">
1085
+ <div class="generation-controls" style="display: flex; gap: 10px; padding: 20px;">
1086
+ <button class="btn btn-primary" id="plumed-generate-files">
1087
+ <i class="fas fa-magic"></i> Generate Files
1088
+ </button>
1089
+ <button class="btn btn-secondary" id="plumed-preview-files">
1090
+ <i class="fas fa-eye"></i> Preview Files
1091
+ </button>
1092
+ <button class="btn btn-success" id="plumed-download-files">
1093
+ <i class="fas fa-download"></i> Download Files
1094
+ </button>
1095
+ </div>
1096
+ </div>
1097
+ </div>
1098
+ </div>
1099
+ </main>
1100
+
1101
+ <!-- Step Navigation Controls -->
1102
+ <div class="step-navigation">
1103
+ <button class="nav-btn prev-btn" id="prev-tab" disabled>
1104
+ <i class="fas fa-chevron-left"></i> Previous
1105
+ </button>
1106
+ <div class="step-indicator">
1107
+ <span id="current-step">1</span> of <span id="total-steps">7</span>
1108
+ </div>
1109
+ <button class="nav-btn next-btn" id="next-tab">
1110
+ Next <i class="fas fa-chevron-right"></i>
1111
+ </button>
1112
+ </div>
1113
+
1114
+ <!-- Footer -->
1115
+ <footer class="footer">
1116
+ <p>&copy; 2025 MD Simulation Pipeline. Built for molecular dynamics simulations.</p>
1117
+ </footer>
1118
+ </div>
1119
+
1120
+ <!-- PDB Viewer Modal -->
1121
+ <div class="modal" id="pdb-viewer-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; overflow: auto;">
1122
+ <div class="modal-content" style="max-width: 90%; max-height: 90vh; margin: 5vh auto; background: white; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
1123
+ <div class="modal-header" style="display: flex; justify-content: space-between; align-items: center; padding: 15px; border-bottom: 1px solid #ddd; background: #f8f9fa; border-radius: 8px 8px 0 0;">
1124
+ <h3 style="margin: 0;"><i class="fas fa-file-code"></i> Viewer PDB File</h3>
1125
+ <button class="btn btn-sm btn-secondary" id="close-pdb-modal" style="margin: 0;">
1126
+ <i class="fas fa-times"></i> Close
1127
+ </button>
1128
+ </div>
1129
+ <div class="modal-body" style="padding: 15px; overflow: auto; max-height: calc(90vh - 100px);">
1130
+ <div id="pdb-content-loading" style="text-align: center; padding: 20px;">
1131
+ <i class="fas fa-spinner fa-spin"></i> Loading PDB file...
1132
+ </div>
1133
+ <div id="pdb-content-error" style="display: none; color: #dc3545; padding: 20px; text-align: center;">
1134
+ <i class="fas fa-exclamation-triangle"></i> <span id="pdb-error-message"></span>
1135
+ </div>
1136
+ <pre id="pdb-content" style="display: none; font-family: 'Courier New', monospace; font-size: 12px; line-height: 1.4; background: #f8f9fa; padding: 15px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"></pre>
1137
+ </div>
1138
+ </div>
1139
+ </div>
1140
+
1141
+ <script src="../js/script.js"></script>
1142
+ <script src="../js/plumed_cv_docs.js"></script>
1143
+ <script src="../js/plumed.js"></script>
1144
+ </body>
1145
+ </html>
ambermdflow/html/plumed.html ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!-- PLUMED Collective Variables Section -->
2
+ <div id="plumed" class="tab-content">
3
+ <div class="card">
4
+ <h2><i class="fas fa-chart-line"></i> PLUMED Collective Variables</h2>
5
+ <p class="section-description">
6
+ Configure Collective Variables (CVs) for biased MD simulations. Select a CV from the sidebar to view documentation and examples.
7
+ </p>
8
+
9
+ <div class="plumed-container">
10
+ <!-- Left Sidebar: CV List -->
11
+ <div class="plumed-sidebar">
12
+ <div class="sidebar-header">
13
+ <h3><i class="fas fa-list"></i> Collective Variables</h3>
14
+ <div class="search-box">
15
+ <input type="text" id="cv-search" placeholder="Search CVs..." class="search-input">
16
+ <i class="fas fa-search search-icon"></i>
17
+ </div>
18
+ </div>
19
+ <div class="cv-list" id="cv-list">
20
+ <!-- CV items will be populated by JavaScript -->
21
+ </div>
22
+ </div>
23
+
24
+ <!-- Right Panel: Documentation and Editor -->
25
+ <div class="plumed-content">
26
+ <div class="content-header" id="content-header" style="display: none;">
27
+ <h3 id="cv-title"></h3>
28
+ <button class="btn btn-sm btn-secondary" id="reset-cv">
29
+ <i class="fas fa-redo"></i> Reset to Example
30
+ </button>
31
+ </div>
32
+
33
+ <div class="content-body" id="content-body">
34
+ <div class="welcome-message" id="welcome-message">
35
+ <i class="fas fa-hand-pointer fa-3x"></i>
36
+ <h3>Select a Collective Variable</h3>
37
+ <p>Choose a CV from the left sidebar to view its documentation and configure it for your simulation.</p>
38
+ </div>
39
+
40
+ <!-- Documentation Section -->
41
+ <div class="cv-documentation" id="cv-documentation" style="display: none;">
42
+ <div class="doc-section">
43
+ <h4><i class="fas fa-info-circle"></i> Description</h4>
44
+ <div class="doc-content" id="cv-description"></div>
45
+ </div>
46
+
47
+ <div class="doc-section">
48
+ <h4><i class="fas fa-code"></i> Syntax</h4>
49
+ <div class="doc-content">
50
+ <pre class="syntax-box" id="cv-syntax"></pre>
51
+ </div>
52
+ </div>
53
+
54
+ <div class="doc-section">
55
+ <h4><i class="fas fa-book"></i> Example</h4>
56
+ <div class="doc-content">
57
+ <pre class="example-box" id="cv-example"></pre>
58
+ </div>
59
+ </div>
60
+
61
+ <div class="doc-section" id="cv-components-section" style="display: none;">
62
+ <h4><i class="fas fa-cogs"></i> Components</h4>
63
+ <div class="doc-content" id="cv-components"></div>
64
+ </div>
65
+ </div>
66
+
67
+ <!-- Editable Editor Section -->
68
+ <div class="cv-editor-section" id="cv-editor-section" style="display: none;">
69
+ <div class="editor-header">
70
+ <h4><i class="fas fa-edit"></i> Your Configuration</h4>
71
+ <div class="editor-actions">
72
+ <button class="btn btn-sm btn-info" id="copy-config">
73
+ <i class="fas fa-copy"></i> Copy
74
+ </button>
75
+ <button class="btn btn-sm btn-success" id="save-config">
76
+ <i class="fas fa-save"></i> Save
77
+ </button>
78
+ </div>
79
+ </div>
80
+ <textarea id="cv-editor" class="cv-editor" placeholder="Enter your PLUMED configuration here..."></textarea>
81
+ <div class="editor-footer">
82
+ <span class="char-count"><span id="char-count">0</span> characters</span>
83
+ <span class="line-count"><span id="line-count">0</span> lines</span>
84
+ </div>
85
+ </div>
86
+
87
+ <!-- Saved Configurations -->
88
+ <div class="saved-configs" id="saved-configs" style="display: none;">
89
+ <h4><i class="fas fa-bookmark"></i> Saved Configurations</h4>
90
+ <div class="configs-list" id="configs-list">
91
+ <!-- Saved configs will be shown here -->
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+
ambermdflow/js/plumed.js ADDED
The diff for this file is too large to render. See raw diff
 
ambermdflow/js/plumed_cv_docs.js ADDED
The diff for this file is too large to render. See raw diff
 
ambermdflow/js/script.js ADDED
The diff for this file is too large to render. See raw diff
 
ambermdflow/structure_preparation.py ADDED
@@ -0,0 +1,1194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ AMBER Structure Preparation Script using MDAnalysis
4
+ Complete pipeline: extract protein, add caps, handle ligands
5
+ """
6
+
7
+ import glob
8
+ import os
9
+ import re
10
+ import subprocess
11
+ import sys
12
+ import shutil
13
+ import logging
14
+ from pathlib import Path
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ def run_command(cmd, description=""):
19
+ """Run a command and return success status"""
20
+ try:
21
+ print(f"Running: {description}")
22
+ print(f"Command: {cmd}")
23
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=120)
24
+ print(f"Return code: {result.returncode}")
25
+ if result.stdout:
26
+ print(f"STDOUT: {result.stdout}")
27
+ if result.stderr:
28
+ print(f"STDERR: {result.stderr}")
29
+ if result.returncode != 0:
30
+ print(f"Error: {result.stderr}")
31
+ return False
32
+ return True
33
+ except subprocess.TimeoutExpired:
34
+ print(f"Timeout: {description}")
35
+ return False
36
+ except Exception as e:
37
+ print(f"Error running {description}: {str(e)}")
38
+ return False
39
+
40
+ def extract_protein_only(pdb_content, output_file, selected_chains=None):
41
+ """Extract protein without hydrogens using MDAnalysis. Optionally restrict to selected chains."""
42
+ # Write input content to output file first
43
+ with open(output_file, 'w') as f:
44
+ f.write(pdb_content)
45
+
46
+ try:
47
+ # Run MDAnalysis command with the output file as input
48
+ chain_sel = ''
49
+ if selected_chains:
50
+ chain_filters = ' or '.join([f'chain {c}' for c in selected_chains])
51
+ chain_sel = f' and ({chain_filters})'
52
+ selection = f"protein{chain_sel} and not name H* 1H* 2H* 3H*"
53
+ abspath = os.path.abspath(output_file)
54
+ cmd = f'python -c "import MDAnalysis as mda; u=mda.Universe(\'{abspath}\'); u.select_atoms(\'{selection}\').write(\'{abspath}\')"'
55
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
56
+
57
+ if result.returncode != 0:
58
+ raise Exception(f"MDAnalysis error: {result.stderr}")
59
+
60
+ return True
61
+ except Exception as e:
62
+ print(f"Error in extract_protein_only: {e}")
63
+ return False
64
+
65
+ def add_capping_groups(input_file, output_file):
66
+ """Add ACE and NME capping groups using add_caps.py"""
67
+ add_caps_script = (Path(__file__).resolve().parent / "add_caps.py")
68
+ # First add caps
69
+ temp_capped = output_file.replace('.pdb', '_temp.pdb')
70
+ cmd = f"python {add_caps_script} -i {input_file} -o {temp_capped}"
71
+ if not run_command(cmd, f"Adding capping groups to {input_file}"):
72
+ return False
73
+
74
+ # Then add TER cards using awk
75
+ cmd = f"awk '/NME/{{nme=NR}} /ACE/ && nme && NR > nme {{print \"TER\"; nme=0}} {{print}}' {temp_capped} > {output_file}"
76
+ if not run_command(cmd, f"Adding TER cards to {temp_capped}"):
77
+ return False
78
+
79
+ # Clean up temp file
80
+ if os.path.exists(temp_capped):
81
+ os.remove(temp_capped)
82
+
83
+ return True
84
+
85
+
86
+ def replace_chain_in_pdb(target_pdb, chain_id, source_pdb):
87
+ """
88
+ Replace a specific chain in target_pdb with the chain from source_pdb.
89
+ Only performs replacement if the target actually contains the chain_id.
90
+ Used to merge ESMFold-minimized chains into 1_protein_no_hydrogens.pdb.
91
+ If the source has no ATOM lines (or none matching the chain), we do NOT
92
+ modify the target, to avoid wiping the protein when the minimized file is
93
+ empty or has an unexpected format.
94
+ """
95
+ with open(target_pdb, 'r') as f:
96
+ target_lines = f.readlines()
97
+ if not any(
98
+ ln.startswith(('ATOM', 'HETATM')) and len(ln) >= 22 and ln[21] == chain_id
99
+ for ln in target_lines
100
+ ):
101
+ return
102
+ with open(source_pdb, 'r') as f:
103
+ source_lines = f.readlines()
104
+ source_chain_lines = []
105
+ for ln in source_lines:
106
+ if ln.startswith(('ATOM', 'HETATM')) and len(ln) >= 22:
107
+ ch = ln[21]
108
+ if ch == 'A' or ch == chain_id:
109
+ source_chain_lines.append(ln[:21] + chain_id + ln[22:])
110
+ if not source_chain_lines:
111
+ # Fallback: minimized PDB may use chain ' ' or other; take all ATOM/HETATM.
112
+ for ln in source_lines:
113
+ if ln.startswith(('ATOM', 'HETATM')) and len(ln) >= 22:
114
+ source_chain_lines.append(ln[:21] + chain_id + ln[22:])
115
+ if not source_chain_lines:
116
+ return # Do not modify target: we have nothing to add; avoid wiping the protein.
117
+ filtered_target = [
118
+ ln for ln in target_lines
119
+ if not (ln.startswith(('ATOM', 'HETATM')) and len(ln) >= 22 and ln[21] == chain_id)
120
+ ]
121
+ combined = []
122
+ for ln in filtered_target:
123
+ if ln.startswith('END'):
124
+ combined.extend(source_chain_lines)
125
+ combined.append("TER\n")
126
+ combined.append(ln)
127
+ with open(target_pdb, 'w') as f:
128
+ f.writelines(combined)
129
+
130
+
131
+ def extract_selected_chains(pdb_content, output_file, selected_chains):
132
+ """Extract selected chains using PyMOL commands"""
133
+ try:
134
+ # Write input content to temp file
135
+ temp_input = output_file.replace('.pdb', '_temp_input.pdb')
136
+ with open(temp_input, 'w') as f:
137
+ f.write(pdb_content)
138
+
139
+ # Build chain selection string
140
+ chain_filters = ' or '.join([f'chain {c}' for c in selected_chains])
141
+ selection = f"({chain_filters}) and polymer.protein"
142
+
143
+ # Use PyMOL to extract chains
144
+ cmd = f'''python -c "
145
+ import pymol
146
+ pymol.finish_launching(['pymol', '-c'])
147
+ pymol.cmd.load('{temp_input}')
148
+ pymol.cmd.save('{output_file}', '{selection}')
149
+ pymol.cmd.quit()
150
+ "'''
151
+
152
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
153
+
154
+ # Clean up temp file
155
+ if os.path.exists(temp_input):
156
+ os.remove(temp_input)
157
+
158
+ if result.returncode != 0:
159
+ print(f"PyMOL chain extraction error: {result.stderr}")
160
+ return False
161
+
162
+ return True
163
+ except Exception as e:
164
+ print(f"Error extracting selected chains: {e}")
165
+ return False
166
+
167
+ def extract_selected_ligands(pdb_content, output_file, selected_ligands):
168
+ """Extract selected ligands using PyMOL commands.
169
+ selected_ligands: list of dicts with resn, chain, and optionally resi.
170
+ When resi is provided, use (resn X and chain Y and resi Z) to uniquely pick
171
+ one instance when the same ligand (resn) appears multiple times in the same chain.
172
+ """
173
+ try:
174
+ # Write input content to temp file
175
+ temp_input = output_file.replace('.pdb', '_temp_input.pdb')
176
+ with open(temp_input, 'w') as f:
177
+ f.write(pdb_content)
178
+
179
+ # Build ligand selection string (include resi when present to disambiguate duplicates)
180
+ parts = []
181
+ for lig in selected_ligands:
182
+ resn = lig.get('resn', '').strip()
183
+ chain = lig.get('chain', '').strip()
184
+ resi = lig.get('resi') if lig.get('resi') is not None else ''
185
+ resi = str(resi).strip() if resi else ''
186
+ if resn and chain:
187
+ if resi:
188
+ parts.append(f"(resn {resn} and chain {chain} and resi {resi})")
189
+ else:
190
+ parts.append(f"(resn {resn} and chain {chain})")
191
+ elif resn:
192
+ parts.append(f"resn {resn}")
193
+
194
+ if not parts:
195
+ # No ligands to extract
196
+ with open(output_file, 'w') as f:
197
+ f.write('\n')
198
+ return True
199
+
200
+ selection = ' or '.join(parts)
201
+
202
+ # Use PyMOL to extract ligands
203
+ cmd = f'''python -c "
204
+ import pymol
205
+ pymol.finish_launching(['pymol', '-c'])
206
+ pymol.cmd.load('{temp_input}')
207
+ pymol.cmd.save('{output_file}', '{selection}')
208
+ pymol.cmd.quit()
209
+ "'''
210
+
211
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
212
+
213
+ # Clean up temp file
214
+ if os.path.exists(temp_input):
215
+ os.remove(temp_input)
216
+
217
+ if result.returncode != 0:
218
+ print(f"PyMOL ligand extraction error: {result.stderr}")
219
+ return False
220
+
221
+ return True
222
+ except Exception as e:
223
+ print(f"Error extracting selected ligands: {e}")
224
+ return False
225
+
226
+ def extract_ligands(pdb_content, output_file, ligand_residue_name=None, selected_ligands=None):
227
+ """Extract ligands using MDAnalysis. Optionally restrict to selected ligands (list of dicts with resn, chain, resi)."""
228
+ # Write input content to output file first
229
+ with open(output_file, 'w') as f:
230
+ f.write(pdb_content)
231
+
232
+ try:
233
+ # Run MDAnalysis command with the output file as input
234
+ if selected_ligands:
235
+ # Build selection from provided ligand list; include resid when present to disambiguate
236
+ # when the same ligand (resn) appears multiple times in the same chain (GOL-A-1, GOL-A-2)
237
+ parts = []
238
+ for lig in selected_ligands:
239
+ resn = lig.get('resn', '').strip()
240
+ chain = lig.get('chain', '').strip()
241
+ resi = lig.get('resi') if lig.get('resi') is not None else ''
242
+ resi = str(resi).strip() if resi else ''
243
+ if resn and chain:
244
+ if resi:
245
+ # Extract leading digits for resid in case of insertion codes (e.g. 100A -> 100)
246
+ m = re.search(r'^(-?\d+)', resi)
247
+ resid_val = m.group(1) if m else resi
248
+ parts.append(f"(resname {resn} and segid {chain} and resid {resid_val})")
249
+ else:
250
+ parts.append(f"(resname {resn} and segid {chain})")
251
+ elif resn:
252
+ parts.append(f"resname {resn}")
253
+ if parts:
254
+ selection = ' or '.join(parts)
255
+ cmd = f'''python -c "
256
+ import MDAnalysis as mda
257
+ u = mda.Universe('{output_file}')
258
+ u.select_atoms('{selection}').write('{output_file}')
259
+ "'''
260
+ else:
261
+ cmd = f"python -c \"open('{output_file}','w').write('\\n')\""
262
+ elif ligand_residue_name:
263
+ # Use specified ligand residue name - extract from both ATOM and HETATM records
264
+ cmd = f'''python -c "
265
+ import MDAnalysis as mda
266
+ u = mda.Universe('{output_file}')
267
+ # Extract specific ligand residue from both ATOM and HETATM records
268
+ u.select_atoms('resname {ligand_residue_name}').write('{output_file}')
269
+ "'''
270
+ else:
271
+ # Auto-detect ligand residues
272
+ cmd = f'''python -c "
273
+ import MDAnalysis as mda
274
+ u = mda.Universe('{output_file}')
275
+ # Get all unique residue names from HETATM records
276
+ hetatm_residues = set()
277
+ for atom in u.atoms:
278
+ if atom.record_type == 'HETATM':
279
+ hetatm_residues.add(atom.resname)
280
+ # Remove water and ions
281
+ ligand_residues = hetatm_residues - {{'HOH', 'WAT', 'TIP3', 'TIP4', 'SPC', 'SPCE', 'NA', 'CL', 'K', 'MG', 'CA', 'ZN', 'FE', 'MN', 'CU', 'NI', 'CO', 'CD', 'HG', 'PB', 'SR', 'BA', 'RB', 'CS', 'LI', 'F', 'BR', 'I', 'PO4', 'PO3', 'H2PO4', 'HPO4', 'H3PO4', 'SO4'}}
282
+ if ligand_residues:
283
+ resname_sel = ' or '.join([f'resname {{res}}' for res in ligand_residues])
284
+ u.select_atoms(resname_sel).write('{output_file}')
285
+ else:
286
+ # No ligands found, create empty file
287
+ with open('{output_file}', 'w') as f:
288
+ f.write('\\n')
289
+ "'''
290
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
291
+
292
+ if result.returncode != 0:
293
+ raise Exception(f"MDAnalysis error: {result.stderr}")
294
+
295
+ # If specific ligand residue name was provided, convert ATOM to HETATM
296
+ if ligand_residue_name:
297
+ convert_atom_to_hetatm(output_file)
298
+
299
+ return True
300
+ except Exception as e:
301
+ print(f"Error in extract_ligands: {e}")
302
+ return False
303
+
304
+ def convert_atom_to_hetatm(pdb_file):
305
+ """Convert ATOM records to HETATM in PDB file"""
306
+ try:
307
+ with open(pdb_file, 'r') as f:
308
+ lines = f.readlines()
309
+
310
+ # Convert ATOM to HETATM
311
+ converted_lines = []
312
+ for line in lines:
313
+ if line.startswith('ATOM'):
314
+ # Replace ATOM with HETATM
315
+ converted_line = 'HETATM' + line[6:]
316
+ converted_lines.append(converted_line)
317
+ else:
318
+ converted_lines.append(line)
319
+
320
+ # Write back to file
321
+ with open(pdb_file, 'w') as f:
322
+ f.writelines(converted_lines)
323
+
324
+ print(f"Converted ATOM records to HETATM in {pdb_file}")
325
+ return True
326
+ except Exception as e:
327
+ print(f"Error converting ATOM to HETATM: {e}")
328
+ return False
329
+
330
+ def extract_original_residue_info(ligand_file):
331
+ """Extract original residue name, chain ID, and residue number from ligand PDB file"""
332
+ residue_info = {}
333
+ try:
334
+ with open(ligand_file, 'r') as f:
335
+ for line in f:
336
+ if line.startswith(('ATOM', 'HETATM')):
337
+ resname = line[17:20].strip()
338
+ chain_id = line[21:22].strip()
339
+ resnum = line[22:26].strip()
340
+ # Store the first residue info we find (assuming single residue per file)
341
+ if resname and resname not in residue_info:
342
+ residue_info = {
343
+ 'resname': resname,
344
+ 'chain_id': chain_id,
345
+ 'resnum': resnum
346
+ }
347
+ break # We only need the first residue info
348
+ return residue_info
349
+ except Exception as e:
350
+ print(f"Error extracting residue info: {e}")
351
+ return {}
352
+
353
+ def restore_residue_info_in_pdb(pdb_file, original_resname, original_chain_id, original_resnum):
354
+ """Restore original residue name, chain ID, and residue number in PDB file"""
355
+ try:
356
+ with open(pdb_file, 'r') as f:
357
+ lines = f.readlines()
358
+
359
+ restored_lines = []
360
+ for line in lines:
361
+ if line.startswith(('ATOM', 'HETATM')):
362
+ # Restore residue name (columns 17-20)
363
+ restored_line = line[:17] + f"{original_resname:>3}" + line[20:]
364
+ # Restore chain ID (column 21)
365
+ if original_chain_id:
366
+ restored_line = restored_line[:21] + original_chain_id + restored_line[22:]
367
+ # Restore residue number (columns 22-26)
368
+ if original_resnum:
369
+ restored_line = restored_line[:22] + f"{original_resnum:>4}" + restored_line[26:]
370
+ restored_lines.append(restored_line)
371
+ elif line.startswith('MASTER'):
372
+ # Skip MASTER records
373
+ continue
374
+ else:
375
+ restored_lines.append(line)
376
+
377
+ with open(pdb_file, 'w') as f:
378
+ f.writelines(restored_lines)
379
+
380
+ print(f"Restored residue info: {original_resname} {original_chain_id} {original_resnum} in {pdb_file}")
381
+ return True
382
+ except Exception as e:
383
+ print(f"Error restoring residue info: {e}")
384
+ return False
385
+
386
+ def correct_ligand_with_openbabel(ligand_file, corrected_file):
387
+ """Correct ligand using OpenBabel (add hydrogens at pH 7.4) and preserve original residue info"""
388
+ ligand_path = os.path.abspath(ligand_file)
389
+ corrected_path = os.path.abspath(corrected_file)
390
+ if not os.path.isfile(ligand_path) or os.path.getsize(ligand_path) == 0:
391
+ print("Ligand file missing or empty:", ligand_path)
392
+ return False
393
+
394
+ # Extract original residue info before OpenBabel processing
395
+ residue_info = extract_original_residue_info(ligand_path)
396
+ original_resname = residue_info.get('resname', 'UNL')
397
+ original_chain_id = residue_info.get('chain_id', '')
398
+ original_resnum = residue_info.get('resnum', '1')
399
+
400
+ print(f"Original residue info: {original_resname} {original_chain_id} {original_resnum}")
401
+
402
+ # Use OpenBabel to add hydrogens at pH 7.4
403
+ cmd = f'obabel -i pdb {ligand_path} -o pdb -O {corrected_path} -p 7.4'
404
+ success = run_command(cmd, f"Correcting ligand with OpenBabel")
405
+
406
+ if not success:
407
+ return False
408
+
409
+ # Restore original residue name, chain ID, and residue number
410
+ if residue_info:
411
+ restore_residue_info_in_pdb(corrected_path, original_resname, original_chain_id, original_resnum)
412
+
413
+ return True
414
+
415
+ def split_ligands_by_residue(ligand_file, output_dir):
416
+ """Split multi-ligand PDB file into individual ligand files using MDAnalysis (one file per residue)
417
+ This is more robust than splitting by TER records as it properly handles residue-based splitting.
418
+ """
419
+ ligand_files = []
420
+ try:
421
+ ligand_path = os.path.abspath(ligand_file)
422
+ output_dir_abs = os.path.abspath(output_dir)
423
+
424
+ # Use MDAnalysis to split ligands by residue - this is the robust method
425
+ # Command: python -c "import MDAnalysis as mda; u=mda.Universe('3_ligands_extracted.pdb'); [res.atoms.write(f'3_ligand_extracted_{i}.pdb') for i,res in enumerate(u.residues,1)]"
426
+ cmd = f'''python -c "import MDAnalysis as mda; import os; u=mda.Universe('{ligand_path}'); os.chdir('{output_dir_abs}'); [res.atoms.write(f'3_ligand_extracted_{{i}}.pdb') for i,res in enumerate(u.residues,1)]"'''
427
+
428
+ print(f"Running MDAnalysis command to split ligands by residue...")
429
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True, cwd=output_dir_abs)
430
+
431
+ if result.returncode != 0:
432
+ print(f"Error running MDAnalysis command: {result.stderr}")
433
+ print(f"Command output: {result.stdout}")
434
+ return []
435
+
436
+ # Collect all generated ligand files
437
+ ligand_files = []
438
+ for f in os.listdir(output_dir):
439
+ if f.startswith('3_ligand_extracted_') and f.endswith('.pdb'):
440
+ ligand_files.append(os.path.join(output_dir, f))
441
+
442
+ # Sort by number in filename (e.g., 3_ligand_extracted_1.pdb, 3_ligand_extracted_2.pdb, ...)
443
+ ligand_files.sort(key=lambda x: int(os.path.basename(x).split('_')[-1].split('.')[0]))
444
+
445
+ print(f"Split {len(ligand_files)} ligand(s) from {ligand_file}")
446
+ return ligand_files
447
+ except Exception as e:
448
+ print(f"Error splitting ligands: {e}")
449
+ import traceback
450
+ traceback.print_exc()
451
+ return []
452
+
453
+ def remove_connect_records(pdb_file):
454
+ """Remove CONNECT and MASTER records from PDB file"""
455
+ try:
456
+ with open(pdb_file, 'r') as f:
457
+ lines = f.readlines()
458
+
459
+ # Filter out CONNECT and MASTER records
460
+ filtered_lines = [line for line in lines if not line.startswith(('CONECT', 'MASTER'))]
461
+
462
+ with open(pdb_file, 'w') as f:
463
+ f.writelines(filtered_lines)
464
+
465
+ print(f"Removed CONNECT and MASTER records from {pdb_file}")
466
+ return True
467
+ except Exception as e:
468
+ print(f"Error removing CONNECT/MASTER records: {e}")
469
+ return False
470
+
471
+ def convert_atom_to_hetatm_in_ligand(pdb_file):
472
+ """Convert ATOM records to HETATM in ligand PDB file for consistency"""
473
+ try:
474
+ with open(pdb_file, 'r') as f:
475
+ lines = f.readlines()
476
+
477
+ converted_lines = []
478
+ converted_count = 0
479
+ for line in lines:
480
+ if line.startswith('ATOM'):
481
+ # Replace ATOM with HETATM, preserving the rest of the line
482
+ converted_line = 'HETATM' + line[6:]
483
+ converted_lines.append(converted_line)
484
+ converted_count += 1
485
+ else:
486
+ converted_lines.append(line)
487
+
488
+ with open(pdb_file, 'w') as f:
489
+ f.writelines(converted_lines)
490
+
491
+ if converted_count > 0:
492
+ print(f"Converted {converted_count} ATOM record(s) to HETATM in {pdb_file}")
493
+
494
+ return True
495
+ except Exception as e:
496
+ print(f"Error converting ATOM to HETATM: {e}")
497
+ return False
498
+
499
+ def make_atom_names_distinct(pdb_file):
500
+ """Make all atom names distinct (C1, C2, O1, O2, H1, H2, etc.) for antechamber compatibility
501
+ Antechamber requires each atom to have a unique name.
502
+ """
503
+ try:
504
+ from collections import defaultdict
505
+
506
+ with open(pdb_file, 'r') as f:
507
+ lines = f.readlines()
508
+
509
+ # Track counts for each element type
510
+ element_counts = defaultdict(int)
511
+ modified_lines = []
512
+ modified_count = 0
513
+
514
+ for line in lines:
515
+ if line.startswith(('ATOM', 'HETATM')):
516
+ # Extract element from the last field (column 76-78) or from atom name (columns 12-16)
517
+ # Try to get element from the last field first (more reliable)
518
+ element = line[76:78].strip()
519
+
520
+ # If element not found in last field, try to extract from atom name
521
+ if not element:
522
+ atom_name = line[12:16].strip()
523
+ # Extract element symbol (first letter, or first two letters for two-letter elements)
524
+ if len(atom_name) >= 1:
525
+ # Check for two-letter elements (common ones: Cl, Br, etc.)
526
+ if len(atom_name) >= 2 and atom_name[:2].upper() in ['CL', 'BR', 'MG', 'CA', 'ZN', 'FE', 'MN', 'CU', 'NI', 'CO', 'CD', 'HG', 'PB', 'SR', 'BA', 'RB', 'CS', 'LI']:
527
+ element = atom_name[:2].upper()
528
+ else:
529
+ element = atom_name[0].upper()
530
+
531
+ # Increment count for this element
532
+ element_counts[element] += 1
533
+ count = element_counts[element]
534
+
535
+ # Create distinct atom name: Element + number (e.g., C1, C2, O1, O2, H1, H2)
536
+ # Atom name is in columns 12-16 (4 characters, right-aligned)
537
+ distinct_name = f"{element}{count}"
538
+
539
+ # Ensure the name fits in 4 characters (right-aligned)
540
+ if len(distinct_name) > 4:
541
+ # For long element names, use abbreviation or truncate
542
+ if element == 'CL':
543
+ distinct_name = f"Cl{count}"[:4]
544
+ elif element == 'BR':
545
+ distinct_name = f"Br{count}"[:4]
546
+ else:
547
+ distinct_name = distinct_name[:4]
548
+
549
+ # Replace atom name (columns 12-16, right-aligned)
550
+ modified_line = line[:12] + f"{distinct_name:>4}" + line[16:]
551
+ modified_lines.append(modified_line)
552
+ modified_count += 1
553
+ else:
554
+ modified_lines.append(line)
555
+
556
+ with open(pdb_file, 'w') as f:
557
+ f.writelines(modified_lines)
558
+
559
+ if modified_count > 0:
560
+ print(f"Made {modified_count} atom name(s) distinct in {pdb_file}")
561
+ print(f"Element counts: {dict(element_counts)}")
562
+
563
+ return True
564
+ except Exception as e:
565
+ print(f"Error making atom names distinct: {e}")
566
+ import traceback
567
+ traceback.print_exc()
568
+ return False
569
+
570
+ def sanity_check_ligand_pdb(pdb_file):
571
+ """Perform sanity checks on ligand PDB file after OpenBabel processing:
572
+ 1. Remove CONECT and MASTER records
573
+ 2. Convert ATOM records to HETATM for consistency
574
+ 3. Make all atom names distinct (C1, C2, O1, O2, H1, H2, etc.) for antechamber compatibility
575
+ """
576
+ try:
577
+ # Step 1: Remove CONECT and MASTER records
578
+ if not remove_connect_records(pdb_file):
579
+ return False
580
+
581
+ # Step 2: Convert ATOM to HETATM for consistency
582
+ if not convert_atom_to_hetatm_in_ligand(pdb_file):
583
+ return False
584
+
585
+ # Step 3: Make atom names distinct (required by antechamber)
586
+ if not make_atom_names_distinct(pdb_file):
587
+ return False
588
+
589
+ print(f"Sanity check completed for {pdb_file}")
590
+ return True
591
+ except Exception as e:
592
+ print(f"Error in sanity check: {e}")
593
+ return False
594
+
595
+ def merge_protein_and_ligand(protein_file, ligand_file, output_file, ligand_lines_list=None, ligand_groups=None):
596
+ """Merge capped protein and corrected ligand(s) with proper PDB formatting
597
+
598
+ Args:
599
+ protein_file: Path to protein PDB file
600
+ ligand_file: Path to ligand PDB file (optional, if ligand_lines_list or ligand_groups is provided)
601
+ output_file: Path to output merged PDB file
602
+ ligand_lines_list: List of ligand lines (optional, for backward compatibility - single ligand)
603
+ ligand_groups: List of ligand line groups, where each group is a list of lines for one ligand (for multiple ligands with TER separation)
604
+ """
605
+ try:
606
+ # Read protein file
607
+ with open(protein_file, 'r') as f:
608
+ protein_lines = f.readlines()
609
+
610
+ # Get ligand lines - prioritize ligand_groups for multiple ligands
611
+ if ligand_groups is not None:
612
+ # Multiple ligands: each group will be separated by TER
613
+ ligand_groups_processed = ligand_groups
614
+ elif ligand_lines_list is not None:
615
+ # Single ligand: wrap in a list for consistent processing
616
+ ligand_groups_processed = [ligand_lines_list] if ligand_lines_list else []
617
+ elif ligand_file:
618
+ # Read ligand file
619
+ with open(ligand_file, 'r') as f:
620
+ ligand_lines = f.readlines()
621
+ # Process ligand file: remove header info (CRYST, REMARK, etc.) and keep only ATOM/HETATM
622
+ ligand_processed = []
623
+ for line in ligand_lines:
624
+ if line.startswith(('ATOM', 'HETATM')):
625
+ ligand_processed.append(line)
626
+ ligand_groups_processed = [ligand_processed] if ligand_processed else []
627
+ else:
628
+ ligand_groups_processed = []
629
+
630
+ # Process protein file: remove 'END' and add properly formatted 'TER'
631
+ protein_processed = []
632
+ last_atom_line = None
633
+ for line in protein_lines:
634
+ if line.strip() == 'END':
635
+ # Create properly formatted TER card using the last atom's info
636
+ if last_atom_line and last_atom_line.startswith('ATOM'):
637
+ # Extract atom number and residue info from last atom
638
+ atom_num = last_atom_line[6:11].strip()
639
+ res_name = last_atom_line[17:20].strip()
640
+ chain_id = last_atom_line[21:22].strip()
641
+ res_num = last_atom_line[22:26].strip()
642
+ ter_line = f"TER {atom_num:>5} {res_name} {chain_id}{res_num}\n"
643
+ protein_processed.append(ter_line)
644
+ else:
645
+ protein_processed.append('TER\n')
646
+ else:
647
+ protein_processed.append(line)
648
+ if line.startswith('ATOM'):
649
+ last_atom_line = line
650
+
651
+ # Combine ligands with TER records between each ligand
652
+ ligand_content = []
653
+ for i, ligand_group in enumerate(ligand_groups_processed):
654
+ if ligand_group: # Only process non-empty groups
655
+ # Add ligand atoms
656
+ ligand_content.extend(ligand_group)
657
+ # Add TER record after each ligand (except the last one, which will be followed by END)
658
+ if i < len(ligand_groups_processed) - 1:
659
+ # Get last atom info from current ligand group to create TER
660
+ if ligand_group:
661
+ last_ligand_atom = ligand_group[-1]
662
+ if last_ligand_atom.startswith(('ATOM', 'HETATM')):
663
+ atom_num = last_ligand_atom[6:11].strip()
664
+ res_name = last_ligand_atom[17:20].strip()
665
+ chain_id = last_ligand_atom[21:22].strip()
666
+ res_num = last_ligand_atom[22:26].strip()
667
+ ter_line = f"TER {atom_num:>5} {res_name} {chain_id}{res_num}\n"
668
+ ligand_content.append(ter_line)
669
+ else:
670
+ ligand_content.append('TER\n')
671
+
672
+ # Combine: protein + TER + ligand(s) with TER between ligands + END
673
+ merged_content = ''.join(protein_processed) + ''.join(ligand_content) + 'END\n'
674
+
675
+ with open(output_file, 'w') as f:
676
+ f.write(merged_content)
677
+
678
+ return True
679
+ except Exception as e:
680
+ print(f"Error merging files: {str(e)}")
681
+ import traceback
682
+ traceback.print_exc()
683
+ return False
684
+
685
+ def prepare_structure(pdb_content, options, output_dir="output"):
686
+ """Main function to prepare structure for AMBER simulation"""
687
+ try:
688
+ # Create output directory if it doesn't exist
689
+ os.makedirs(output_dir, exist_ok=True)
690
+
691
+ # Define all file paths in output directory
692
+ # Prefer the superimposed completed structure (0_complete_structure.pdb) when it
693
+ # exists: it has ESMFold/minimized chains aligned to the original frame so that
694
+ # ligands stay in the same coordinate frame throughout the pipeline.
695
+ complete_structure_file = os.path.join(output_dir, "0_complete_structure.pdb")
696
+ original_input_file = os.path.join(output_dir, "0_original_input.pdb")
697
+
698
+ if os.path.exists(complete_structure_file):
699
+ input_file = complete_structure_file
700
+ logger.info("Using superimposed completed structure (0_complete_structure.pdb) as input for coordinate-frame consistency with ligands")
701
+ else:
702
+ input_file = original_input_file
703
+ logger.info("Using original input (0_original_input.pdb) as input")
704
+
705
+ user_chain_file = os.path.join(output_dir, "0_user_chain_selected.pdb")
706
+ protein_file = os.path.join(output_dir, "1_protein_no_hydrogens.pdb")
707
+ protein_capped_file = os.path.join(output_dir, "2_protein_with_caps.pdb")
708
+ ligand_file = os.path.join(output_dir, "3_ligands_extracted.pdb")
709
+ ligand_corrected_file = os.path.join(output_dir, "4_ligands_corrected.pdb")
710
+ tleap_ready_file = os.path.join(output_dir, "tleap_ready.pdb")
711
+
712
+ # Step 0: Save original input for reference (only if using original input)
713
+ # If using completed structure, we don't overwrite it
714
+ if input_file == original_input_file:
715
+ print("Step 0: Saving original input...")
716
+ with open(input_file, 'w') as f:
717
+ f.write(pdb_content)
718
+ else:
719
+ # If using completed structure, read it instead of using pdb_content
720
+ print("Step 0: Using completed structure as input...")
721
+ with open(input_file, 'r') as f:
722
+ pdb_content = f.read()
723
+ # Also save a reference to original input if it doesn't exist
724
+ if not os.path.exists(original_input_file):
725
+ print("Step 0: Saving reference to original input...")
726
+ with open(original_input_file, 'w') as f:
727
+ f.write(pdb_content)
728
+
729
+ # Step 0.5: Extract user-selected chains and ligands
730
+ selected_chains = options.get('selected_chains', [])
731
+ selected_ligands = options.get('selected_ligands', [])
732
+
733
+ if selected_chains:
734
+ print(f"Step 0.5a: Extracting selected chains: {', '.join(selected_chains)}")
735
+ if not extract_selected_chains(pdb_content, user_chain_file, selected_chains):
736
+ raise Exception("Failed to extract selected chains")
737
+ else:
738
+ # No chains selected - raise an error instead of using all chains
739
+ raise Exception("No chains selected. Please select at least one chain for structure preparation.")
740
+
741
+ if selected_ligands:
742
+ ligand_names = []
743
+ for l in selected_ligands:
744
+ s = f"{l.get('resn', '')}-{l.get('chain', '')}"
745
+ if l.get('resi'):
746
+ s += f" (resi {l.get('resi')})"
747
+ ligand_names.append(s)
748
+ print(f"Step 0.5b: Extracting selected ligands: {ligand_names}")
749
+ if not extract_selected_ligands(pdb_content, ligand_file, selected_ligands):
750
+ raise Exception("Failed to extract selected ligands")
751
+ else:
752
+ print("Step 0.5b: No ligands selected, creating empty ligand file")
753
+ with open(ligand_file, 'w') as f:
754
+ f.write('\n')
755
+
756
+ # Step 1: Extract protein only (remove hydrogens) from user-selected chains
757
+ print("Step 1: Extracting protein without hydrogens from selected chains...")
758
+ # Read the user-selected chain file
759
+ with open(user_chain_file, 'r') as f:
760
+ chain_content = f.read()
761
+
762
+ if not extract_protein_only(chain_content, protein_file):
763
+ raise Exception("Failed to extract protein")
764
+
765
+ # Step 1b: Merge minimized chains into 1_protein_no_hydrogens.pdb only when the
766
+ # input is NOT 0_complete_structure. When we use 0_complete_structure, it was
767
+ # built by rebuild_pdb_with_esmfold, which already incorporates and superimposes
768
+ # the minimized chains; the raw *_esmfold_minimized_noH.pdb files are in the
769
+ # minimization frame, so merging them here would break the coordinate frame.
770
+ if input_file != complete_structure_file:
771
+ for path in glob.glob(os.path.join(output_dir, "*_chain_*_esmfold_minimized_noH.pdb")):
772
+ name = os.path.basename(path).replace(".pdb", "")
773
+ parts = name.split("_chain_")
774
+ if len(parts) == 2:
775
+ chain_id = parts[1].split("_")[0]
776
+ replace_chain_in_pdb(protein_file, chain_id, path)
777
+ logger.info("Merged minimized chain %s into 1_protein_no_hydrogens.pdb", chain_id)
778
+
779
+ # Step 2: Add capping groups (only if add_ace or add_nme is True)
780
+ add_ace = options.get('add_ace', True)
781
+ add_nme = options.get('add_nme', True)
782
+
783
+ if add_ace or add_nme:
784
+ print("Step 2: Adding ACE and NME capping groups...")
785
+ if not add_capping_groups(protein_file, protein_capped_file):
786
+ raise Exception("Failed to add capping groups")
787
+ else:
788
+ print("Step 2: Skipping capping groups (add_ace=False, add_nme=False)")
789
+ print("Using protein without capping - copying to capped file")
790
+ # Copy protein file to capped file (no capping)
791
+ shutil.copy2(protein_file, protein_capped_file)
792
+
793
+ # Step 3: Handle ligands (use pre-extracted ligand file)
794
+ preserve_ligands = options.get('preserve_ligands', True)
795
+ ligand_present = False
796
+ ligand_count = 0
797
+ selected_ligand_count = 0 # Store count from selected_ligands separately
798
+
799
+ # Count selected ligands if provided (before processing)
800
+ if selected_ligands:
801
+ # Count unique ligand entities (by residue name, chain, and residue number)
802
+ unique_ligands = set()
803
+ for lig in selected_ligands:
804
+ resn = str(lig.get('resn') or '')
805
+ chain = str(lig.get('chain') or '')
806
+ resi = str(lig.get('resi') or '')
807
+ # Create unique identifier (resi disambiguates when same resn+chain appears multiple times)
808
+ unique_id = f"{resn}_{chain}_{resi}"
809
+ unique_ligands.add(unique_id)
810
+ selected_ligand_count = len(unique_ligands)
811
+ ligand_count = selected_ligand_count # Initialize with selected count
812
+ print(f"Found {selected_ligand_count} unique selected ligand(s)")
813
+
814
+ if preserve_ligands:
815
+ print("Step 3: Processing pre-extracted ligands...")
816
+
817
+ # Check if ligand file has content (not just empty or newline)
818
+ with open(ligand_file, 'r') as f:
819
+ ligand_content = f.read().strip()
820
+
821
+ if ligand_content and len(ligand_content) > 1:
822
+ ligand_present = True
823
+ print("Found pre-extracted ligands")
824
+
825
+ # Split ligands into individual files using MDAnalysis (by residue)
826
+ individual_ligand_files = split_ligands_by_residue(ligand_file, output_dir)
827
+ # Update ligand_count based on actual split results if not already set from selected_ligands
828
+ if not selected_ligands or len(individual_ligand_files) != ligand_count:
829
+ ligand_count = len(individual_ligand_files)
830
+ print(f"Split into {ligand_count} individual ligand file(s)")
831
+
832
+ if ligand_count == 0:
833
+ print("Warning: No ligands could be extracted from file")
834
+ shutil.copy2(protein_capped_file, tleap_ready_file)
835
+ else:
836
+ print(f"Processing {ligand_count} ligand(s) individually...")
837
+
838
+ # Process each ligand: OpenBabel -> sanity check -> final corrected file
839
+ corrected_ligand_files = []
840
+ for i, individual_file in enumerate(individual_ligand_files, 1):
841
+ # OpenBabel output file (intermediate, kept for reference)
842
+ obabel_file = os.path.join(output_dir, f"4_ligands_corrected_obabel_{i}.pdb")
843
+ # Final corrected file (after sanity checks)
844
+ corrected_file = os.path.join(output_dir, f"4_ligands_corrected_{i}.pdb")
845
+
846
+ # Use OpenBabel to add hydrogens (write to obabel_file)
847
+ if not correct_ligand_with_openbabel(individual_file, obabel_file):
848
+ print(f"Error: Failed to process ligand {i} with OpenBabel")
849
+ continue
850
+
851
+ # Copy obabel file to corrected file before sanity check
852
+ shutil.copy2(obabel_file, corrected_file)
853
+
854
+ # Perform sanity check on corrected_file: remove CONECT/MASTER, convert ATOM to HETATM, make names distinct
855
+ if not sanity_check_ligand_pdb(corrected_file):
856
+ print(f"Warning: Sanity check failed for ligand {i}, but continuing...")
857
+
858
+ corrected_ligand_files.append(corrected_file)
859
+
860
+ if not corrected_ligand_files:
861
+ print("Error: Failed to process any ligands")
862
+ return {
863
+ 'error': 'Failed to process ligands with OpenBabel',
864
+ 'prepared_structure': '',
865
+ 'original_atoms': 0,
866
+ 'prepared_atoms': 0,
867
+ 'removed_components': {},
868
+ 'added_capping': {},
869
+ 'preserved_ligands': 0,
870
+ 'ligand_present': False
871
+ }
872
+
873
+ # Merge all corrected ligands into a single file for tleap_ready
874
+ # Read all corrected ligand files and group them by ligand (for TER separation)
875
+ all_ligand_groups = []
876
+ for corrected_lig_file in corrected_ligand_files:
877
+ with open(corrected_lig_file, 'r') as f:
878
+ lig_lines = [line for line in f if line.startswith(('ATOM', 'HETATM'))]
879
+ if lig_lines: # Only add non-empty ligand groups
880
+ all_ligand_groups.append(lig_lines)
881
+
882
+ # Create combined ligand file (4_ligands_corrected.pdb) for separate download
883
+ with open(ligand_corrected_file, 'w') as f:
884
+ for i, lig_group in enumerate(all_ligand_groups):
885
+ for line in lig_group:
886
+ f.write(line if line.endswith('\n') else line + '\n')
887
+ if i < len(all_ligand_groups) - 1:
888
+ f.write('TER\n')
889
+ f.write('END\n')
890
+ print(f"Created combined ligand file: {ligand_corrected_file}")
891
+
892
+ # Merge protein and all ligands (with TER records between ligands)
893
+ if not merge_protein_and_ligand(protein_capped_file, None, tleap_ready_file, ligand_groups=all_ligand_groups):
894
+ raise Exception("Failed to merge protein and ligands")
895
+ elif selected_ligands and ligand_count > 0:
896
+ # If ligands were selected but file is empty, still mark as present if we have a count
897
+ ligand_present = True
898
+ print(f"Ligands were selected ({ligand_count} unique), but ligand file appears empty")
899
+ # Use protein only since no ligand content found
900
+ shutil.copy2(protein_capped_file, tleap_ready_file)
901
+ else:
902
+ print("No ligands found in pre-extracted file, using protein only")
903
+ # Copy protein file to tleap_ready
904
+ shutil.copy2(protein_capped_file, tleap_ready_file)
905
+ else:
906
+ print("Step 3: Skipping ligand processing (preserve_ligands=False)")
907
+ print("Using protein only - copying capped protein to tleap_ready")
908
+ # Copy protein file to tleap_ready (protein only, no ligands)
909
+ shutil.copy2(protein_capped_file, tleap_ready_file)
910
+
911
+ # Ensure tleap_ready.pdb exists before proceeding
912
+ if not os.path.exists(tleap_ready_file):
913
+ print(f"Error: tleap_ready.pdb was not created. Checking what went wrong...")
914
+ # Try to create it from protein_capped_file as fallback
915
+ if os.path.exists(protein_capped_file):
916
+ print("Creating tleap_ready.pdb from protein_capped_file as fallback...")
917
+ shutil.copy2(protein_capped_file, tleap_ready_file)
918
+ else:
919
+ raise Exception(f"tleap_ready.pdb was not created and protein_capped_file also doesn't exist")
920
+
921
+ # Remove CONNECT records from tleap_ready.pdb (PyMOL adds them)
922
+ print("Removing CONNECT records from tleap_ready.pdb...")
923
+ if not remove_connect_records(tleap_ready_file):
924
+ print("Warning: Failed to remove CONNECT records, but continuing...")
925
+
926
+ # Read the final prepared structure
927
+ if not os.path.exists(tleap_ready_file):
928
+ raise Exception("tleap_ready.pdb does not exist after processing")
929
+
930
+ with open(tleap_ready_file, 'r') as f:
931
+ prepared_content = f.read()
932
+
933
+ # Calculate statistics
934
+ original_atoms = len([line for line in pdb_content.split('\n') if line.startswith('ATOM')])
935
+ prepared_atoms = len([line for line in prepared_content.split('\n') if line.startswith('ATOM')])
936
+
937
+ # Calculate removed components
938
+ water_count = len([line for line in pdb_content.split('\n') if line.startswith('HETATM') and line[17:20].strip() in ['HOH', 'WAT', 'TIP3', 'TIP4', 'TIP5', 'SPC', 'SPCE']])
939
+ ion_count = len([line for line in pdb_content.split('\n') if line.startswith('HETATM') and line[17:20].strip() in ['NA', 'CL', 'K', 'MG', 'CA', 'ZN', 'FE', 'MN', 'CU', 'NI', 'CO', 'CD', 'HG', 'PB', 'SR', 'BA', 'RB', 'CS', 'LI', 'F', 'BR', 'I', 'PO4', 'PO3', 'H2PO4', 'HPO4', 'H3PO4']])
940
+ hydrogen_count = len([line for line in pdb_content.split('\n') if line.startswith('ATOM') and line[76:78].strip() == 'H'])
941
+
942
+ # If not preserving ligands, count them as removed
943
+ ligand_count = 0
944
+ if not preserve_ligands and ligand_present:
945
+ # Count ligands from the pre-extracted file
946
+ with open(ligand_file, 'r') as f:
947
+ ligand_lines = [line for line in f if line.startswith('HETATM')]
948
+ ligand_count = len(set(line[17:20].strip() for line in ligand_lines))
949
+
950
+ removed_components = {
951
+ 'water': water_count,
952
+ 'ions': ion_count,
953
+ 'hydrogens': hydrogen_count,
954
+ 'ligands': ligand_count
955
+ }
956
+
957
+ # Calculate added capping groups (only if capping was performed)
958
+ if add_ace or add_nme:
959
+ # Count unique ACE and NME residues, not individual atoms
960
+ ace_residues = set()
961
+ nme_residues = set()
962
+
963
+ for line in prepared_content.split('\n'):
964
+ if line.startswith('ATOM') and 'ACE' in line:
965
+ # Extract residue number to count unique ACE groups
966
+ res_num = line[22:26].strip()
967
+ ace_residues.add(res_num)
968
+ elif line.startswith('ATOM') and 'NME' in line:
969
+ # Extract residue number to count unique NME groups
970
+ res_num = line[22:26].strip()
971
+ nme_residues.add(res_num)
972
+
973
+ added_capping = {
974
+ 'ace_groups': len(ace_residues),
975
+ 'nme_groups': len(nme_residues)
976
+ }
977
+ else:
978
+ added_capping = {
979
+ 'ace_groups': 0,
980
+ 'nme_groups': 0
981
+ }
982
+
983
+ # Count preserved ligands
984
+ # Priority: 1) selected_ligands count, 2) processed ligand_count, 3) 0
985
+ if preserve_ligands:
986
+ if selected_ligand_count > 0:
987
+ # Use count from selected_ligands (most reliable)
988
+ preserved_ligands = selected_ligand_count
989
+ print(f"Using selected ligand count: {preserved_ligands}")
990
+ elif ligand_present and ligand_count > 0:
991
+ # Use count from processing
992
+ preserved_ligands = ligand_count
993
+ print(f"Using processed ligand count: {preserved_ligands}")
994
+ elif ligand_present:
995
+ # Ligands were present but count is 0, try to count from tleap_ready
996
+ # Count unique ligand residue names in tleap_ready.pdb
997
+ ligand_resnames = set()
998
+ for line in prepared_content.split('\n'):
999
+ if line.startswith('HETATM'):
1000
+ resname = line[17:20].strip()
1001
+ if resname and resname not in ['HOH', 'WAT', 'TIP', 'SPC', 'NA', 'CL', 'ACE', 'NME']:
1002
+ ligand_resnames.add(resname)
1003
+ preserved_ligands = len(ligand_resnames)
1004
+ print(f"Counted {preserved_ligands} unique ligand residue name(s) from tleap_ready.pdb")
1005
+ else:
1006
+ preserved_ligands = 0
1007
+ else:
1008
+ preserved_ligands = 0
1009
+
1010
+ result = {
1011
+ 'prepared_structure': prepared_content,
1012
+ 'original_atoms': original_atoms,
1013
+ 'prepared_atoms': prepared_atoms,
1014
+ 'removed_components': removed_components,
1015
+ 'added_capping': added_capping,
1016
+ 'preserved_ligands': preserved_ligands,
1017
+ 'ligand_present': ligand_present,
1018
+ 'separate_ligands': options.get('separate_ligands', False)
1019
+ }
1020
+
1021
+ # If separate ligands is enabled and ligands are present, include ligand content
1022
+ if ligand_present and options.get('separate_ligands', False):
1023
+ with open(ligand_corrected_file, 'r') as f:
1024
+ result['ligand_content'] = f.read()
1025
+
1026
+ return result
1027
+
1028
+ except Exception as e:
1029
+ return {
1030
+ 'error': str(e),
1031
+ 'prepared_structure': '',
1032
+ 'original_atoms': 0,
1033
+ 'prepared_atoms': 0,
1034
+ 'removed_components': {},
1035
+ 'added_capping': {},
1036
+ 'preserved_ligands': 0,
1037
+ 'ligand_present': False
1038
+ }
1039
+
1040
+ def parse_structure_info(pdb_content):
1041
+ """Parse structure information for display"""
1042
+ lines = pdb_content.split('\n')
1043
+ atom_count = 0
1044
+ chains = set()
1045
+ residues = set()
1046
+ water_molecules = 0
1047
+ ions = 0
1048
+ ligands = set()
1049
+ hetatoms = 0
1050
+
1051
+ # Common water molecule names
1052
+ water_names = {'HOH', 'WAT', 'TIP3', 'TIP4', 'SPC', 'SPCE'}
1053
+
1054
+ # Common ion names
1055
+ ion_names = {'NA', 'CL', 'K', 'MG', 'CA', 'ZN', 'FE', 'MN', 'CU', 'NI', 'CO', 'CD', 'HG', 'PB', 'SR', 'BA', 'RB', 'CS', 'LI', 'F', 'BR', 'I', 'PO4', 'PO3', 'H2PO4', 'HPO4', 'H3PO4','SO4'}
1056
+
1057
+ # Common ligand indicators
1058
+ ligand_indicators = {'ATP', 'ADP', 'AMP', 'GDP', 'GTP', 'NAD', 'FAD', 'HEM', 'HEME', 'COA', 'SAM', 'PLP', 'THF', 'FMN', 'FAD', 'NADP', 'UDP', 'CDP', 'TDP', 'GDP', 'ADP', 'ATP'}
1059
+
1060
+ for line in lines:
1061
+ if line.startswith('ATOM'):
1062
+ atom_count += 1
1063
+ chain_id = line[21:22].strip()
1064
+ if chain_id:
1065
+ chains.add(chain_id)
1066
+
1067
+ res_name = line[17:20].strip()
1068
+ res_num = line[22:26].strip()
1069
+ residues.add(f"{res_name}{res_num}")
1070
+ elif line.startswith('HETATM'):
1071
+ hetatoms += 1
1072
+ res_name = line[17:20].strip()
1073
+
1074
+ if res_name in water_names:
1075
+ water_molecules += 1
1076
+ elif res_name in ion_names:
1077
+ ions += 1
1078
+ elif res_name in ligand_indicators:
1079
+ ligands.add(res_name)
1080
+
1081
+ # Count unique water molecules
1082
+ unique_water_residues = set()
1083
+ for line in lines:
1084
+ if line.startswith('HETATM'):
1085
+ res_name = line[17:20].strip()
1086
+ res_num = line[22:26].strip()
1087
+ if res_name in water_names:
1088
+ unique_water_residues.add(f"{res_name}{res_num}")
1089
+
1090
+ return {
1091
+ 'atom_count': atom_count,
1092
+ 'chains': list(chains),
1093
+ 'residue_count': len(residues),
1094
+ 'water_molecules': len(unique_water_residues),
1095
+ 'ions': ions,
1096
+ 'ligands': list(ligands),
1097
+ 'hetatoms': hetatoms
1098
+ }
1099
+
1100
+ def test_structure_preparation():
1101
+ """Test function to verify structure preparation works correctly"""
1102
+ # Create a simple test PDB content
1103
+ test_pdb = """HEADER TEST PROTEIN
1104
+ ATOM 1 N MET A 1 16.347 37.019 21.335 1.00 50.73 N
1105
+ ATOM 2 CA MET A 1 15.737 37.120 20.027 1.00 45.30 C
1106
+ ATOM 3 C MET A 1 15.955 35.698 19.546 1.00 41.78 C
1107
+ ATOM 4 O MET A 1 16.847 35.123 20.123 1.00 40.15 O
1108
+ ATOM 5 CB MET A 1 14.234 37.456 19.789 1.00 44.12 C
1109
+ ATOM 6 CG MET A 1 13.456 36.123 19.234 1.00 43.45 C
1110
+ ATOM 7 SD MET A 1 12.123 35.456 18.123 1.00 42.78 S
1111
+ ATOM 8 CE MET A 1 11.456 34.123 17.456 1.00 42.11 C
1112
+ ATOM 9 N ALA A 2 15.123 35.456 18.789 1.00 40.44 N
1113
+ ATOM 10 CA ALA A 2 14.456 34.123 18.123 1.00 39.77 C
1114
+ ATOM 11 C ALA A 2 13.123 33.456 17.456 1.00 39.10 C
1115
+ ATOM 12 O ALA A 2 12.456 32.123 16.789 1.00 38.43 O
1116
+ ATOM 13 CB ALA A 2 13.789 33.123 17.123 1.00 38.76 C
1117
+ ATOM 14 N ALA A 3 12.789 32.456 16.123 1.00 38.09 N
1118
+ ATOM 15 CA ALA A 3 11.456 31.789 15.456 1.00 37.42 C
1119
+ ATOM 16 C ALA A 3 10.123 30.456 14.789 1.00 36.75 C
1120
+ ATOM 17 O ALA A 3 9.456 29.123 14.123 1.00 36.08 O
1121
+ ATOM 18 CB ALA A 3 9.789 29.456 13.456 1.00 35.41 C
1122
+ ATOM 19 OXT ALA A 3 8.123 28.789 13.456 1.00 35.74 O
1123
+ HETATM 20 O HOH A 4 20.000 20.000 20.000 1.00 20.00 O
1124
+ HETATM 21 H1 HOH A 4 20.500 20.500 20.500 1.00 20.00 H
1125
+ HETATM 22 H2 HOH A 4 19.500 19.500 19.500 1.00 20.00 H
1126
+ HETATM 23 NA NA A 5 25.000 25.000 25.000 1.00 25.00 NA
1127
+ HETATM 24 CL CL A 6 30.000 30.000 30.000 1.00 30.00 CL
1128
+ HETATM 1 PG GTP A 180 29.710 30.132 -5.989 1.00 52.48 A P
1129
+ HETATM 2 O1G GTP A 180 29.197 28.937 -5.265 1.00 43.51 A O
1130
+ HETATM 3 O2G GTP A 180 30.881 29.816 -6.827 1.00 63.11 A O
1131
+ HETATM 4 O3G GTP A 180 30.013 31.278 -5.117 1.00 29.97 A O
1132
+ HETATM 5 O3B GTP A 180 28.517 30.631 -6.995 1.00 23.23 A O
1133
+ HETATM 6 PB GTP A 180 27.017 31.171 -6.766 1.00 29.58 A P
1134
+ HETATM 7 O1B GTP A 180 26.072 30.050 -6.958 1.00 17.62 A O
1135
+ HETATM 8 O2B GTP A 180 26.960 31.913 -5.483 1.00 38.76 A O
1136
+ HETATM 9 O3A GTP A 180 26.807 32.212 -7.961 1.00 13.12 A O
1137
+ HETATM 10 PA GTP A 180 26.277 33.726 -8.045 1.00 25.06 A P
1138
+ HETATM 11 O1A GTP A 180 25.089 33.867 -7.187 1.00 44.06 A O
1139
+ HETATM 12 O2A GTP A 180 27.427 34.635 -7.843 1.00 23.47 A O
1140
+ HETATM 13 O5' GTP A 180 25.804 33.834 -9.555 1.00 42.05 A O
1141
+ HETATM 14 C5' GTP A 180 26.615 33.475 -10.679 1.00 19.97 A C
1142
+ HETATM 15 C4' GTP A 180 26.219 34.288 -11.894 1.00 14.90 A C
1143
+ HETATM 16 O4' GTP A 180 24.826 34.017 -12.143 1.00 19.00 A O
1144
+ HETATM 17 C3' GTP A 180 26.372 35.802 -11.724 1.00 4.96 A C
1145
+ HETATM 18 O3' GTP A 180 26.880 36.347 -12.936 1.00 44.49 A O
1146
+ HETATM 19 C2' GTP A 180 24.932 36.243 -11.481 1.00 17.12 A C
1147
+ HETATM 20 O2' GTP A 180 24.719 37.581 -11.901 1.00 32.45 A O
1148
+ HETATM 21 C1' GTP A 180 24.069 35.240 -12.240 1.00 16.17 A C
1149
+ HETATM 22 N9 GTP A 180 22.724 35.005 -11.630 1.00 28.10 A N
1150
+ HETATM 23 C8 GTP A 180 22.443 34.655 -10.325 1.00 27.05 A C
1151
+ HETATM 24 N7 GTP A 180 21.168 34.483 -10.079 1.00 33.25 A N
1152
+ HETATM 25 C5 GTP A 180 20.554 34.737 -11.307 1.00 26.23 A C
1153
+ HETATM 26 C6 GTP A 180 19.183 34.712 -11.659 1.00 29.31 A C
1154
+ HETATM 27 O6 GTP A 180 18.205 34.448 -10.957 1.00 40.80 A O
1155
+ HETATM 28 N1 GTP A 180 19.000 35.036 -13.013 1.00 26.85 A N
1156
+ HETATM 29 C2 GTP A 180 20.022 35.339 -13.903 1.00 28.70 A C
1157
+ HETATM 30 N2 GTP A 180 19.627 35.619 -15.147 1.00 44.24 A N
1158
+ HETATM 31 N3 GTP A 180 21.301 35.367 -13.569 1.00 21.67 A N
1159
+ HETATM 32 C4 GTP A 180 21.489 35.054 -12.257 1.00 41.91 A C
1160
+ END
1161
+ """
1162
+
1163
+ options = {
1164
+ 'remove_water': True,
1165
+ 'remove_ions': True,
1166
+ 'remove_hydrogens': True,
1167
+ 'add_ace': True,
1168
+ 'add_nme': True,
1169
+ 'preserve_ligands': True,
1170
+ 'separate_ligands': False,
1171
+ 'fix_missing_atoms': False,
1172
+ 'standardize_residues': False
1173
+ }
1174
+
1175
+ print("Testing structure preparation...")
1176
+ result = prepare_structure(test_pdb, options, "output")
1177
+
1178
+ print("\n=== STATISTICS ===")
1179
+ print(f"Original atoms: {result['original_atoms']}")
1180
+ print(f"Prepared atoms: {result['prepared_atoms']}")
1181
+ print(f"Removed: {result['removed_components']}")
1182
+ print(f"Added: {result['added_capping']}")
1183
+ print(f"Ligands: {result['preserved_ligands']}")
1184
+ print(f"Ligand present: {result['ligand_present']}")
1185
+
1186
+ print(f"\nTest completed! Check 'output' folder for results:")
1187
+ print("- 1_protein_no_hydrogens.pdb (protein without hydrogens)")
1188
+ print("- 2_protein_with_caps.pdb (protein with ACE/NME caps)")
1189
+ print("- 3_ligands_extracted.pdb (extracted ligands, if any)")
1190
+ print("- 4_ligands_corrected.pdb (corrected ligands, if any)")
1191
+ print("- tleap_ready.pdb (final structure ready for tleap)")
1192
+
1193
+ if __name__ == "__main__":
1194
+ test_structure_preparation()
pyproject.toml ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ambermdflow"
7
+ version = "0.0.1"
8
+ description = "Web-based MD simulation pipeline with AMBER, ESMFold, docking, and PLUMED"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ authors = [
12
+ {name = "Hemant Nagar", email = "hn533621@ohio.edu"}
13
+ ]
14
+ keywords = [
15
+ "molecular-dynamics",
16
+ "amber",
17
+ "md-simulation",
18
+ "protein-structure",
19
+ "esmfold",
20
+ "docking",
21
+ "autodock-vina",
22
+ "plumed",
23
+ "computational-chemistry",
24
+ "bioinformatics"
25
+ ]
26
+ classifiers = [
27
+ "Development Status :: 4 - Beta",
28
+ "Environment :: Web Environment",
29
+ "Framework :: Flask",
30
+ "Intended Audience :: Science/Research",
31
+ "License :: OSI Approved :: MIT License",
32
+ "Operating System :: OS Independent",
33
+ "Programming Language :: Python :: 3",
34
+ "Programming Language :: Python :: 3.10",
35
+ "Programming Language :: Python :: 3.11",
36
+ "Programming Language :: Python :: 3.12",
37
+ "Topic :: Scientific/Engineering :: Bio-Informatics",
38
+ "Topic :: Scientific/Engineering :: Chemistry",
39
+ ]
40
+ requires-python = ">=3.10"
41
+
42
+ # Core dependencies installable via pip
43
+ # Note: Users MUST install these via conda FIRST (not available on PyPI):
44
+ # conda install -c conda-forge ambertools pymol-open-source vina openbabel rdkit gemmi
45
+ # These conda packages also provide: numpy, pandas, matplotlib (don't override them)
46
+ dependencies = [
47
+ "flask==2.3.3",
48
+ "flask-cors==4.0.0",
49
+ "biopython",
50
+ "seaborn",
51
+ "mdanalysis",
52
+ "gunicorn==21.2.0",
53
+ "requests",
54
+ "meeko>=0.7.0",
55
+ "prody",
56
+ "numpy<2.0",
57
+ ]
58
+
59
+ [project.optional-dependencies]
60
+ dev = [
61
+ "pytest>=7.0.0",
62
+ "pytest-cov>=4.0.0",
63
+ "black>=23.0.0",
64
+ "isort>=5.12.0",
65
+ "flake8>=6.0.0",
66
+ ]
67
+
68
+ [project.urls]
69
+ Homepage = "https://huggingface.co/spaces/hemantn/AmberMDFlow"
70
+ Documentation = "https://github.com/nagarh/AmberMDFlow"
71
+ Repository = "https://github.com/nagarh/AmberMDFlow"
72
+ Issues = "https://github.com/nagarh/AmberMDFlow/issues"
73
+
74
+ [project.scripts]
75
+ ambermdflow = "ambermdflow.__main__:main"
76
+
77
+ [tool.setuptools]
78
+ include-package-data = true
79
+
80
+ [tool.setuptools.packages.find]
81
+ where = ["."]
82
+ include = ["ambermdflow*"]
83
+
84
+ [tool.setuptools.package-data]
85
+ ambermdflow = [
86
+ "html/*.html",
87
+ "css/*.css",
88
+ "js/*.js",
89
+ ]
90
+
91
+ [tool.black]
92
+ line-length = 100
93
+ target-version = ["py310", "py311", "py312"]
94
+
95
+ [tool.isort]
96
+ profile = "black"
97
+ line_length = 100
start_web_server.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ AmberMDFlow - Entry point when running from project root.
4
+ Uses the ambermdflow package. For installed package: use `ambermdflow` or `python -m ambermdflow`.
5
+ """
6
+
7
+ from ambermdflow.app import app
8
+
9
+ if __name__ == "__main__":
10
+ app.run(debug=False, host="0.0.0.0", port=7860)