hemantn commited on
Commit
cde5d27
·
1 Parent(s): 3000eac

Sync with development: Dockerfile (meeko, vina, openbabel, PYTHONPATH), amberflow package, .gitignore. Exclude Test/ (binary .ncrst not allowed on HF Hub).

Browse files
.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
+
.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
+ # Test data (local only; binaries not allowed on HF Hub)
61
+ Test/
Dockerfile CHANGED
@@ -34,13 +34,14 @@ RUN conda tos accept --override-channels --channel https://repo.anaconda.com/pkg
34
  # Install mamba for faster package installation
35
  RUN conda install -n base -c conda-forge mamba -y
36
 
37
- # Install AMBER tools and PyMOL using mamba with Python 3.11 (compatible version)
38
- RUN mamba install -y python=3.11 conda-forge::ambertools conda-forge::pymol-open-source
 
39
 
40
  # Clean up conda/mamba cache to reduce image size
41
  RUN conda clean -afy
42
 
43
- # Install Python packages via pip
44
  RUN pip install --no-cache-dir \
45
  flask==2.3.3 \
46
  flask-cors==4.0.0 \
@@ -53,7 +54,7 @@ RUN pip install --no-cache-dir \
53
  gunicorn==21.2.0 \
54
  requests==2.31.0 \
55
  rdkit==2023.3.1 \
56
- scipy==1.11.1
57
 
58
  # Set working directory
59
  WORKDIR /AmberFlow
@@ -65,12 +66,11 @@ COPY . .
65
  RUN mkdir -p /AmberFlow/obsolete /AmberFlow/pdb /AmberFlow/temp /AmberFlow/output && \
66
  chmod -R 777 /AmberFlow
67
 
68
- # Make sure the python directory is in the Python path
69
- ENV PYTHONPATH="${PYTHONPATH}:/AmberFlow/python"
70
 
71
  # Expose the port
72
  EXPOSE 7860
73
 
74
  # Run the application
75
  CMD ["python", "start_web_server.py"]
76
-
 
34
  # Install mamba for faster package installation
35
  RUN conda install -n base -c conda-forge mamba -y
36
 
37
+ # Install AMBER tools, PyMOL, AutoDock Vina, and Open Babel (for docking)
38
+ RUN mamba install -y python=3.11 conda-forge::ambertools conda-forge::pymol-open-source \
39
+ conda-forge::autodock-vina conda-forge::openbabel
40
 
41
  # Clean up conda/mamba cache to reduce image size
42
  RUN conda clean -afy
43
 
44
+ # Install Python packages via pip (AmberFlow deps: Dockerfile minus scipy, plus meeko for docking)
45
  RUN pip install --no-cache-dir \
46
  flask==2.3.3 \
47
  flask-cors==4.0.0 \
 
54
  gunicorn==21.2.0 \
55
  requests==2.31.0 \
56
  rdkit==2023.3.1 \
57
+ meeko>=0.7.0
58
 
59
  # Set working directory
60
  WORKDIR /AmberFlow
 
66
  RUN mkdir -p /AmberFlow/obsolete /AmberFlow/pdb /AmberFlow/temp /AmberFlow/output && \
67
  chmod -R 777 /AmberFlow
68
 
69
+ # Make sure the amberflow package is on the Python path
70
+ ENV PYTHONPATH="${PYTHONPATH}:/AmberFlow"
71
 
72
  # Expose the port
73
  EXPOSE 7860
74
 
75
  # Run the application
76
  CMD ["python", "start_web_server.py"]
 
README.md CHANGED
@@ -1,124 +1,295 @@
 
 
 
 
1
  ---
2
- title: AmberFlow - MD Simulation Pipeline
3
- emoji: 🧬
4
- colorFrom: blue
5
- colorTo: purple
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- app_port: 7860
 
 
 
 
 
 
10
  ---
11
 
12
- # AmberFlow - Molecular Dynamics Simulation Pipeline
13
 
14
- 🧬 **AmberFlow** is a comprehensive web-based pipeline for preparing and setting up molecular dynamics (MD) simulations using the AMBER force field. This tool provides an intuitive interface for protein structure preparation, parameter generation, and simulation file creation.
15
 
16
- ## Features
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- ### 🔬 Structure Preparation
19
- - **Protein Loading**: Upload PDB files or fetch from RCSB PDB database
20
- - **Structure Cleaning**: Remove water molecules, ions, and hydrogen atoms
21
- - **Capping Groups**: Add ACE (N-terminal) and NME (C-terminal) capping groups
22
- - **Ligand Handling**: Preserve and process ligands with automatic force field parameter generation
23
- - **3D Visualization**: Interactive molecular viewer using NGL
24
-
25
- ### ⚙️ Simulation Parameters
26
- - **Force Fields**: Support for ff14SB and ff19SB protein force fields
27
- - **Water Models**: TIP3P and SPCE water models
28
- - **System Setup**: Configurable box size and ion addition
29
- - **Thermodynamics**: Temperature and pressure control
30
-
31
- ### 📋 Simulation Steps
32
- - **Restrained Minimization**: Position-restrained energy minimization
33
- - **Minimization**: Full system energy minimization
34
- - **NPT Heating**: Temperature equilibration
35
- - **NPT Equilibration**: Pressure and temperature equilibration
36
- - **Production Run**: Configurable production MD simulation
37
-
38
- ### 📁 File Generation
39
- - **AMBER Input Files**: Complete set of .in files for all simulation steps
40
- - **Force Field Parameters**: Generated .prmtop and .inpcrd files
41
- - **PBS Scripts**: HPC submission scripts
42
- - **Analysis Scripts**: Post-simulation analysis tools
43
 
44
  ## Usage
45
 
46
- 1. **Load Protein Structure**
47
- - Upload a PDB file or enter a PDB ID to fetch from RCSB
48
- - View 3D structure and basic information
49
-
50
- 2. **Prepare Structure**
51
- - Configure structure preparation options
52
- - Remove unwanted components (water, ions, hydrogens)
53
- - Add capping groups for termini
54
- - Handle ligands if present
55
-
56
- 3. **Set Simulation Parameters**
57
- - Choose force field and water model
58
- - Configure system parameters
59
- - Set temperature and pressure
60
-
61
- 4. **Configure Simulation Steps**
62
- - Enable/disable simulation steps
63
- - Set step-specific parameters
64
- - Configure production run duration
65
-
66
- 5. **Generate Files**
67
- - Generate all simulation input files
68
- - Download files as ZIP archive
69
- - Preview generated files
70
-
71
- ## Technical Details
72
-
73
- ### Dependencies
74
- - **MDAnalysis**: Structure manipulation and analysis
75
- - **BioPython**: PDB file parsing
76
- - **Flask**: Web framework
77
- - **NGL Viewer**: 3D molecular visualization
78
- - **AMBER Tools**: Force field parameter generation
79
-
80
- ### File Structure
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  ```
82
  AmberFlow/
83
- ├── app.py # Hugging Face Spaces entry point
84
- ├── requirements.txt # Python dependencies
85
- ├── python/
86
- │ ├── app.py # Main Flask application
87
- │ ├── structure_preparation.py
88
- │ └── requirements.txt
89
  ├── html/
90
- ── index.html # Web interface
 
91
  ├── css/
92
- ── styles.css # Styling
 
93
  ├── js/
94
- ── script.js # Frontend logic
95
- ├── templates/ # AMBER input file templates
96
- └── add_caps.py # Capping group addition script
 
 
 
 
 
 
 
 
 
 
97
  ```
98
 
 
 
99
  ## Citation
100
 
101
- If you use AmberFlow in your research, please cite:
102
 
103
  ```bibtex
104
- @software{Amberflow2025,
105
- title={AmberFlow: Molecular Dynamics Simulation Pipeline},
106
- author={Hemant Nagar},
107
- year={2025},
108
- url={https://huggingface.co/spaces/hemantn/AmberFlow}
109
  }
110
  ```
111
 
 
 
 
 
 
 
 
 
 
 
112
  ## Acknowledgments
113
 
114
- - **Mohd Ibrahim** (Technical University of Munich) for the protein capping functionality (`add_caps.py`)
 
 
115
 
116
  ## License
117
 
118
- This project is licensed under the MIT License - see the LICENSE file for details.
 
 
119
 
120
  ## Contact
121
 
122
- - **Author**: Hemant Nagar
123
  - **Email**: hn533621@ohio.edu
124
-
 
1
+ # AmberFlow
2
+
3
+ **AmberFlow** 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.
4
+
5
  ---
6
+
7
+ ## Features
8
+
9
+ | Section | Description |
10
+ |--------|-------------|
11
+ | **Protein Loading** | Upload PDB files or fetch from RCSB PDB; 3D visualization with NGL |
12
+ | **Fill Missing Residues** | Detect missing residues (RCSB annotations), complete with ESMFold, optional trimming and energy minimization of predicted structure|
13
+ | **Structure Preparation** | Remove water/ions/H; add ACE/NME capping; chain and ligand selection; GAFF/GAFF2 parameterization |
14
+ | **Ligand Docking** | AutoDock Vina + Meeko; configurable search box; pose selection and use selected ligand pose to setup MD simulations |
15
+ | **Simulation Parameters** | Force fields (ff14SB, ff19SB), water models (TIP3P, SPCE), box size, temperature, pressure |
16
+ | **Simulation Steps** | Restrained minimization, minimization, NVT, NPT, production — each with configurable parameters |
17
+ | **Generate Files** | AMBER `.in` files, `prmtop`/`inpcrd`, PBS submission scripts |
18
+ | **PLUMED** | Collective variables (PLUMED v2.9), `plumed.dat` editor, and simulation file generation with PLUMED |
19
+
20
  ---
21
 
22
+ ## Requirements for Custom PDB Files
23
 
24
+ For **custom PDB files** (uploaded or fetched), ensure:
25
 
26
+ | Requirement | Description |
27
+ |-------------|-------------|
28
+ | **Chain IDs** | Chain IDs must be clearly marked in the PDB (column 22). The pipeline uses them for chain selection, missing-residue filling, and structure preparation. |
29
+ | **Ligands as HETATM** | All non-protein, non-water, non-ion molecules (e.g., cofactors, drugs) must be in **HETATM** records. The pipeline detects and lists only HETATM entities as ligands. |
30
+ | **Standard amino acids** | AmberFlow supports **standard amino acids** only. Non-standard residues (e.g., MSE, HYP, SEC, non-canonical modifications) are not explicitly parameterized; pre-process or replace them before use if needed. |
31
+
32
+ For RCSB structures, the pipeline parses the header and HETATM as provided; for your own PDBs, apply the above conventions.
33
+
34
+ ---
35
+
36
+ ## Installation
37
+
38
+ ### Option 1: Docker (recommended)
39
+
40
+ ```bash
41
+ git clone https://github.com/your-org/AmberFlow.git
42
+ cd AmberFlow
43
+ docker build -t amberflow .
44
+ docker run -p 7860:7860 amberflow
45
+ ```
46
+
47
+ Open `http://localhost:7860` in your browser.
48
+
49
+ ### Option 2: Local (Conda + pip)
50
+
51
+ 1. **Conda environment** with AMBER tools, PyMOL, and Python 3.10–3.11:
52
+
53
+ ```bash
54
+ conda create -n amberflow python=3.11
55
+ conda activate amberflow
56
+ conda install -c conda-forge ambertools pymol-open-source
57
+ ```
58
+
59
+ 2. **Conda packages for docking** (Vina, Open Babel; Meeko is via pip):
60
+
61
+ ```bash
62
+ conda install -c conda-forge autodock-vina openbabel
63
+ ```
64
+
65
+ 3. **Python packages**:
66
+
67
+ ```bash
68
+ pip install -r requirements.txt
69
+ # or: pip install flask flask-cors biopython numpy pandas matplotlib seaborn mdanalysis gunicorn requests rdkit meeko vina
70
+ ```
71
+
72
+ 3. **Run the app**:
73
+
74
+ ```bash
75
+ python start_web_server.py
76
+ ```
77
+
78
+ The app listens on `http://0.0.0.0:7860` by default.
79
 
80
+ ### Option 3: Install from PyPI (when published)
81
+
82
+ ```bash
83
+ pip install amberflow
84
+ # Requires: AMBER tools, PyMOL, AutoDock Vina, Open Babel (e.g. conda install -c conda-forge ambertools pymol-open-source autodock-vina openbabel)
85
+ amberflow
86
+ ```
87
+
88
+ See **[PACKAGING.md](PACKAGING.md)** for dependency list, build, and PyPI release steps.
89
+
90
+ ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
  ## Usage
93
 
94
+ ### 1. Protein Loading
95
+
96
+ - **Upload**: Drag-and-drop or choose a `.pdb` / `.ent` file.
97
+ - **Fetch**: Enter a 4-character PDB ID (e.g. `1CRN`) to download from RCSB.
98
+
99
+ 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.
100
+
101
+ ---
102
+
103
+ ### 2. Fill Missing Residues
104
+
105
+ - Click **Analyze Missing Residues** to detect gaps from RCSB metadata.
106
+ - **Select chains** to complete with ESMFold.
107
+ - **Trim residues** (optional): remove residues from N- or C-terminal edges; internal loops are always filled by ESMFold.
108
+ - **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.
109
+ - **Build Completed Structure** to run ESMFold and (if requested) minimization. Use **Preview Completed Structure** and **View Superimposed Structures** to compare original and completed chains.
110
+
111
+ > If you use ESMFold in this workflow, please cite [ESM Atlas](https://esmatlas.com/about).
112
+
113
+ ---
114
+
115
+ ### 3. Structure Preparation
116
+
117
+ - **Remove**: Water, ions, and hydrogens (options are pre-configured).
118
+ - **Add capping**: ACE (N-terminal) and NME (C-terminal).
119
+ - **Chains**: Select which protein chains to keep for force field generation.
120
+ - **Ligands**:
121
+ - **Preserve ligands** to keep them in the structure.
122
+ - **Select ligands to preserve** (e.g. `GOL-A-1`, `LIZ-A`). Unselected ligands are dropped.
123
+ - **Create separate ligand file** to export selected ligand(s) to a PDB.
124
+
125
+ 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.
126
+
127
+ **Ligand Docking** (nested in this tab):
128
+
129
+ - Select ligands to dock.
130
+ - Set the **search space** (center and size in X, Y, Z) with live 3D visualization.
131
+ - **Run Docking** (AutoDock Vina + Meeko). Progress and logs are shown in the docking panel.
132
+ - **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.
133
+
134
+ ---
135
+
136
+ ### 4. Simulation Parameters
137
+
138
+ - **Force field**: ff14SB or ff19SB.
139
+ - **Water model**: TIP3P or SPCE.
140
+ - **Box size** (Å): padding for solvation.
141
+ - **Add ions**: to neutralize (and optionally reach a salt concentration).
142
+ - **Temperature** and **Pressure** (e.g. 300 K, 1 bar).
143
+ - **Time step** and **Cutoff** for non-bonded interactions.
144
+
145
+ If ligands were preserved, **Ligand force field** (GAFF/GAFF2) is configured here; net charge is computed before `antechamber` runs.
146
+
147
+ ---
148
+
149
+ ### 5. Simulation Steps
150
+
151
+ Enable/disable and set parameters for:
152
+
153
+ - **Restrained minimization** (steps, force constant)
154
+ - **Minimization** (steps, cutoff)
155
+ - **NVT heating** (steps, temperature)
156
+ - **NPT equilibration** (steps, temperature, pressure)
157
+ - **Production** (steps, temperature, pressure)
158
+
159
+ ---
160
+
161
+ ### 6. Generate Files
162
+
163
+ - **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`.
164
+ - **Preview Files** to open and **edit** each file (e.g. `min.in`, `submit_job.pbs`) and **Save**; changes are written to the output directory.
165
+ - **Preview Solvated Protein** / **Download Solvated Protein** to inspect and download the solvated system.
166
+
167
+ 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.
168
+
169
+ ---
170
+
171
+ ### 7. PLUMED
172
+
173
+ - **Collective Variables**: search and select CVs from the PLUMED v2.9 set; view docs and add/edit lines in `plumed.dat`.
174
+ - **Custom PLUMED**: edit `plumed.dat` directly.
175
+ - **Generate simulation files**: create AMBER + PLUMED input files. Generated files can be **previewed, edited, and saved** as in the main **Generate Files** tab.
176
+
177
+ > PLUMED citation: [plumed.org/cite](https://www.plumed.org/cite).
178
+
179
+ ---
180
+
181
+ ## Pipeline Overview
182
+
183
+ ```
184
+ Protein Loading (upload/fetch)
185
+
186
+ Fill Missing Residues (detect → ESMFold → optional trim & minimize)
187
+
188
+ Structure Preparation (clean, cap, chains, ligands) → optional Docking (Vina, apply pose)
189
+
190
+ Simulation Parameters (FF, water, box, T, P, etc.)
191
+
192
+ Simulation Steps (min, NVT, NPT, prod)
193
+
194
+ Generate Files (AMBER .in, tleap, prmtop/inpcrd, PBS)
195
+
196
+ [Optional] PLUMED (CVs, plumed.dat, generate PLUMED-enabled files)
197
+ ```
198
+
199
+ ---
200
+
201
+ ## Output Layout
202
+
203
+ Generated files are written under `output/` (or the path set in the app), for example:
204
+
205
+ - `0_original_input.pdb` — raw input
206
+ - `1_protein_no_hydrogens.pdb` — cleaned, capped, chain/ligand selection applied
207
+ - `2_protein_with_caps.pdb`, `tleap_ready.pdb` — intermediates
208
+ - `4_ligands_corrected_*.pdb` — prepared ligands
209
+ - `protein.prmtop`, `protein.inpcrd` — after `tleap`
210
+ - `min_restrained.in`, `min.in`, `HeatNPT.in`, `mdin_equi.in`, `mdin_prod.in`, `submit_job.pbs`
211
+ - `output/docking/` — receptor, ligands, Vina configs, poses, logs
212
+ - `plumed.dat` — when using PLUMED
213
+
214
+ ---
215
+
216
+ ## Dependencies
217
+
218
+ | Category | Tools / libraries |
219
+ |----------|-------------------|
220
+ | **Python** | Flask, Flask-CORS, BioPython, NumPy, Pandas, Matplotlib, Seaborn, MDAnalysis, Requests, RDKit, SciPy |
221
+ | **AMBER** | AMBER Tools (tleap, antechamber, sander, ambpdb, etc.) |
222
+ | **Docking** | Meeko (`mk_prepare_ligand`, `mk_prepare_receptor`), AutoDock Vina, Open Babel |
223
+ | **Visualization** | PyMOL (scripted for H removal, structure editing), NGL (in-browser 3D) |
224
+ | **Structure completion** | ESMFold (via API or local, depending on deployment) |
225
+
226
+ ---
227
+
228
+ ## Project Structure
229
+
230
  ```
231
  AmberFlow/
232
+ ├── start_web_server.py # Entry point
 
 
 
 
 
233
  ├── html/
234
+ ── index.html # Main UI
235
+ │ └── plumed.html # PLUMED-focused view (if used)
236
  ├── css/
237
+ ── styles.css
238
+ │ └── plumed.css
239
  ├── js/
240
+ ── script.js # Main frontend logic
241
+ ├── plumed.js # PLUMED + docking UI
242
+ └── plumed_cv_docs.js # CV documentation
243
+ ├── python/
244
+ │ ├── app.py # Flask backend, API, file generation
245
+ │ ├── structure_preparation.py
246
+ │ ├── add_caps.py # ACE/NME capping
247
+ │ ├── Fill_missing_residues.py # ESMFold, trimming, minimization
248
+ │ ├── docking.py # Docking helpers
249
+ │ └── docking_utils.py
250
+ ├── output/ # Generated files (gitignored in dev)
251
+ ├── Dockerfile
252
+ └── README.md
253
  ```
254
 
255
+ ---
256
+
257
  ## Citation
258
 
259
+ If you use AmberFlow in your work, please cite:
260
 
261
  ```bibtex
262
+ @software{AmberFlow,
263
+ title = {AmberFlow: Molecular Dynamics and Docking Pipeline},
264
+ author = {Nagar, Hemant},
265
+ year = {2025},
266
+ url = {https://github.com/your-org/AmberFlow}
267
  }
268
  ```
269
 
270
+ **Related software to cite when used:**
271
+
272
+ - **AMBER**: [ambermd.org](https://ambermd.org)
273
+ - **PLUMED**: [plumed.org/cite](https://www.plumed.org/cite)
274
+ - **ESMFold / ESM Atlas**: [esmatlas.com/about](https://esmatlas.com/about)
275
+ - **AutoDock Vina**: Trott & Olson, *J. Comput. Chem.* (2010)
276
+ - **Meeko**: [github.com/forlilab/Meeko](https://github.com/forlilab/Meeko)
277
+
278
+ ---
279
+
280
  ## Acknowledgments
281
 
282
+ - **Mohd Ibrahim** (Technical University of Munich) for the protein capping logic (`add_caps.py`).
283
+
284
+ ---
285
 
286
  ## License
287
 
288
+ MIT License. See `LICENSE` for details.
289
+
290
+ ---
291
 
292
  ## Contact
293
 
294
+ - **Author**: Hemant Nagar
295
  - **Email**: hn533621@ohio.edu
 
SETUP_STEPS.md ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AmberFlow_development Repository Setup Steps
2
+
3
+ This document outlines all the steps taken to create the `AmberFlow_development` repository and sync files from the original `AmberFlow` repository.
4
+
5
+ ## Steps Performed
6
+
7
+ ### 1. Create New Folder
8
+ ```bash
9
+ mkdir -p AmberFlow_development
10
+ ```
11
+ Created a new directory called `AmberFlow_development` in the home directory (`/home/hn533621/`).
12
+
13
+ ### 2. Copy All Files from AmberFlow
14
+ ```bash
15
+ cp -r AmberFlow/* AmberFlow_development/
16
+ cp -r AmberFlow/.[!.]* AmberFlow_development/
17
+ ```
18
+ Copied all files and hidden files (except `.` and `..`) from the `AmberFlow` directory to the new `AmberFlow_development` directory.
19
+
20
+ **Files copied:**
21
+ - `add_caps.py`
22
+ - `css/` directory
23
+ - `Dockerfile`
24
+ - `.gitattributes`
25
+ - `html/` directory
26
+ - `js/` directory
27
+ - `obsolete/` directory
28
+ - `output/` directory
29
+ - `python/` directory
30
+ - `README.md`
31
+ - `start_web_server.py`
32
+
33
+ ### 3. Remove Old Git Repository
34
+ ```bash
35
+ rm -rf .git
36
+ ```
37
+ Removed the existing `.git` directory from the copied files to start fresh with a new git repository.
38
+
39
+ ### 4. Create .gitignore File
40
+ Created a `.gitignore` file to exclude unnecessary files from version control:
41
+
42
+ **Contents:**
43
+ - Python cache files (`__pycache__/`, `*.pyc`, etc.)
44
+ - Output files (`output/`, `*.pdb`, `*.cif`, `*.log`)
45
+ - IDE files (`.vscode/`, `.idea/`, etc.)
46
+ - OS files (`.DS_Store`, `Thumbs.db`)
47
+ - Environment files (`.env`, `venv/`, etc.)
48
+
49
+ ### 5. Initialize Git Repository
50
+ ```bash
51
+ git init
52
+ ```
53
+ Initialized a new empty git repository in the `AmberFlow_development` directory.
54
+
55
+ ### 6. Rename Branch to main
56
+ ```bash
57
+ git branch -m main
58
+ ```
59
+ Renamed the default branch from `master` to `main` to follow modern git conventions.
60
+
61
+ ### 7. Stage All Files
62
+ ```bash
63
+ git add .
64
+ ```
65
+ Staged all files in the repository for the initial commit.
66
+
67
+ ### 8. Create Initial Commit
68
+ ```bash
69
+ git commit -m "Initial commit: Copy from AmberFlow"
70
+ ```
71
+ Created the initial commit with all copied files.
72
+
73
+ **Commit details:**
74
+ - 11 files changed
75
+ - 6,567 insertions
76
+ - Files included: `.gitattributes`, `.gitignore`, `Dockerfile`, `README.md`, `add_caps.py`, CSS, HTML, JS files, Python application files, and `start_web_server.py`
77
+
78
+ ### 9. Create Private GitHub Repository
79
+ ```bash
80
+ gh repo create AmberFlow_development --private --source=. --remote=origin --push
81
+ ```
82
+ Created a private GitHub repository named `AmberFlow_development` and pushed all files.
83
+
84
+ **Repository details:**
85
+ - Repository URL: https://github.com/nagarh/AmberFlow_development
86
+ - Visibility: Private
87
+ - Remote name: `origin`
88
+ - Branch: `main`
89
+
90
+ ### 10. Verify Setup
91
+ ```bash
92
+ git remote -v
93
+ git status
94
+ ```
95
+ Verified that:
96
+ - Remote `origin` is correctly configured to point to the GitHub repository
97
+ - All files are committed and pushed
98
+ - Working tree is clean
99
+
100
+ ## Summary
101
+
102
+ The `AmberFlow_development` repository has been successfully created as a private GitHub repository with all files from the original `AmberFlow` directory. The repository is now ready for development work and version control.
103
+
104
+ ## Future Git Operations
105
+
106
+ To make changes and push them to GitHub:
107
+
108
+ 1. **Stage changes:**
109
+ ```bash
110
+ git add .
111
+ ```
112
+
113
+ 2. **Commit changes:**
114
+ ```bash
115
+ git commit -m "Your commit message"
116
+ ```
117
+
118
+ 3. **Push to GitHub:**
119
+ ```bash
120
+ git push
121
+ ```
122
+
123
+ ## Notes
124
+
125
+ - The original `AmberFlow` repository remains unchanged and still points to the Hugging Face Space
126
+ - The new `AmberFlow_development` repository is completely independent
127
+ - A `.gitignore` file has been added to exclude unnecessary files from version control
128
+
amberflow/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
+ )
amberflow/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """AmberFlow: MD simulation pipeline with AMBER, ESMFold, docking, and PLUMED."""
2
+
3
+ __version__ = "0.1.0"
amberflow/__main__.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run the AmberFlow web server. Use: python -m amberflow or amberflow"""
2
+
3
+ from amberflow.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()
add_caps.py → amberflow/add_caps.py RENAMED
File without changes
amberflow/app.py ADDED
The diff for this file is too large to render. See raw diff
 
amberflow/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
+
{css → amberflow/css}/styles.css RENAMED
@@ -231,6 +231,43 @@ body {
231
  color: #3498db;
232
  }
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  /* Protein Loading Styles */
235
  .input-methods {
236
  display: grid;
@@ -398,6 +435,32 @@ body {
398
  margin-top: 1rem;
399
  }
400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  .protein-info p {
402
  margin: 0.5rem 0;
403
  font-size: 1rem;
@@ -952,6 +1015,12 @@ input:checked + .slider:before {
952
  color: #3498db;
953
  }
954
 
 
 
 
 
 
 
955
  .prep-options {
956
  display: flex;
957
  flex-direction: column;
@@ -1022,6 +1091,13 @@ input:checked + .slider:before {
1022
  padding: 1.5rem;
1023
  border: 1px solid #dee2e6;
1024
  margin-top: 2rem;
 
 
 
 
 
 
 
1025
  }
1026
 
1027
  .prepared-structure-preview h3 {
@@ -1111,3 +1187,835 @@ input:checked + .slider:before {
1111
  .p-1 { padding: 0.5rem; }
1112
  .p-2 { padding: 1rem; }
1113
  .p-3 { padding: 1.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;
 
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;
 
1015
  color: #3498db;
1016
  }
1017
 
1018
+ .prep-section-fullwidth {
1019
+ width: 100%;
1020
+ margin-top: 1rem;
1021
+ margin-bottom: 2rem;
1022
+ }
1023
+
1024
  .prep-options {
1025
  display: flex;
1026
  flex-direction: column;
 
1091
  padding: 1.5rem;
1092
  border: 1px solid #dee2e6;
1093
  margin-top: 2rem;
1094
+ width: 100%;
1095
+ box-sizing: border-box;
1096
+ }
1097
+
1098
+ #sequence-viewer-section {
1099
+ width: 100%;
1100
+ box-sizing: border-box;
1101
  }
1102
 
1103
  .prepared-structure-preview h3 {
 
1187
  .p-1 { padding: 0.5rem; }
1188
  .p-2 { padding: 1rem; }
1189
  .p-3 { padding: 1.5rem; }
1190
+
1191
+ /* Missing Residues Horizontal Display */
1192
+ .missing-residues-horizontal {
1193
+ display: inline;
1194
+ color: #155724;
1195
+ font-weight: bold;
1196
+ font-size: 0.95rem;
1197
+ word-wrap: break-word;
1198
+ white-space: normal;
1199
+ line-height: 1.6;
1200
+ margin: 0;
1201
+ padding: 0;
1202
+ }
1203
+
1204
+ .chain-missing-info {
1205
+ margin-bottom: 1.5rem;
1206
+ }
1207
+
1208
+ .chain-missing-info h4 {
1209
+ color: #155724;
1210
+ margin-bottom: 0.5rem;
1211
+ }
1212
+
1213
+ /* Sequence Viewer Styles */
1214
+ .sequence-viewer-container {
1215
+ background: #ffffff;
1216
+ border: 1px solid #dee2e6;
1217
+ border-radius: 8px;
1218
+ padding: 1.5rem;
1219
+ max-height: 600px;
1220
+ overflow-y: auto;
1221
+ font-family: 'Courier New', monospace;
1222
+ width: 100%;
1223
+ box-sizing: border-box;
1224
+ }
1225
+
1226
+ .sequence-chain-container {
1227
+ margin-bottom: 2rem;
1228
+ border-bottom: 2px solid #e9ecef;
1229
+ padding-bottom: 1.5rem;
1230
+ }
1231
+
1232
+ .sequence-chain-container:last-child {
1233
+ border-bottom: none;
1234
+ margin-bottom: 0;
1235
+ }
1236
+
1237
+ .sequence-chain-header {
1238
+ margin-bottom: 1rem;
1239
+ }
1240
+
1241
+ .sequence-chain-header h4 {
1242
+ margin: 0;
1243
+ font-size: 1.1rem;
1244
+ font-weight: 600;
1245
+ }
1246
+
1247
+ .sequence-display {
1248
+ background: #f8f9fa;
1249
+ border: 1px solid #dee2e6;
1250
+ border-radius: 4px;
1251
+ padding: 1rem;
1252
+ font-size: 14px;
1253
+ line-height: 1.8;
1254
+ width: 100%;
1255
+ box-sizing: border-box;
1256
+ }
1257
+
1258
+ .sequence-line {
1259
+ display: flex;
1260
+ margin-bottom: 2px;
1261
+ white-space: nowrap;
1262
+ }
1263
+
1264
+ .sequence-line-number {
1265
+ color: #6c757d;
1266
+ margin-right: 1rem;
1267
+ min-width: 60px;
1268
+ text-align: right;
1269
+ font-size: 12px;
1270
+ user-select: none;
1271
+ }
1272
+
1273
+ .sequence-characters {
1274
+ letter-spacing: 2px;
1275
+ word-spacing: 0;
1276
+ flex: 1;
1277
+ overflow-x: auto;
1278
+ min-width: 0;
1279
+ }
1280
+
1281
+ .sequence-char {
1282
+ display: inline-block;
1283
+ padding: 2px 1px;
1284
+ transition: background-color 0.2s;
1285
+ }
1286
+
1287
+ .sequence-char:hover {
1288
+ background-color: rgba(0, 0, 0, 0.1);
1289
+ border-radius: 2px;
1290
+ }
1291
+
1292
+ /* Trim Residues Section */
1293
+ .trim-info-box {
1294
+ background: #e7f3ff;
1295
+ border: 1px solid #b3d9ff;
1296
+ border-radius: 6px;
1297
+ padding: 1rem;
1298
+ margin-top: 1rem;
1299
+ margin-bottom: 1rem;
1300
+ color: #004085;
1301
+ font-size: 0.9rem;
1302
+ line-height: 1.6;
1303
+ }
1304
+
1305
+ .trim-info-box i {
1306
+ color: #0066cc;
1307
+ margin-right: 0.5rem;
1308
+ }
1309
+
1310
+ .trim-info-box strong {
1311
+ color: #003366;
1312
+ }
1313
+
1314
+ .trim-residues-container {
1315
+ margin-top: 1rem;
1316
+ display: grid;
1317
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
1318
+ gap: 1rem;
1319
+ }
1320
+
1321
+ .trim-chain-controls {
1322
+ background: #f8f9fa;
1323
+ border: 1px solid #dee2e6;
1324
+ border-radius: 6px;
1325
+ padding: 1rem;
1326
+ }
1327
+
1328
+ .trim-chain-controls h5 {
1329
+ color: #2c3e50;
1330
+ margin-bottom: 0.75rem;
1331
+ font-size: 1rem;
1332
+ }
1333
+
1334
+ .trim-inputs {
1335
+ display: flex;
1336
+ gap: 1.5rem;
1337
+ align-items: center;
1338
+ flex-wrap: wrap;
1339
+ }
1340
+
1341
+ .trim-input-group {
1342
+ display: flex;
1343
+ align-items: center;
1344
+ gap: 0.5rem;
1345
+ }
1346
+
1347
+ .trim-input-group label {
1348
+ font-weight: 600;
1349
+ color: #495057;
1350
+ font-size: 0.9rem;
1351
+ min-width: 100px;
1352
+ }
1353
+
1354
+ .trim-limit {
1355
+ font-weight: normal;
1356
+ color: #6c757d;
1357
+ font-size: 0.85rem;
1358
+ font-style: italic;
1359
+ }
1360
+
1361
+ .trim-input-group input[type="number"] {
1362
+ width: 80px;
1363
+ padding: 0.5rem;
1364
+ border: 1px solid #ced4da;
1365
+ border-radius: 4px;
1366
+ font-size: 0.9rem;
1367
+ }
1368
+
1369
+ .trim-input-group input[type="number"]:focus {
1370
+ outline: none;
1371
+ border-color: #3498db;
1372
+ box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
1373
+ }
1374
+
1375
+ .trim-info {
1376
+ font-size: 0.85rem;
1377
+ color: #6c757d;
1378
+ margin-top: 0.5rem;
1379
+ font-style: italic;
1380
+ }
1381
+
1382
+ /* Log Modal Styles */
1383
+ .log-modal {
1384
+ display: none;
1385
+ position: fixed;
1386
+ z-index: 10000;
1387
+ left: 0;
1388
+ top: 0;
1389
+ width: 100%;
1390
+ height: 100%;
1391
+ background-color: rgba(0, 0, 0, 0.5);
1392
+ animation: fadeIn 0.3s;
1393
+ }
1394
+
1395
+ @keyframes fadeIn {
1396
+ from { opacity: 0; }
1397
+ to { opacity: 1; }
1398
+ }
1399
+
1400
+ .log-modal-content {
1401
+ background-color: #fefefe;
1402
+ margin: 2% auto;
1403
+ padding: 0;
1404
+ border: 1px solid #888;
1405
+ border-radius: 8px;
1406
+ width: 90%;
1407
+ max-width: 900px;
1408
+ max-height: 90vh;
1409
+ display: flex;
1410
+ flex-direction: column;
1411
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
1412
+ animation: slideDown 0.3s;
1413
+ }
1414
+
1415
+ @keyframes slideDown {
1416
+ from {
1417
+ transform: translateY(-50px);
1418
+ opacity: 0;
1419
+ }
1420
+ to {
1421
+ transform: translateY(0);
1422
+ opacity: 1;
1423
+ }
1424
+ }
1425
+
1426
+ .log-modal-header {
1427
+ background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
1428
+ color: white;
1429
+ padding: 1rem 1.5rem;
1430
+ display: flex;
1431
+ justify-content: space-between;
1432
+ align-items: center;
1433
+ border-radius: 8px 8px 0 0;
1434
+ }
1435
+
1436
+ .log-modal-header h3 {
1437
+ margin: 0;
1438
+ font-size: 1.3rem;
1439
+ font-weight: 500;
1440
+ }
1441
+
1442
+ .log-modal-close {
1443
+ color: white;
1444
+ font-size: 2rem;
1445
+ font-weight: bold;
1446
+ background: none;
1447
+ border: none;
1448
+ cursor: pointer;
1449
+ padding: 0;
1450
+ width: 30px;
1451
+ height: 30px;
1452
+ display: flex;
1453
+ align-items: center;
1454
+ justify-content: center;
1455
+ border-radius: 50%;
1456
+ transition: background-color 0.2s;
1457
+ }
1458
+
1459
+ .log-modal-close:hover {
1460
+ background-color: rgba(255, 255, 255, 0.2);
1461
+ }
1462
+
1463
+ .log-container {
1464
+ flex: 1;
1465
+ overflow-y: auto;
1466
+ padding: 1rem;
1467
+ background-color: #1e1e1e;
1468
+ color: #d4d4d4;
1469
+ font-family: 'Courier New', monospace;
1470
+ font-size: 0.9rem;
1471
+ line-height: 1.6;
1472
+ max-height: calc(90vh - 80px);
1473
+ }
1474
+
1475
+ .log-content {
1476
+ min-height: 100%;
1477
+ }
1478
+
1479
+ .log-line {
1480
+ padding: 0.3rem 0;
1481
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
1482
+ word-wrap: break-word;
1483
+ }
1484
+
1485
+ .log-line:last-child {
1486
+ border-bottom: none;
1487
+ }
1488
+
1489
+ .log-time {
1490
+ color: #858585;
1491
+ margin-right: 0.5rem;
1492
+ }
1493
+
1494
+ .log-icon {
1495
+ margin-right: 0.5rem;
1496
+ }
1497
+
1498
+ .log-message {
1499
+ color: #d4d4d4;
1500
+ }
1501
+
1502
+ .log-info .log-message {
1503
+ color: #d4d4d4;
1504
+ }
1505
+
1506
+ .log-success .log-message {
1507
+ color: #4ec9b0;
1508
+ }
1509
+
1510
+ .log-warning .log-message {
1511
+ color: #dcdcaa;
1512
+ }
1513
+
1514
+ .log-error .log-message {
1515
+ color: #f48771;
1516
+ }
1517
+
1518
+ .log-result {
1519
+ margin-top: 1.5rem;
1520
+ padding: 1rem;
1521
+ background-color: #252526;
1522
+ border-left: 4px solid #4ec9b0;
1523
+ border-radius: 4px;
1524
+ }
1525
+
1526
+ .log-result h4 {
1527
+ color: #4ec9b0;
1528
+ margin-bottom: 0.5rem;
1529
+ }
1530
+
1531
+ .log-result p {
1532
+ color: #d4d4d4;
1533
+ margin: 0.5rem 0;
1534
+ }
1535
+
1536
+ .log-result ul {
1537
+ margin: 0.5rem 0;
1538
+ padding-left: 1.5rem;
1539
+ color: #d4d4d4;
1540
+ }
1541
+
1542
+ .log-result li {
1543
+ margin: 0.5rem 0;
1544
+ }
1545
+
1546
+ /* File Editor Modal Styles */
1547
+ #file-content-modal {
1548
+ animation: fadeIn 0.3s;
1549
+ }
1550
+
1551
+ #file-content-modal > div {
1552
+ animation: slideDown 0.3s;
1553
+ }
1554
+
1555
+ #modal-content-edit {
1556
+ line-height: 1.6;
1557
+ tab-size: 4;
1558
+ -moz-tab-size: 4;
1559
+ }
1560
+
1561
+ #modal-content-edit:focus {
1562
+ outline: none;
1563
+ border-color: #0056b3;
1564
+ box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
1565
+ }
1566
+
1567
+ #save-status {
1568
+ font-weight: 500;
1569
+ animation: slideUp 0.3s;
1570
+ }
1571
+
1572
+ @keyframes slideUp {
1573
+ from {
1574
+ opacity: 0;
1575
+ transform: translateY(10px);
1576
+ }
1577
+ to {
1578
+ opacity: 1;
1579
+ transform: translateY(0);
1580
+ }
1581
+ }
1582
+
1583
+ #edit-file-btn:hover {
1584
+ background: #0056b3 !important;
1585
+ }
1586
+
1587
+ #save-file-btn:hover {
1588
+ background: #218838 !important;
1589
+ }
1590
+
1591
+ #cancel-edit-btn:hover {
1592
+ background: #5a6268 !important;
1593
+ }
1594
+
1595
+ /* Frozen/Disabled Checkboxes - Gray only the checkbox, keep text bold and normal */
1596
+ .checkbox-container input[type="checkbox"]:disabled {
1597
+ opacity: 0.5 !important;
1598
+ cursor: not-allowed !important;
1599
+ pointer-events: none !important;
1600
+ -webkit-appearance: none !important;
1601
+ appearance: none !important;
1602
+ }
1603
+
1604
+ .checkbox-container input[type="checkbox"]:disabled + .checkmark {
1605
+ opacity: 0.5 !important;
1606
+ }
1607
+
1608
+ /* Keep text labels bold and normal color when checkbox is disabled */
1609
+ .checkbox-container:has(input[type="checkbox"]:disabled) {
1610
+ cursor: not-allowed !important;
1611
+ pointer-events: none !important;
1612
+ opacity: 1 !important;
1613
+ color: #2c3e50 !important;
1614
+ font-weight: 600 !important;
1615
+ }
1616
+
1617
+ .checkbox-container:has(input[type="checkbox"]:disabled):hover {
1618
+ color: #2c3e50 !important;
1619
+ }
1620
+
1621
+ /* Prevent interaction on disabled checkbox inputs */
1622
+ .checkbox-container input[type="checkbox"]:disabled {
1623
+ cursor: not-allowed !important;
1624
+ pointer-events: none !important;
1625
+ opacity: 0.5 !important;
1626
+ }
1627
+
1628
+ /* Gray out the checkmark for disabled checkboxes */
1629
+ .checkbox-container input[type="checkbox"]:disabled:checked {
1630
+ background-color: #6c757d !important;
1631
+ border-color: #6c757d !important;
1632
+ }
1633
+
1634
+ .checkbox-container input[type="checkbox"]:disabled:checked::after {
1635
+ color: #ffffff !important;
1636
+ }
1637
+
1638
+ /* Frozen/Disabled Toggle Switches - Gray only the toggle, keep it hidden */
1639
+ .switch input[type="checkbox"]:disabled {
1640
+ opacity: 0 !important; /* Keep the input completely hidden */
1641
+ cursor: not-allowed !important;
1642
+ pointer-events: none !important;
1643
+ width: 0 !important;
1644
+ height: 0 !important;
1645
+ }
1646
+
1647
+ .switch input[type="checkbox"]:disabled + .slider {
1648
+ opacity: 0.5 !important;
1649
+ cursor: not-allowed !important;
1650
+ pointer-events: none !important;
1651
+ background-color: #95a5a6 !important; /* Gray color for disabled toggles */
1652
+ }
1653
+
1654
+ .switch input[type="checkbox"]:disabled:checked + .slider {
1655
+ background-color: #95a5a6 !important; /* Gray color even when checked */
1656
+ }
1657
+
1658
+ .switch input[type="checkbox"]:disabled:not(:checked) + .slider {
1659
+ background-color: #bdc3c7 !important; /* Lighter gray when unchecked */
1660
+ }
1661
+
1662
+ .switch:has(input[type="checkbox"]:disabled) {
1663
+ cursor: not-allowed !important;
1664
+ pointer-events: none !important;
1665
+ opacity: 1 !important; /* Keep switch container visible */
1666
+ }
1667
+
1668
+ .switch:has(input[type="checkbox"]:disabled) .slider {
1669
+ cursor: not-allowed !important;
1670
+ pointer-events: none !important;
1671
+ }
1672
+
1673
+ /* Ensure disabled switch input stays completely hidden */
1674
+ .switch input[type="checkbox"]:disabled {
1675
+ position: absolute !important;
1676
+ opacity: 0 !important;
1677
+ width: 0 !important;
1678
+ height: 0 !important;
1679
+ margin: 0 !important;
1680
+ padding: 0 !important;
1681
+ }
1682
+
1683
+ /* Docking Section Collapsible Styles */
1684
+ #docking-section.collapsed .section-description,
1685
+ #docking-section.collapsed .custom-plumed-section {
1686
+ max-height: 0;
1687
+ opacity: 0;
1688
+ margin-top: 0;
1689
+ margin-bottom: 0;
1690
+ padding-top: 0;
1691
+ padding-bottom: 0;
1692
+ overflow: hidden;
1693
+ transition: max-height 0.4s ease, opacity 0.3s ease, margin 0.3s ease, padding 0.3s ease;
1694
+ }
1695
+
1696
+ #docking-section.collapsed .plumed-toggle-header {
1697
+ margin-bottom: 0;
1698
+ }
1699
+
1700
+ .docking-box-row {
1701
+ display: flex;
1702
+ gap: 10px;
1703
+ margin-bottom: 10px;
1704
+ }
1705
+
1706
+ .docking-box-row .form-group {
1707
+ flex: 1;
1708
+ }
1709
+
1710
+ .docking-setup-entry {
1711
+ margin-bottom: 15px;
1712
+ padding: 10px;
1713
+ background: white;
1714
+ border-radius: 5px;
1715
+ border: 1px solid #dee2e6;
1716
+ }
1717
+
1718
+ .docking-setup-header {
1719
+ display: flex;
1720
+ justify-content: space-between;
1721
+ align-items: center;
1722
+ margin-bottom: 10px;
1723
+ }
1724
+
1725
+ .docking-setup-chains {
1726
+ font-size: 0.9em;
1727
+ color: #6c757d;
1728
+ }
1729
+
1730
+ /* Docking Ligand Selection Row Styles */
1731
+ .docking-ligand-row {
1732
+ transition: all 0.2s ease;
1733
+ }
1734
+
1735
+ .docking-ligand-row:hover {
1736
+ background: #e9ecef !important;
1737
+ border-color: #6f42c1 !important;
1738
+ }
1739
+
1740
+ .docking-ligand-row .checkbox-container {
1741
+ display: flex;
1742
+ align-items: center;
1743
+ gap: 8px;
1744
+ }
1745
+
1746
+ /* Docking Box Controls Compact Layout */
1747
+ #docking-setup-list {
1748
+ display: flex;
1749
+ flex-wrap: wrap;
1750
+ gap: 15px;
1751
+ }
1752
+
1753
+ .docking-setup-entry {
1754
+ transition: all 0.2s ease;
1755
+ }
1756
+
1757
+ .docking-setup-entry:hover {
1758
+ border-color: #6f42c1 !important;
1759
+ box-shadow: 0 2px 8px rgba(111, 66, 193, 0.15);
1760
+ }
1761
+
1762
+ .docking-setup-entry input[type="number"] {
1763
+ transition: border-color 0.2s ease;
1764
+ }
1765
+
1766
+ .docking-setup-entry input[type="number"]:focus {
1767
+ border-color: #6f42c1;
1768
+ outline: none;
1769
+ box-shadow: 0 0 0 2px rgba(111, 66, 193, 0.1);
1770
+ }
1771
+
1772
+ /* Small checkmarks for chain selection */
1773
+ .docking-ligand-row .checkmark {
1774
+ width: 16px !important;
1775
+ height: 16px !important;
1776
+ }
1777
+
1778
+ .docking-ligand-row .checkmark:after {
1779
+ left: 5px !important;
1780
+ top: 2px !important;
1781
+ width: 4px !important;
1782
+ height: 8px !important;
1783
+ }
1784
+
1785
+
1786
+ /* Docking Setup Collapsible Section */
1787
+ .docking-setup-collapsible {
1788
+ transition: all 0.3s ease;
1789
+ }
1790
+
1791
+ .docking-setup-header {
1792
+ user-select: none;
1793
+ transition: background 0.2s ease;
1794
+ }
1795
+
1796
+ .docking-setup-header:hover {
1797
+ background: linear-gradient(135deg, #5a31a8 0%, #7d4bc7 100%) !important;
1798
+ }
1799
+
1800
+ .docking-setup-content {
1801
+ transition: all 0.3s ease;
1802
+ }
1803
+
1804
+ #docking-setup-toggle-icon {
1805
+ transition: transform 0.3s ease;
1806
+ }
1807
+
1808
+ /* Docking Poses Viewer Styles */
1809
+ .docking-ligand-tabs {
1810
+ display: flex;
1811
+ gap: 8px;
1812
+ margin-bottom: 15px;
1813
+ flex-wrap: wrap;
1814
+ }
1815
+
1816
+ .docking-ligand-tab {
1817
+ padding: 8px 16px;
1818
+ border: 2px solid #dee2e6;
1819
+ border-radius: 6px;
1820
+ background: #f8f9fa;
1821
+ cursor: pointer;
1822
+ font-weight: 500;
1823
+ transition: all 0.2s ease;
1824
+ display: flex;
1825
+ align-items: center;
1826
+ gap: 8px;
1827
+ }
1828
+
1829
+ .docking-ligand-tab:hover {
1830
+ border-color: #6f42c1;
1831
+ background: #f3e8ff;
1832
+ }
1833
+
1834
+ .docking-ligand-tab.active {
1835
+ border-color: #6f42c1;
1836
+ background: linear-gradient(135deg, #6f42c1 0%, #9b59b6 100%);
1837
+ color: white;
1838
+ }
1839
+
1840
+ .docking-ligand-tab .ligand-color-dot {
1841
+ width: 12px;
1842
+ height: 12px;
1843
+ border-radius: 50%;
1844
+ display: inline-block;
1845
+ }
1846
+
1847
+ .docking-poses-viewer-wrapper {
1848
+ position: relative;
1849
+ margin-bottom: 15px;
1850
+ max-width: 700px;
1851
+ margin-left: auto;
1852
+ margin-right: auto;
1853
+ }
1854
+
1855
+ .docking-poses-viewer {
1856
+ width: 100%;
1857
+ max-width: 700px;
1858
+ height: 500px;
1859
+ background: #fff;
1860
+ border-radius: 5px;
1861
+ border: 2px solid #6f42c1;
1862
+ overflow: hidden;
1863
+ margin: 0 auto;
1864
+ }
1865
+
1866
+ .pose-nav-controls {
1867
+ position: absolute;
1868
+ bottom: 0;
1869
+ left: 0;
1870
+ right: 0;
1871
+ display: flex;
1872
+ justify-content: center;
1873
+ align-items: center;
1874
+ gap: 20px;
1875
+ padding: 15px;
1876
+ background: linear-gradient(to top, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0) 100%);
1877
+ border-radius: 0 0 5px 5px;
1878
+ }
1879
+
1880
+ .pose-nav-btn {
1881
+ width: 50px;
1882
+ height: 50px;
1883
+ border-radius: 50%;
1884
+ border: 2px solid #6f42c1;
1885
+ background: white;
1886
+ color: #6f42c1;
1887
+ cursor: pointer;
1888
+ transition: all 0.2s ease;
1889
+ display: flex;
1890
+ align-items: center;
1891
+ justify-content: center;
1892
+ font-size: 1.2rem;
1893
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
1894
+ }
1895
+
1896
+ .pose-nav-btn:hover:not(:disabled) {
1897
+ background: #6f42c1;
1898
+ border-color: #6f42c1;
1899
+ color: white;
1900
+ transform: scale(1.1);
1901
+ }
1902
+
1903
+ .pose-nav-btn:disabled {
1904
+ opacity: 0.3;
1905
+ cursor: not-allowed;
1906
+ }
1907
+
1908
+ .pose-info-display {
1909
+ text-align: center;
1910
+ min-width: 200px;
1911
+ }
1912
+
1913
+ .pose-mode-label {
1914
+ font-size: 1.1rem;
1915
+ font-weight: 600;
1916
+ color: #333;
1917
+ }
1918
+
1919
+ .pose-energy-label {
1920
+ font-size: 0.95rem;
1921
+ color: #28a745;
1922
+ font-weight: 500;
1923
+ }
1924
+
1925
+ .pose-color-legend {
1926
+ display: flex;
1927
+ gap: 20px;
1928
+ justify-content: center;
1929
+ margin-top: 8px;
1930
+ font-size: 0.85rem;
1931
+ }
1932
+
1933
+ .pose-color-legend span {
1934
+ display: flex;
1935
+ align-items: center;
1936
+ gap: 6px;
1937
+ color: #555;
1938
+ }
1939
+
1940
+ .legend-dot {
1941
+ width: 12px;
1942
+ height: 12px;
1943
+ border-radius: 50%;
1944
+ display: inline-block;
1945
+ }
1946
+
1947
+ .legend-dot.original {
1948
+ background: #00ff00;
1949
+ }
1950
+
1951
+ .legend-dot.docked {
1952
+ background: #ff6b6b;
1953
+ }
1954
+
1955
+ .docking-poses-selection {
1956
+ background: #f8f9fa;
1957
+ border-radius: 8px;
1958
+ padding: 15px;
1959
+ border: 1px solid #dee2e6;
1960
+ }
1961
+
1962
+ .docking-poses-selection h5 {
1963
+ margin-bottom: 10px;
1964
+ color: #495057;
1965
+ font-size: 0.95rem;
1966
+ }
1967
+
1968
+ .pose-selection-row {
1969
+ display: flex;
1970
+ align-items: center;
1971
+ gap: 15px;
1972
+ padding: 10px;
1973
+ background: white;
1974
+ border-radius: 6px;
1975
+ margin-bottom: 8px;
1976
+ border: 1px solid #e9ecef;
1977
+ }
1978
+
1979
+ .pose-selection-row:last-child {
1980
+ margin-bottom: 0;
1981
+ }
1982
+
1983
+ .pose-selection-label {
1984
+ font-weight: 500;
1985
+ min-width: 100px;
1986
+ }
1987
+
1988
+ .pose-selection-options {
1989
+ display: flex;
1990
+ gap: 15px;
1991
+ flex-wrap: wrap;
1992
+ }
1993
+
1994
+ .pose-selection-option {
1995
+ display: flex;
1996
+ align-items: center;
1997
+ gap: 6px;
1998
+ cursor: pointer;
1999
+ padding: 4px 8px;
2000
+ border-radius: 4px;
2001
+ transition: background 0.2s ease;
2002
+ }
2003
+
2004
+ .pose-selection-option:hover {
2005
+ background: #e9ecef;
2006
+ }
2007
+
2008
+ .pose-selection-option input[type="radio"] {
2009
+ accent-color: #6f42c1;
2010
+ }
2011
+
2012
+ .pose-selection-option.selected {
2013
+ background: #f3e8ff;
2014
+ border: 1px solid #6f42c1;
2015
+ }
2016
+
2017
+ .pose-selection-energy {
2018
+ font-size: 0.85rem;
2019
+ color: #28a745;
2020
+ font-weight: 500;
2021
+ }
amberflow/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
amberflow/docking_utils.py ADDED
@@ -0,0 +1,639 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Docking Utilities for AmberFlow
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')}")
{html → amberflow/html}/index.html RENAMED
@@ -5,7 +5,10 @@
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 href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
 
 
9
  <!-- NGL 3D Molecular Viewer -->
10
  <script src="https://unpkg.com/ngl@2.0.0-dev.35/dist/ngl.js"></script>
11
  </head>
@@ -24,6 +27,9 @@
24
  <button class="tab-button active" data-tab="protein-loading">
25
  <i class="fas fa-upload"></i> Protein Loading
26
  </button>
 
 
 
27
  <button class="tab-button" data-tab="structure-prep">
28
  <i class="fas fa-tools"></i> Structure Preparation
29
  </button>
@@ -36,6 +42,9 @@
36
  <button class="tab-button" data-tab="file-generation">
37
  <i class="fas fa-file-download"></i> Generate Files
38
  </button>
 
 
 
39
  </nav>
40
 
41
  <!-- Main Content -->
@@ -86,7 +95,7 @@
86
  <div class="preview-content">
87
  <div class="protein-info">
88
  <p><strong>Structure ID:</strong> <span id="structure-id"></span></p>
89
- <p><strong>Number of atoms:</strong> <span id="atom-count"></span></p>
90
  <p><strong>Chains:</strong> <span id="chain-info"></span></p>
91
  <p><strong>Residues:</strong> <span id="residue-count"></span></p>
92
  <p><strong>Water molecules:</strong> <span id="water-count"></span></p>
@@ -116,6 +125,208 @@
116
  </div>
117
  </div>
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  <!-- Structure Preparation Tab -->
120
  <div id="structure-prep" class="tab-content">
121
  <div class="card">
@@ -128,7 +339,7 @@
128
  <div class="prep-options">
129
  <div class="prep-option">
130
  <label class="checkbox-container">
131
- <input type="checkbox" id="remove-water" checked>
132
  <span class="checkmark"></span>
133
  Remove water molecules
134
  </label>
@@ -137,7 +348,7 @@
137
 
138
  <div class="prep-option">
139
  <label class="checkbox-container">
140
- <input type="checkbox" id="remove-ions" checked>
141
  <span class="checkmark"></span>
142
  Remove ions
143
  </label>
@@ -146,7 +357,7 @@
146
 
147
  <div class="prep-option">
148
  <label class="checkbox-container">
149
- <input type="checkbox" id="remove-hydrogens" checked>
150
  <span class="checkmark"></span>
151
  Remove hydrogen atoms
152
  </label>
@@ -247,8 +458,8 @@
247
  <h3><i class="fas fa-eye"></i> Prepared Structure Preview</h3>
248
  <div class="preview-content">
249
  <div class="structure-info">
250
- <p><strong>Original atoms:</strong> <span id="original-atoms"></span></p>
251
- <p><strong>Prepared atoms:</strong> <span id="prepared-atoms"></span></p>
252
  <p><strong>Removed components:</strong> <span id="removed-components"></span></p>
253
  <p><strong>Added capping groups:</strong> <span id="added-capping"></span></p>
254
  <p><strong>Ligands preserved:</strong> <span id="preserved-ligands"></span></p>
@@ -271,6 +482,114 @@
271
  </div>
272
  </div>
273
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  </div>
275
  </div>
276
 
@@ -400,7 +719,7 @@
400
  <div class="step-header">
401
  <h3><i class="fas fa-lock"></i> Restrained Minimization</h3>
402
  <label class="switch">
403
- <input type="checkbox" id="enable-restrained-min" checked>
404
  <span class="slider"></span>
405
  </label>
406
  </div>
@@ -422,7 +741,7 @@
422
  <div class="step-header">
423
  <h3><i class="fas fa-compress"></i> Minimization</h3>
424
  <label class="switch">
425
- <input type="checkbox" id="enable-minimization" checked>
426
  <span class="slider"></span>
427
  </label>
428
  </div>
@@ -446,7 +765,7 @@
446
  <div class="step-header">
447
  <h3><i class="fas fa-fire"></i> NPT Heating</h3>
448
  <label class="switch">
449
- <input type="checkbox" id="enable-nvt" checked>
450
  <span class="slider"></span>
451
  </label>
452
  </div>
@@ -468,7 +787,7 @@
468
  <div class="step-header">
469
  <h3><i class="fas fa-compress-arrows-alt"></i> NPT Equilibration</h3>
470
  <label class="switch">
471
- <input type="checkbox" id="enable-npt" checked>
472
  <span class="slider"></span>
473
  </label>
474
  </div>
@@ -494,7 +813,7 @@
494
  <div class="step-header">
495
  <h3><i class="fas fa-play"></i> Production Run(NPT)</h3>
496
  <label class="switch">
497
- <input type="checkbox" id="enable-production" checked>
498
  <span class="slider"></span>
499
  </label>
500
  </div>
@@ -521,6 +840,14 @@
521
 
522
  <!-- File Generation Tab -->
523
  <div id="file-generation" class="tab-content">
 
 
 
 
 
 
 
 
524
  <div class="card">
525
  <h2><i class="fas fa-file-download"></i> Generate Simulation Files</h2>
526
 
@@ -534,10 +861,18 @@
534
  <button class="btn btn-info" id="preview-solvated">
535
  <i class="fas fa-tint"></i> Preview Solvated Protein
536
  </button>
 
 
 
537
  </div>
538
 
539
  <div class="files-preview" id="files-preview" style="display: none;">
540
- <h3><i class="fas fa-files"></i> Generated Files</h3>
 
 
 
 
 
541
  <div class="files-list" id="files-list">
542
  <!-- Generated files will be listed here -->
543
  </div>
@@ -561,6 +896,206 @@
561
  </div>
562
  </div>
563
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  </main>
565
 
566
  <!-- Step Navigation Controls -->
@@ -569,7 +1104,7 @@
569
  <i class="fas fa-chevron-left"></i> Previous
570
  </button>
571
  <div class="step-indicator">
572
- <span id="current-step">1</span> of <span id="total-steps">5</span>
573
  </div>
574
  <button class="nav-btn next-btn" id="next-tab">
575
  Next <i class="fas fa-chevron-right"></i>
@@ -582,6 +1117,29 @@
582
  </footer>
583
  </div>
584
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
  <script src="../js/script.js"></script>
 
 
586
  </body>
587
  </html>
 
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>
 
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>
 
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 -->
 
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>
 
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 id="superimposed-viewer-controls" class="viewer-controls" style="display: none; gap: 10px;">
309
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.resetSuperimposedView()">
310
+ <i class="fas fa-home"></i> Reset
311
+ </button>
312
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleSuperimposedRepresentation()">
313
+ <i class="fas fa-eye"></i> <span id="superimposed-style-text">Cartoon</span>
314
+ </button>
315
+ <button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleSuperimposedSpin()">
316
+ <i class="fas fa-sync"></i> Spin
317
+ </button>
318
+ </div>
319
+ </div>
320
+ <div id="superimposed-molecule-viewer" class="molecule-viewer" style="border: 2px solid #007bff; border-radius: 5px; width: 100%; height: 600px; position: relative;">
321
+ <div id="superimposed-ngl-viewer" style="width: 100%; height: 100%; position: absolute; top: 0; left: 0;"></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">
 
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>
 
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>
 
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>
 
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>
 
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
 
 
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>
 
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>
 
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>
 
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>
 
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>
 
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
 
 
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>
 
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 -->
 
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>
 
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>
amberflow/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
+
amberflow/js/plumed.js ADDED
The diff for this file is too large to render. See raw diff
 
amberflow/js/plumed_cv_docs.js ADDED
The diff for this file is too large to render. See raw diff
 
amberflow/js/script.js ADDED
The diff for this file is too large to render. See raw diff
 
{python → amberflow}/structure_preparation.py RENAMED
@@ -4,10 +4,16 @@ AMBER Structure Preparation Script using MDAnalysis
4
  Complete pipeline: extract protein, add caps, handle ligands
5
  """
6
 
 
7
  import os
 
8
  import subprocess
9
  import sys
10
  import shutil
 
 
 
 
11
 
12
  def run_command(cmd, description=""):
13
  """Run a command and return success status"""
@@ -44,7 +50,8 @@ def extract_protein_only(pdb_content, output_file, selected_chains=None):
44
  chain_filters = ' or '.join([f'chain {c}' for c in selected_chains])
45
  chain_sel = f' and ({chain_filters})'
46
  selection = f"protein{chain_sel} and not name H* 1H* 2H* 3H*"
47
- cmd = f'python -c "import MDAnalysis as mda; u=mda.Universe(\'{output_file}\'); u.select_atoms(\'{selection}\').write(\'{output_file}\')"'
 
48
  result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
49
 
50
  if result.returncode != 0:
@@ -57,9 +64,10 @@ def extract_protein_only(pdb_content, output_file, selected_chains=None):
57
 
58
  def add_capping_groups(input_file, output_file):
59
  """Add ACE and NME capping groups using add_caps.py"""
 
60
  # First add caps
61
  temp_capped = output_file.replace('.pdb', '_temp.pdb')
62
- cmd = f"python add_caps.py -i {input_file} -o {temp_capped}"
63
  if not run_command(cmd, f"Adding capping groups to {input_file}"):
64
  return False
65
 
@@ -74,6 +82,52 @@ def add_capping_groups(input_file, output_file):
74
 
75
  return True
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  def extract_selected_chains(pdb_content, output_file, selected_chains):
78
  """Extract selected chains using PyMOL commands"""
79
  try:
@@ -111,20 +165,29 @@ pymol.cmd.quit()
111
  return False
112
 
113
  def extract_selected_ligands(pdb_content, output_file, selected_ligands):
114
- """Extract selected ligands using PyMOL commands"""
 
 
 
 
115
  try:
116
  # Write input content to temp file
117
  temp_input = output_file.replace('.pdb', '_temp_input.pdb')
118
  with open(temp_input, 'w') as f:
119
  f.write(pdb_content)
120
 
121
- # Build ligand selection string
122
  parts = []
123
  for lig in selected_ligands:
124
  resn = lig.get('resn', '').strip()
125
  chain = lig.get('chain', '').strip()
 
 
126
  if resn and chain:
127
- parts.append(f"(resn {resn} and chain {chain})")
 
 
 
128
  elif resn:
129
  parts.append(f"resn {resn}")
130
 
@@ -169,13 +232,22 @@ def extract_ligands(pdb_content, output_file, ligand_residue_name=None, selected
169
  try:
170
  # Run MDAnalysis command with the output file as input
171
  if selected_ligands:
172
- # Build selection from provided ligand list (RESN-CHAIN groups)
 
173
  parts = []
174
  for lig in selected_ligands:
175
  resn = lig.get('resn', '').strip()
176
  chain = lig.get('chain', '').strip()
 
 
177
  if resn and chain:
178
- parts.append(f"(resname {resn} and segid {chain})")
 
 
 
 
 
 
179
  elif resn:
180
  parts.append(f"resname {resn}")
181
  if parts:
@@ -255,46 +327,305 @@ def convert_atom_to_hetatm(pdb_file):
255
  print(f"Error converting ATOM to HETATM: {e}")
256
  return False
257
 
258
- def correct_ligand_with_pymol(ligand_file, corrected_file):
259
- """Correct ligand using PyMOL"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  ligand_path = os.path.abspath(ligand_file)
261
  corrected_path = os.path.abspath(corrected_file)
262
  if not os.path.isfile(ligand_path) or os.path.getsize(ligand_path) == 0:
263
  print("Ligand file missing or empty:", ligand_path)
264
  return False
265
 
266
- # Use PyMOL to add hydrogens and save corrected ligand
267
- cmd = f'pymol -cq {ligand_path} -d "h_add; save {corrected_path}; quit"'
268
- return run_command(cmd, f"Correcting ligand with PyMOL")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
  def remove_connect_records(pdb_file):
271
- """Remove CONNECT records from PDB file"""
272
  try:
273
  with open(pdb_file, 'r') as f:
274
  lines = f.readlines()
275
 
276
- # Filter out CONNECT records
277
- filtered_lines = [line for line in lines if not line.startswith('CONECT')]
278
 
279
  with open(pdb_file, 'w') as f:
280
  f.writelines(filtered_lines)
281
 
282
- print(f"Removed CONNECT records from {pdb_file}")
283
  return True
284
  except Exception as e:
285
- print(f"Error removing CONNECT records: {e}")
286
  return False
287
 
288
- def merge_protein_and_ligand(protein_file, ligand_file, output_file):
289
- """Merge capped protein and corrected ligand with proper PDB formatting"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  try:
291
  # Read protein file
292
  with open(protein_file, 'r') as f:
293
  protein_lines = f.readlines()
294
 
295
- # Read ligand file
296
- with open(ligand_file, 'r') as f:
297
- ligand_lines = f.readlines()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
 
299
  # Process protein file: remove 'END' and add properly formatted 'TER'
300
  protein_processed = []
@@ -317,14 +648,29 @@ def merge_protein_and_ligand(protein_file, ligand_file, output_file):
317
  if line.startswith('ATOM'):
318
  last_atom_line = line
319
 
320
- # Process ligand file: remove header info (CRYST, REMARK, etc.) and keep only ATOM/HETATM
321
- ligand_processed = []
322
- for line in ligand_lines:
323
- if line.startswith(('ATOM', 'HETATM')):
324
- ligand_processed.append(line)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
- # Combine: protein + TER + ligand + END (no extra newline between TER and ligand)
327
- merged_content = ''.join(protein_processed) + ''.join(ligand_processed) + 'END\n'
328
 
329
  with open(output_file, 'w') as f:
330
  f.write(merged_content)
@@ -332,6 +678,8 @@ def merge_protein_and_ligand(protein_file, ligand_file, output_file):
332
  return True
333
  except Exception as e:
334
  print(f"Error merging files: {str(e)}")
 
 
335
  return False
336
 
337
  def prepare_structure(pdb_content, options, output_dir="output"):
@@ -341,7 +689,19 @@ def prepare_structure(pdb_content, options, output_dir="output"):
341
  os.makedirs(output_dir, exist_ok=True)
342
 
343
  # Define all file paths in output directory
344
- input_file = os.path.join(output_dir, "0_original_input.pdb")
 
 
 
 
 
 
 
 
 
 
 
 
345
  user_chain_file = os.path.join(output_dir, "0_user_chain_selected.pdb")
346
  protein_file = os.path.join(output_dir, "1_protein_no_hydrogens.pdb")
347
  protein_capped_file = os.path.join(output_dir, "2_protein_with_caps.pdb")
@@ -349,10 +709,22 @@ def prepare_structure(pdb_content, options, output_dir="output"):
349
  ligand_corrected_file = os.path.join(output_dir, "4_ligands_corrected.pdb")
350
  tleap_ready_file = os.path.join(output_dir, "tleap_ready.pdb")
351
 
352
- # Step 0: Save original input for reference
353
- print("Step 0: Saving original input...")
354
- with open(input_file, 'w') as f:
355
- f.write(pdb_content)
 
 
 
 
 
 
 
 
 
 
 
 
356
 
357
  # Step 0.5: Extract user-selected chains and ligands
358
  selected_chains = options.get('selected_chains', [])
@@ -367,7 +739,12 @@ def prepare_structure(pdb_content, options, output_dir="output"):
367
  shutil.copy2(input_file, user_chain_file)
368
 
369
  if selected_ligands:
370
- ligand_names = [f"{l.get('resn', '')}-{l.get('chain', '')}" for l in selected_ligands]
 
 
 
 
 
371
  print(f"Step 0.5b: Extracting selected ligands: {ligand_names}")
372
  if not extract_selected_ligands(pdb_content, ligand_file, selected_ligands):
373
  raise Exception("Failed to extract selected ligands")
@@ -385,6 +762,20 @@ def prepare_structure(pdb_content, options, output_dir="output"):
385
  if not extract_protein_only(chain_content, protein_file):
386
  raise Exception("Failed to extract protein")
387
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
  # Step 2: Add capping groups (only if add_ace or add_nme is True)
389
  add_ace = options.get('add_ace', True)
390
  add_nme = options.get('add_nme', True)
@@ -402,6 +793,23 @@ def prepare_structure(pdb_content, options, output_dir="output"):
402
  # Step 3: Handle ligands (use pre-extracted ligand file)
403
  preserve_ligands = options.get('preserve_ligands', True)
404
  ligand_present = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
 
406
  if preserve_ligands:
407
  print("Step 3: Processing pre-extracted ligands...")
@@ -414,23 +822,72 @@ def prepare_structure(pdb_content, options, output_dir="output"):
414
  ligand_present = True
415
  print("Found pre-extracted ligands")
416
 
417
- # Correct ligand with PyMOL
418
- if not correct_ligand_with_pymol(ligand_file, ligand_corrected_file):
419
- print("Error: Failed to process ligand")
420
- return {
421
- 'error': 'Failed to process ligand with PyMOL',
422
- 'prepared_structure': '',
423
- 'original_atoms': 0,
424
- 'prepared_atoms': 0,
425
- 'removed_components': {},
426
- 'added_capping': {},
427
- 'preserved_ligands': 0,
428
- 'ligand_present': False
429
- }
430
 
431
- # Merge protein and ligand
432
- if not merge_protein_and_ligand(protein_capped_file, ligand_corrected_file, tleap_ready_file):
433
- raise Exception("Failed to merge protein and ligand")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  else:
435
  print("No ligands found in pre-extracted file, using protein only")
436
  # Copy protein file to tleap_ready
@@ -441,11 +898,25 @@ def prepare_structure(pdb_content, options, output_dir="output"):
441
  # Copy protein file to tleap_ready (protein only, no ligands)
442
  shutil.copy2(protein_capped_file, tleap_ready_file)
443
 
 
 
 
 
 
 
 
 
 
 
444
  # Remove CONNECT records from tleap_ready.pdb (PyMOL adds them)
445
  print("Removing CONNECT records from tleap_ready.pdb...")
446
- remove_connect_records(tleap_ready_file)
 
447
 
448
  # Read the final prepared structure
 
 
 
449
  with open(tleap_ready_file, 'r') as f:
450
  prepared_content = f.read()
451
 
@@ -499,12 +970,32 @@ def prepare_structure(pdb_content, options, output_dir="output"):
499
  'nme_groups': 0
500
  }
501
 
502
- # Count preserved ligands from the pre-extracted file
503
- preserved_ligands = 0
504
- if ligand_present and preserve_ligands:
505
- with open(ligand_file, 'r') as f:
506
- ligand_lines = [line for line in f if line.startswith('HETATM')]
507
- preserved_ligands = len(set(line[17:20].strip() for line in ligand_lines))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
 
509
  result = {
510
  'prepared_structure': prepared_content,
 
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"""
 
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:
 
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
 
 
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:
 
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
 
 
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:
 
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 = []
 
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)
 
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"):
 
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")
 
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', [])
 
739
  shutil.copy2(input_file, user_chain_file)
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")
 
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)
 
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...")
 
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
+ # Merge protein and all ligands (with TER records between ligands)
883
+ if not merge_protein_and_ligand(protein_capped_file, None, tleap_ready_file, ligand_groups=all_ligand_groups):
884
+ raise Exception("Failed to merge protein and ligands")
885
+ elif selected_ligands and ligand_count > 0:
886
+ # If ligands were selected but file is empty, still mark as present if we have a count
887
+ ligand_present = True
888
+ print(f"Ligands were selected ({ligand_count} unique), but ligand file appears empty")
889
+ # Use protein only since no ligand content found
890
+ shutil.copy2(protein_capped_file, tleap_ready_file)
891
  else:
892
  print("No ligands found in pre-extracted file, using protein only")
893
  # Copy protein file to tleap_ready
 
898
  # Copy protein file to tleap_ready (protein only, no ligands)
899
  shutil.copy2(protein_capped_file, tleap_ready_file)
900
 
901
+ # Ensure tleap_ready.pdb exists before proceeding
902
+ if not os.path.exists(tleap_ready_file):
903
+ print(f"Error: tleap_ready.pdb was not created. Checking what went wrong...")
904
+ # Try to create it from protein_capped_file as fallback
905
+ if os.path.exists(protein_capped_file):
906
+ print("Creating tleap_ready.pdb from protein_capped_file as fallback...")
907
+ shutil.copy2(protein_capped_file, tleap_ready_file)
908
+ else:
909
+ raise Exception(f"tleap_ready.pdb was not created and protein_capped_file also doesn't exist")
910
+
911
  # Remove CONNECT records from tleap_ready.pdb (PyMOL adds them)
912
  print("Removing CONNECT records from tleap_ready.pdb...")
913
+ if not remove_connect_records(tleap_ready_file):
914
+ print("Warning: Failed to remove CONNECT records, but continuing...")
915
 
916
  # Read the final prepared structure
917
+ if not os.path.exists(tleap_ready_file):
918
+ raise Exception("tleap_ready.pdb does not exist after processing")
919
+
920
  with open(tleap_ready_file, 'r') as f:
921
  prepared_content = f.read()
922
 
 
970
  'nme_groups': 0
971
  }
972
 
973
+ # Count preserved ligands
974
+ # Priority: 1) selected_ligands count, 2) processed ligand_count, 3) 0
975
+ if preserve_ligands:
976
+ if selected_ligand_count > 0:
977
+ # Use count from selected_ligands (most reliable)
978
+ preserved_ligands = selected_ligand_count
979
+ print(f"Using selected ligand count: {preserved_ligands}")
980
+ elif ligand_present and ligand_count > 0:
981
+ # Use count from processing
982
+ preserved_ligands = ligand_count
983
+ print(f"Using processed ligand count: {preserved_ligands}")
984
+ elif ligand_present:
985
+ # Ligands were present but count is 0, try to count from tleap_ready
986
+ # Count unique ligand residue names in tleap_ready.pdb
987
+ ligand_resnames = set()
988
+ for line in prepared_content.split('\n'):
989
+ if line.startswith('HETATM'):
990
+ resname = line[17:20].strip()
991
+ if resname and resname not in ['HOH', 'WAT', 'TIP', 'SPC', 'NA', 'CL', 'ACE', 'NME']:
992
+ ligand_resnames.add(resname)
993
+ preserved_ligands = len(ligand_resnames)
994
+ print(f"Counted {preserved_ligands} unique ligand residue name(s) from tleap_ready.pdb")
995
+ else:
996
+ preserved_ligands = 0
997
+ else:
998
+ preserved_ligands = 0
999
 
1000
  result = {
1001
  'prepared_structure': prepared_content,
js/script.js DELETED
@@ -1,2004 +0,0 @@
1
- // MD Simulation Pipeline JavaScript
2
- console.log('Script loading...'); // Debug log
3
-
4
- class MDSimulationPipeline {
5
- constructor() {
6
- this.currentProtein = null;
7
- this.preparedProtein = null;
8
- this.simulationParams = {};
9
- this.generatedFiles = {};
10
- this.nglStage = null;
11
- this.preparedNglStage = null;
12
- this.currentRepresentation = 'cartoon';
13
- this.preparedRepresentation = 'cartoon';
14
- this.isSpinning = false;
15
- this.preparedIsSpinning = false;
16
- this.currentTabIndex = 0;
17
- this.tabOrder = ['protein-loading', 'structure-prep', 'simulation-params', 'simulation-steps', 'file-generation'];
18
- this.init();
19
- this.initializeTooltips();
20
- }
21
-
22
- init() {
23
- this.setupEventListeners();
24
- this.initializeTabs();
25
- this.initializeStepToggles();
26
- this.loadDefaultParams();
27
- this.updateNavigationState();
28
- }
29
-
30
- initializeTooltips() {
31
- // Initialize Bootstrap tooltips using vanilla JavaScript
32
- // Note: This requires Bootstrap to be loaded
33
- if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
34
- const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-toggle="tooltip"]'));
35
- tooltipTriggerList.map(function (tooltipTriggerEl) {
36
- return new bootstrap.Tooltip(tooltipTriggerEl);
37
- });
38
- } else {
39
- console.log('Bootstrap not loaded, tooltips will not work');
40
- }
41
- }
42
-
43
- setupEventListeners() {
44
- // Tab navigation
45
- document.querySelectorAll('.tab-button').forEach(button => {
46
- button.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab));
47
- });
48
-
49
- // File upload
50
- const fileInput = document.getElementById('pdb-file');
51
- const fileUploadArea = document.getElementById('file-upload-area');
52
- const chooseFileBtn = document.getElementById('choose-file-btn');
53
-
54
- console.log('File input element:', fileInput);
55
- console.log('File upload area:', fileUploadArea);
56
- console.log('Choose file button:', chooseFileBtn);
57
-
58
- if (!fileInput) {
59
- console.error('File input element not found!');
60
- return;
61
- }
62
-
63
- fileInput.addEventListener('change', (e) => this.handleFileUpload(e));
64
-
65
- // Handle click on upload area (but not on the button)
66
- fileUploadArea.addEventListener('click', (e) => {
67
- // Only trigger if not clicking on the button
68
- if (e.target !== chooseFileBtn && !chooseFileBtn.contains(e.target)) {
69
- console.log('Upload area clicked, triggering file input');
70
- fileInput.click();
71
- }
72
- });
73
-
74
- // Handle click on choose file button
75
- chooseFileBtn.addEventListener('click', (e) => {
76
- e.stopPropagation(); // Prevent triggering the upload area click
77
- console.log('Choose file button clicked, triggering file input');
78
- fileInput.click();
79
- });
80
-
81
- fileUploadArea.addEventListener('dragover', (e) => this.handleDragOver(e));
82
- fileUploadArea.addEventListener('drop', (e) => this.handleDrop(e));
83
-
84
- // PDB fetch
85
- document.getElementById('fetch-pdb').addEventListener('click', () => this.fetchPDB());
86
-
87
- // File generation
88
- document.getElementById('generate-files').addEventListener('click', () => this.generateAllFiles());
89
- document.getElementById('preview-files').addEventListener('click', () => this.previewFiles());
90
- document.getElementById('preview-solvated').addEventListener('click', () => this.previewSolvatedProtein());
91
- document.getElementById('download-zip').addEventListener('click', () => this.downloadZip());
92
-
93
-
94
- // Structure preparation
95
- document.getElementById('prepare-structure').addEventListener('click', () => this.prepareStructure());
96
- document.getElementById('preview-prepared').addEventListener('click', () => this.previewPreparedStructure());
97
- document.getElementById('download-prepared').addEventListener('click', () => this.downloadPreparedStructure());
98
-
99
- // Ligand download button
100
- const downloadLigandBtn = document.getElementById('download-ligand');
101
- if (downloadLigandBtn) {
102
- downloadLigandBtn.addEventListener('click', (e) => {
103
- e.preventDefault();
104
- this.downloadLigandFile();
105
- });
106
- }
107
-
108
- // Navigation buttons
109
- document.getElementById('prev-tab').addEventListener('click', () => this.previousTab());
110
- document.getElementById('next-tab').addEventListener('click', () => this.nextTab());
111
-
112
- // Parameter changes
113
- document.querySelectorAll('input, select').forEach(input => {
114
- input.addEventListener('change', () => this.updateSimulationParams());
115
- });
116
-
117
- // Render chain and ligand choices when structure tab becomes visible
118
- document.querySelector('[data-tab="structure-prep"]').addEventListener('click', () => {
119
- this.renderChainAndLigandSelections();
120
- });
121
-
122
- // Separate ligands checkbox change
123
- document.getElementById('separate-ligands').addEventListener('change', (e) => {
124
- const downloadBtn = document.getElementById('download-ligand');
125
-
126
- if (e.target.checked && this.preparedProtein && this.preparedProtein.ligand_present && this.preparedProtein.ligand_content) {
127
- downloadBtn.disabled = false;
128
- downloadBtn.classList.remove('btn-outline-secondary');
129
- downloadBtn.classList.add('btn-outline-primary');
130
- } else {
131
- downloadBtn.disabled = true;
132
- downloadBtn.classList.remove('btn-outline-primary');
133
- downloadBtn.classList.add('btn-outline-secondary');
134
- }
135
- });
136
-
137
- // Preserve ligands checkbox change
138
- document.getElementById('preserve-ligands').addEventListener('change', (e) => {
139
- this.toggleLigandForceFieldGroup(e.target.checked);
140
- });
141
- }
142
-
143
- initializeTabs() {
144
- const tabs = document.querySelectorAll('.tab-content');
145
- tabs.forEach(tab => {
146
- if (!tab.classList.contains('active')) {
147
- tab.style.display = 'none';
148
- }
149
- });
150
- }
151
-
152
- initializeStepToggles() {
153
- document.querySelectorAll('.step-header').forEach(header => {
154
- header.addEventListener('click', () => {
155
- const stepItem = header.parentElement;
156
- const content = stepItem.querySelector('.step-content');
157
- const isActive = content.classList.contains('active');
158
-
159
- // Close all other step contents
160
- document.querySelectorAll('.step-content').forEach(c => c.classList.remove('active'));
161
-
162
- // Toggle current step
163
- if (!isActive) {
164
- content.classList.add('active');
165
- }
166
- });
167
- });
168
- }
169
-
170
- loadDefaultParams() {
171
- this.simulationParams = {
172
- boxType: 'cubic',
173
- boxSize: 1.0,
174
- boxMargin: 1.0,
175
- forceField: 'amber99sb-ildn',
176
- waterModel: 'tip3p',
177
- ionConcentration: 150,
178
- temperature: 300,
179
- pressure: 1.0,
180
- couplingType: 'berendsen',
181
- timestep: 0.002,
182
- cutoff: 1.0,
183
- pmeOrder: 4,
184
- steps: {
185
- restrainedMin: { enabled: true, steps: 1000, force: 1000 },
186
- minimization: { enabled: true, steps: 5000, algorithm: 'steep' },
187
- nvt: { enabled: true, steps: 50000, temperature: 300 },
188
- npt: { enabled: true, steps: 100000, temperature: 300, pressure: 1.0 },
189
- production: { enabled: true, steps: 1000000, temperature: 300, pressure: 1.0 }
190
- }
191
- };
192
- }
193
-
194
- switchTab(tabName) {
195
- // Hide all tab contents
196
- document.querySelectorAll('.tab-content').forEach(tab => {
197
- tab.classList.remove('active');
198
- tab.style.display = 'none';
199
- });
200
-
201
- // Remove active class from all tab buttons
202
- document.querySelectorAll('.tab-button').forEach(button => {
203
- button.classList.remove('active');
204
- });
205
-
206
- // Show selected tab
207
- document.getElementById(tabName).classList.add('active');
208
- document.getElementById(tabName).style.display = 'block';
209
-
210
- // Add active class to clicked button
211
- document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
212
-
213
- // Update current tab index and navigation state
214
- this.currentTabIndex = this.tabOrder.indexOf(tabName);
215
- this.updateNavigationState();
216
- }
217
-
218
- previousTab() {
219
- if (this.currentTabIndex > 0) {
220
- const prevTab = this.tabOrder[this.currentTabIndex - 1];
221
- this.switchTab(prevTab);
222
- }
223
- }
224
-
225
- nextTab() {
226
- if (this.currentTabIndex < this.tabOrder.length - 1) {
227
- const nextTab = this.tabOrder[this.currentTabIndex + 1];
228
- this.switchTab(nextTab);
229
- }
230
- }
231
-
232
- updateNavigationState() {
233
- const prevBtn = document.getElementById('prev-tab');
234
- const nextBtn = document.getElementById('next-tab');
235
- const currentStepSpan = document.getElementById('current-step');
236
- const totalStepsSpan = document.getElementById('total-steps');
237
-
238
- // Update button states
239
- prevBtn.disabled = this.currentTabIndex === 0;
240
- nextBtn.disabled = this.currentTabIndex === this.tabOrder.length - 1;
241
-
242
- // Update step indicator
243
- if (currentStepSpan) {
244
- currentStepSpan.textContent = this.currentTabIndex + 1;
245
- }
246
- if (totalStepsSpan) {
247
- totalStepsSpan.textContent = this.tabOrder.length;
248
- }
249
-
250
- // Update next button text based on current tab
251
- if (this.currentTabIndex === this.tabOrder.length - 1) {
252
- nextBtn.innerHTML = 'Complete <i class="fas fa-check"></i>';
253
- } else {
254
- nextBtn.innerHTML = 'Next <i class="fas fa-chevron-right"></i>';
255
- }
256
- }
257
-
258
- handleDragOver(e) {
259
- e.preventDefault();
260
- e.currentTarget.style.background = '#e3f2fd';
261
- }
262
-
263
- handleDrop(e) {
264
- e.preventDefault();
265
- e.currentTarget.style.background = '#f8f9fa';
266
-
267
- const files = e.dataTransfer.files;
268
- if (files.length > 0) {
269
- this.processFile(files[0]);
270
- }
271
- }
272
-
273
- handleFileUpload(e) {
274
- console.log('File upload triggered');
275
- console.log('Files:', e.target.files);
276
- const file = e.target.files[0];
277
- if (file) {
278
- console.log('File selected:', file.name, file.size, file.type);
279
- this.processFile(file);
280
- } else {
281
- console.log('No file selected');
282
- }
283
- }
284
-
285
- processFile(file) {
286
- console.log('Processing file:', file.name, file.size, file.type);
287
-
288
- if (!file.name.toLowerCase().endsWith('.pdb') && !file.name.toLowerCase().endsWith('.ent')) {
289
- console.log('Invalid file type:', file.name);
290
- this.showStatus('error', 'Please upload a valid PDB file (.pdb or .ent)');
291
- return;
292
- }
293
-
294
- console.log('File validation passed, reading file...');
295
- const reader = new FileReader();
296
- reader.onload = (e) => {
297
- console.log('File read successfully, content length:', e.target.result.length);
298
- const content = e.target.result;
299
- this.parsePDBFile(content, file.name);
300
- };
301
- reader.onerror = (e) => {
302
- console.error('Error reading file:', e);
303
- this.showStatus('error', 'Error reading file');
304
- };
305
- reader.readAsText(file);
306
- }
307
-
308
- async parsePDBFile(content, filename) {
309
- try {
310
- // Clean output folder when new PDB is loaded
311
- try {
312
- await fetch('/api/clean-output', { method: 'POST' });
313
- } catch (error) {
314
- console.log('Could not clean output folder:', error);
315
- }
316
-
317
- const lines = content.split('\n');
318
- let atomCount = 0;
319
- let chains = new Set();
320
- let residues = new Set();
321
- let waterMolecules = 0;
322
- let ions = 0;
323
- let ligands = new Set();
324
- let ligandDetails = [];
325
- let ligandGroups = new Map(); // Group by RESN-CHAIN
326
- let hetatoms = 0;
327
- let structureId = filename.replace(/\.(pdb|ent)$/i, '').toUpperCase();
328
-
329
- // Common water molecule names
330
- const waterNames = new Set(['HOH', 'WAT', 'TIP3', 'TIP4', 'SPC', 'SPCE']);
331
-
332
- // Common ion names (expanded to include more ions)
333
- const ionNames = new Set(['NA', 'CL', 'K', 'MG', 'CA', 'ZN', 'FE', 'MN', 'CU', 'NI', 'CO',
334
- 'CD', 'HG', 'PB', 'SR', 'BA', 'RB', 'CS', 'LI', 'F', 'BR', 'I', 'SO4', 'PO4', 'CO3', 'NO3', 'NH4']);
335
-
336
- // Track ligand entities (separated by TER or different chains)
337
- let ligandEntities = new Map(); // Map to store ligand entities
338
- let currentLigandEntity = null;
339
- let currentChain = null;
340
- let currentResidue = null;
341
-
342
- // Track unique water molecules by residue
343
- const uniqueWaterResidues = new Set();
344
-
345
- lines.forEach(line => {
346
- if (line.startsWith('ATOM')) {
347
- atomCount++;
348
- const chainId = line.substring(21, 22).trim();
349
- if (chainId) chains.add(chainId);
350
-
351
- const resName = line.substring(17, 20).trim();
352
- const resNum = line.substring(22, 26).trim();
353
- residues.add(`${resName}${resNum}`);
354
- } else if (line.startsWith('HETATM')) {
355
- hetatoms++;
356
- const resName = line.substring(17, 20).trim();
357
- const resNum = line.substring(22, 26).trim();
358
- const chainId = line.substring(21, 22).trim();
359
- const entityKey = `${resName}_${resNum}_${chainId}`;
360
-
361
- if (waterNames.has(resName)) {
362
- waterMolecules++;
363
- uniqueWaterResidues.add(entityKey);
364
- } else if (ionNames.has(resName)) {
365
- ions++;
366
- } else {
367
- // Everything else is treated as ligand (FALLBACK LOGIC)
368
- ligands.add(resName);
369
- ligandDetails.push({ resn: resName, chain: chainId, resi: resNum });
370
-
371
- // Group by RESN-CHAIN for UI display
372
- const groupKey = `${resName}-${chainId}`;
373
- if (!ligandGroups.has(groupKey)) {
374
- ligandGroups.set(groupKey, { resn: resName, chain: chainId });
375
- }
376
-
377
- // Track ligand entities (separated by TER or different chains)
378
- if (currentChain !== chainId || currentResidue !== resName) {
379
- // New ligand entity detected
380
- currentLigandEntity = `${resName}_${chainId}`;
381
- currentChain = chainId;
382
- currentResidue = resName;
383
-
384
- if (!ligandEntities.has(currentLigandEntity)) {
385
- ligandEntities.set(currentLigandEntity, {
386
- name: resName,
387
- chain: chainId,
388
- residueNum: resNum,
389
- atomCount: 0
390
- });
391
- }
392
- }
393
-
394
- // Increment atom count for this ligand entity
395
- if (ligandEntities.has(currentLigandEntity)) {
396
- ligandEntities.get(currentLigandEntity).atomCount++;
397
- }
398
- }
399
- } else if (line.startsWith('TER')) {
400
- // TER record separates entities, reset current ligand tracking
401
- currentLigandEntity = null;
402
- currentChain = null;
403
- currentResidue = null;
404
- }
405
- });
406
-
407
- // Count unique water molecules
408
- const uniqueWaterCount = uniqueWaterResidues.size;
409
-
410
- // Count ligand entities (different ligands or same ligand in different chains)
411
- const ligandEntityCount = ligandEntities.size;
412
-
413
- // Get unique ligand names
414
- const uniqueLigandNames = Array.from(ligands);
415
-
416
- // Create ligand info string
417
- let ligandInfo = 'None';
418
- if (uniqueLigandNames.length > 0) {
419
- if (ligandEntityCount > 1) {
420
- // Multiple entities - show count and names
421
- ligandInfo = `${ligandEntityCount} entities: ${uniqueLigandNames.join(', ')}`;
422
- } else {
423
- // Single entity
424
- ligandInfo = uniqueLigandNames.join(', ');
425
- }
426
- }
427
-
428
- this.currentProtein = {
429
- filename: filename,
430
- structureId: structureId,
431
- atomCount: atomCount,
432
- chains: Array.from(chains),
433
- residueCount: residues.size,
434
- waterMolecules: uniqueWaterCount,
435
- ions: ions,
436
- ligands: uniqueLigandNames,
437
- ligandDetails: ligandDetails,
438
- ligandGroups: Array.from(ligandGroups.values()), // RESN-CHAIN groups for UI
439
- ligandEntities: ligandEntityCount,
440
- ligandInfo: ligandInfo,
441
- hetatoms: hetatoms,
442
- content: content
443
- };
444
-
445
- this.displayProteinInfo();
446
- this.showStatus('success', `Successfully loaded ${filename}`);
447
- } catch (error) {
448
- this.showStatus('error', 'Error parsing PDB file: ' + error.message);
449
- }
450
- }
451
-
452
- displayProteinInfo() {
453
- if (!this.currentProtein) return;
454
-
455
- document.getElementById('structure-id').textContent = this.currentProtein.structureId;
456
- document.getElementById('atom-count').textContent = this.currentProtein.atomCount.toLocaleString();
457
- document.getElementById('chain-info').textContent = this.currentProtein.chains.join(', ');
458
- document.getElementById('residue-count').textContent = this.currentProtein.residueCount.toLocaleString();
459
- document.getElementById('water-count').textContent = this.currentProtein.waterMolecules.toLocaleString();
460
- document.getElementById('ion-count').textContent = this.currentProtein.ions.toLocaleString();
461
- document.getElementById('ligand-info').textContent = this.currentProtein.ligandInfo;
462
- document.getElementById('hetatm-count').textContent = this.currentProtein.hetatoms.toLocaleString();
463
-
464
- document.getElementById('protein-preview').style.display = 'block';
465
-
466
- // Load 3D visualization
467
- this.load3DVisualization();
468
-
469
- // Also refresh chain/ligand lists when protein info is displayed
470
- this.renderChainAndLigandSelections();
471
- }
472
-
473
- async fetchPDB() {
474
- const pdbId = document.getElementById('pdb-id').value.trim().toUpperCase();
475
- if (!pdbId) {
476
- this.showStatus('error', 'Please enter a PDB ID');
477
- return;
478
- }
479
-
480
- if (!/^[0-9A-Z]{4}$/.test(pdbId)) {
481
- this.showStatus('error', 'Please enter a valid 4-character PDB ID');
482
- return;
483
- }
484
-
485
- this.showStatus('info', 'Fetching PDB structure...');
486
-
487
- try {
488
- const response = await fetch(`https://files.rcsb.org/download/${pdbId}.pdb`);
489
- if (!response.ok) {
490
- throw new Error(`PDB ID ${pdbId} not found`);
491
- }
492
-
493
- const content = await response.text();
494
- this.parsePDBFile(content, `${pdbId}.pdb`);
495
- this.showStatus('success', `Successfully fetched PDB structure ${pdbId}`);
496
- } catch (error) {
497
- this.showStatus('error', `Error fetching PDB: ${error.message}`);
498
- }
499
- }
500
-
501
- showStatus(type, message) {
502
- const statusDiv = document.getElementById('pdb-status');
503
- statusDiv.className = `status-message ${type}`;
504
- statusDiv.textContent = message;
505
- statusDiv.style.display = 'block';
506
-
507
- // Auto-hide after 5 seconds for success messages
508
- if (type === 'success') {
509
- setTimeout(() => {
510
- statusDiv.style.display = 'none';
511
- }, 5000);
512
- }
513
- }
514
-
515
- updateSimulationParams() {
516
- // Update basic parameters
517
- this.simulationParams.boxType = document.getElementById('box-type').value;
518
- this.simulationParams.boxSize = parseFloat(document.getElementById('box-size').value);
519
- this.simulationParams.forceField = document.getElementById('force-field').value;
520
- this.simulationParams.waterModel = document.getElementById('water-model').value;
521
- this.simulationParams.addIons = document.getElementById('add-ions').value;
522
- this.simulationParams.temperature = parseInt(document.getElementById('temperature').value);
523
- this.simulationParams.pressure = parseFloat(document.getElementById('pressure').value);
524
- this.simulationParams.couplingType = document.getElementById('coupling-type').value;
525
- this.simulationParams.timestep = parseFloat(document.getElementById('timestep').value);
526
- this.simulationParams.cutoff = parseFloat(document.getElementById('cutoff').value);
527
- this.simulationParams.electrostatic = document.getElementById('electrostatic').value;
528
- this.simulationParams.ligandForceField = document.getElementById('ligand-forcefield').value;
529
-
530
- // Update step parameters
531
- this.simulationParams.steps.restrainedMin = {
532
- enabled: document.getElementById('enable-restrained-min').checked,
533
- steps: parseInt(document.getElementById('restrained-steps').value),
534
- force: parseInt(document.getElementById('restrained-force').value)
535
- };
536
-
537
- this.simulationParams.steps.minimization = {
538
- enabled: document.getElementById('enable-minimization').checked,
539
- steps: parseInt(document.getElementById('min-steps').value),
540
- algorithm: document.getElementById('min-algorithm').value
541
- };
542
-
543
- this.simulationParams.steps.nvt = {
544
- enabled: document.getElementById('enable-nvt').checked,
545
- steps: parseInt(document.getElementById('nvt-steps').value),
546
- temperature: parseInt(document.getElementById('nvt-temp').value)
547
- };
548
-
549
- this.simulationParams.steps.npt = {
550
- enabled: document.getElementById('enable-npt').checked,
551
- steps: parseInt(document.getElementById('npt-steps').value),
552
- temperature: parseInt(document.getElementById('npt-temp').value),
553
- pressure: parseFloat(document.getElementById('npt-pressure').value)
554
- };
555
-
556
- this.simulationParams.steps.production = {
557
- enabled: document.getElementById('enable-production').checked,
558
- steps: parseInt(document.getElementById('prod-steps').value),
559
- temperature: parseInt(document.getElementById('prod-temp').value),
560
- pressure: parseFloat(document.getElementById('prod-pressure').value)
561
- };
562
- }
563
-
564
- toggleLigandForceFieldGroup(show) {
565
- const section = document.getElementById('ligand-forcefield-section');
566
- if (show) {
567
- section.style.display = 'block';
568
- section.classList.remove('disabled');
569
- } else {
570
- section.style.display = 'none';
571
- section.classList.add('disabled');
572
- }
573
- }
574
-
575
- async calculateNetCharge(event) {
576
- console.log('calculateNetCharge called'); // Debug log
577
- if (!this.preparedProtein) {
578
- alert('Please prepare structure first before calculating net charge.');
579
- return;
580
- }
581
-
582
- // Show loading state
583
- const button = event ? event.target : document.querySelector('button[onclick*="calculateNetCharge"]');
584
- const originalText = button.innerHTML;
585
- button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Calculating...';
586
- button.disabled = true;
587
-
588
- try {
589
- // Get the selected force field
590
- const selectedForceField = document.getElementById('force-field').value;
591
-
592
- const response = await fetch('/api/calculate-net-charge', {
593
- method: 'POST',
594
- headers: {
595
- 'Content-Type': 'application/json',
596
- },
597
- body: JSON.stringify({
598
- force_field: selectedForceField
599
- })
600
- });
601
-
602
- const result = await response.json();
603
-
604
- if (result.success) {
605
- // Update the Add Ions dropdown based on suggestion
606
- const addIonsSelect = document.getElementById('add-ions');
607
- if (result.ion_type === 'Cl-') {
608
- addIonsSelect.value = 'Cl-';
609
- } else if (result.ion_type === 'Na+') {
610
- addIonsSelect.value = 'Na+';
611
- } else {
612
- addIonsSelect.value = 'None';
613
- }
614
-
615
- // Show detailed results
616
- alert(`✅ System Charge Analysis Complete!\n\n` +
617
- `Net Charge: ${result.net_charge}\n` +
618
- `Recommendation: ${result.suggestion}\n` +
619
- `Ligand Present: ${result.ligand_present ? 'Yes' : 'No'}`);
620
- } else {
621
- alert(`❌ Error: ${result.error}`);
622
- }
623
- } catch (error) {
624
- console.error('Error calculating net charge:', error);
625
- alert(`❌ Error: Failed to calculate net charge. ${error.message}`);
626
- } finally {
627
- // Restore button state
628
- button.innerHTML = originalText;
629
- button.disabled = false;
630
- }
631
- }
632
-
633
- async generateLigandFF(event) {
634
- console.log('generateLigandFF called'); // Debug log
635
- if (!this.preparedProtein || !this.preparedProtein.ligand_present) {
636
- alert('No ligand found. Please ensure ligands are preserved during structure preparation.');
637
- return;
638
- }
639
-
640
- const selectedFF = document.getElementById('ligand-forcefield').value;
641
-
642
- // Show loading state
643
- const button = event ? event.target : document.querySelector('button[onclick*="generateLigandFF"]');
644
- const originalText = button.innerHTML;
645
- button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating...';
646
- button.disabled = true;
647
-
648
- try {
649
- const response = await fetch('/api/generate-ligand-ff', {
650
- method: 'POST',
651
- headers: {
652
- 'Content-Type': 'application/json',
653
- },
654
- body: JSON.stringify({
655
- force_field: selectedFF
656
- })
657
- });
658
-
659
- const result = await response.json();
660
-
661
- if (result.success) {
662
- alert(`✅ ${result.message}\n\nNet charge: ${result.net_charge}\n\nGenerated files:\n- ${result.files.mol2}\n- ${result.files.frcmod}`);
663
- } else {
664
- alert(`❌ Error: ${result.error}`);
665
- }
666
- } catch (error) {
667
- console.error('Error generating ligand force field:', error);
668
- alert(`❌ Error: Failed to generate force field parameters. ${error.message}`);
669
- } finally {
670
- // Restore button state
671
- button.innerHTML = originalText;
672
- button.disabled = false;
673
- }
674
- }
675
-
676
- countAtomsInPDB(pdbContent) {
677
- const lines = pdbContent.split('\n');
678
- return lines.filter(line => line.startsWith('ATOM') || line.startsWith('HETATM')).length;
679
- }
680
-
681
- async generateAllFiles() {
682
- if (!this.preparedProtein) {
683
- alert('Please prepare structure first');
684
- return;
685
- }
686
-
687
- // Show loading state
688
- const button = document.getElementById('generate-files');
689
- const originalText = button.innerHTML;
690
- button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating...';
691
- button.disabled = true;
692
-
693
- try {
694
- // Collect all simulation parameters
695
- const params = {
696
- cutoff_distance: parseFloat(document.getElementById('cutoff').value),
697
- temperature: parseFloat(document.getElementById('temperature').value),
698
- pressure: parseFloat(document.getElementById('pressure').value),
699
- restrained_steps: parseInt(document.getElementById('restrained-steps').value),
700
- restrained_force: parseFloat(document.getElementById('restrained-force').value),
701
- min_steps: parseInt(document.getElementById('min-steps').value),
702
- npt_heating_steps: parseInt(document.getElementById('nvt-steps').value),
703
- npt_equilibration_steps: parseInt(document.getElementById('npt-steps').value),
704
- production_steps: parseInt(document.getElementById('prod-steps').value),
705
- timestep: parseFloat(document.getElementById('timestep').value),
706
- // Force field parameters
707
- force_field: document.getElementById('force-field').value,
708
- water_model: document.getElementById('water-model').value,
709
- add_ions: document.getElementById('add-ions').value,
710
- distance: parseFloat(document.getElementById('box-size').value)
711
- };
712
-
713
- const response = await fetch('/api/generate-all-files', {
714
- method: 'POST',
715
- headers: {
716
- 'Content-Type': 'application/json',
717
- },
718
- body: JSON.stringify(params)
719
- });
720
-
721
- const result = await response.json();
722
-
723
- if (result.success) {
724
- let message = `✅ ${result.message}\n\nGenerated files:\n`;
725
- result.files_generated.forEach(file => {
726
- message += `- ${file}\n`;
727
- });
728
-
729
- if (result.warnings && result.warnings.length > 0) {
730
- message += `\n⚠️ Warnings:\n`;
731
- result.warnings.forEach(warning => {
732
- message += `- ${warning}\n`;
733
- });
734
- }
735
-
736
- alert(message);
737
-
738
- // Reveal the download section
739
- const downloadSection = document.getElementById('download-section');
740
- if (downloadSection) {
741
- downloadSection.style.display = 'block';
742
- }
743
- } else {
744
- alert(`❌ Error: ${result.error}`);
745
- }
746
- } catch (error) {
747
- console.error('Error generating files:', error);
748
- alert(`❌ Error: Failed to generate simulation files. ${error.message}`);
749
- } finally {
750
- // Restore button state
751
- button.innerHTML = originalText;
752
- button.disabled = false;
753
- }
754
- }
755
-
756
- createSimulationFiles() {
757
- const files = {};
758
- const proteinName = this.currentProtein.structureId.toLowerCase();
759
-
760
- // Generate GROMACS input files
761
- files[`${proteinName}.mdp`] = this.generateMDPFile();
762
- files[`${proteinName}_restrained.mdp`] = this.generateRestrainedMDPFile();
763
- files[`${proteinName}_min.mdp`] = this.generateMinimizationMDPFile();
764
- files[`${proteinName}_nvt.mdp`] = this.generateNVTMDPFile();
765
- files[`${proteinName}_npt.mdp`] = this.generateNPTMDPFile();
766
- files[`${proteinName}_prod.mdp`] = this.generateProductionMDPFile();
767
-
768
- // Generate PBS script
769
- files[`${proteinName}_simulation.pbs`] = this.generatePBSScript();
770
-
771
- // Generate setup script
772
- files[`setup_${proteinName}.sh`] = this.generateSetupScript();
773
-
774
- // Generate analysis script
775
- files[`analyze_${proteinName}.sh`] = this.generateAnalysisScript();
776
-
777
- return files;
778
- }
779
-
780
- generateMDPFile() {
781
- const params = this.simulationParams;
782
- return `; MD Simulation Parameters
783
- ; Generated by MD Simulation Pipeline
784
-
785
- ; Run parameters
786
- integrator = md
787
- dt = ${params.timestep}
788
- nsteps = ${params.steps.production.steps}
789
-
790
- ; Output control
791
- nstxout = 5000
792
- nstvout = 5000
793
- nstenergy = 1000
794
- nstlog = 1000
795
-
796
- ; Bond parameters
797
- constraint_algorithm = lincs
798
- constraints = h-bonds
799
- lincs_iter = 1
800
- lincs_order = 4
801
-
802
- ; Neighbor searching
803
- cutoff-scheme = Verlet
804
- ns_type = grid
805
- nstlist = 40
806
- rlist = ${params.cutoff}
807
-
808
- ; Electrostatics
809
- coulombtype = PME
810
- rcoulomb = ${params.cutoff}
811
- pme_order = ${params.pmeOrder}
812
- fourierspacing = 0.16
813
-
814
- ; Van der Waals
815
- vdwtype = Cut-off
816
- rvdw = ${params.cutoff}
817
-
818
- ; Temperature coupling
819
- tcoupl = ${params.couplingType}
820
- tc-grps = Protein Non-Protein
821
- tau_t = 0.1 0.1
822
- ref_t = ${params.temperature} ${params.temperature}
823
-
824
- ; Pressure coupling
825
- pcoupl = ${params.couplingType}
826
- pcoupltype = isotropic
827
- tau_p = 2.0
828
- ref_p = ${params.pressure}
829
- compressibility = 4.5e-5
830
-
831
- ; Dispersion correction
832
- DispCorr = EnerPres
833
-
834
- ; Velocity generation
835
- gen_vel = yes
836
- gen_temp = ${params.temperature}
837
- gen_seed = -1
838
- `;
839
- }
840
-
841
- generateRestrainedMDPFile() {
842
- const params = this.simulationParams;
843
- return `; Restrained Minimization Parameters
844
- integrator = steep
845
- nsteps = ${params.steps.restrainedMin.steps}
846
- emstep = 0.01
847
- emtol = 1000
848
-
849
- ; Position restraints
850
- define = -DPOSRES
851
- refcoord_scaling = com
852
-
853
- ; Output control
854
- nstxout = 100
855
- nstenergy = 100
856
- nstlog = 100
857
-
858
- ; Bond parameters
859
- constraint_algorithm = lincs
860
- constraints = h-bonds
861
-
862
- ; Neighbor searching
863
- cutoff-scheme = Verlet
864
- ns_type = grid
865
- nstlist = 10
866
- rlist = ${params.cutoff}
867
-
868
- ; Electrostatics
869
- coulombtype = PME
870
- rcoulomb = ${params.cutoff}
871
- pme_order = ${params.pme_order}
872
-
873
- ; Van der Waals
874
- vdwtype = Cut-off
875
- rvdw = ${params.cutoff}
876
- `;
877
- }
878
-
879
- generateMinimizationMDPFile() {
880
- const params = this.simulationParams;
881
- return `; Minimization Parameters
882
- integrator = ${params.steps.minimization.algorithm}
883
- nsteps = ${params.steps.minimization.steps}
884
- emstep = 0.01
885
- emtol = 1000
886
-
887
- ; Output control
888
- nstxout = 100
889
- nstenergy = 100
890
- nstlog = 100
891
-
892
- ; Bond parameters
893
- constraint_algorithm = lincs
894
- constraints = h-bonds
895
-
896
- ; Neighbor searching
897
- cutoff-scheme = Verlet
898
- ns_type = grid
899
- nstlist = 10
900
- rlist = ${params.cutoff}
901
-
902
- ; Electrostatics
903
- coulombtype = PME
904
- rcoulomb = ${params.cutoff}
905
- pme_order = ${params.pme_order}
906
-
907
- ; Van der Waals
908
- vdwtype = Cut-off
909
- rvdw = ${params.cutoff}
910
- `;
911
- }
912
-
913
- generateNVTMDPFile() {
914
- const params = this.simulationParams;
915
- return `; NVT Equilibration Parameters
916
- integrator = md
917
- dt = ${params.timestep}
918
- nsteps = ${params.steps.nvt.steps}
919
-
920
- ; Output control
921
- nstxout = 5000
922
- nstvout = 5000
923
- nstenergy = 1000
924
- nstlog = 1000
925
-
926
- ; Bond parameters
927
- constraint_algorithm = lincs
928
- constraints = h-bonds
929
- lincs_iter = 1
930
- lincs_order = 4
931
-
932
- ; Neighbor searching
933
- cutoff-scheme = Verlet
934
- ns_type = grid
935
- nstlist = 40
936
- rlist = ${params.cutoff}
937
-
938
- ; Electrostatics
939
- coulombtype = PME
940
- rcoulomb = ${params.cutoff}
941
- pme_order = ${params.pme_order}
942
-
943
- ; Van der Waals
944
- vdwtype = Cut-off
945
- rvdw = ${params.cutoff}
946
-
947
- ; Temperature coupling
948
- tcoupl = ${params.couplingType}
949
- tc-grps = Protein Non-Protein
950
- tau_t = 0.1 0.1
951
- ref_t = ${params.steps.nvt.temperature} ${params.steps.nvt.temperature}
952
-
953
- ; Pressure coupling (disabled for NVT)
954
- pcoupl = no
955
-
956
- ; Velocity generation
957
- gen_vel = yes
958
- gen_temp = ${params.steps.nvt.temperature}
959
- gen_seed = -1
960
- `;
961
- }
962
-
963
- generateNPTMDPFile() {
964
- const params = this.simulationParams;
965
- return `; NPT Equilibration Parameters
966
- integrator = md
967
- dt = ${params.timestep}
968
- nsteps = ${params.steps.npt.steps}
969
-
970
- ; Output control
971
- nstxout = 5000
972
- nstvout = 5000
973
- nstenergy = 1000
974
- nstlog = 1000
975
-
976
- ; Bond parameters
977
- constraint_algorithm = lincs
978
- constraints = h-bonds
979
- lincs_iter = 1
980
- lincs_order = 4
981
-
982
- ; Neighbor searching
983
- cutoff-scheme = Verlet
984
- ns_type = grid
985
- nstlist = 40
986
- rlist = ${params.cutoff}
987
-
988
- ; Electrostatics
989
- coulombtype = PME
990
- rcoulomb = ${params.cutoff}
991
- pme_order = ${params.pme_order}
992
-
993
- ; Van der Waals
994
- vdwtype = Cut-off
995
- rvdw = ${params.cutoff}
996
-
997
- ; Temperature coupling
998
- tcoupl = ${params.couplingType}
999
- tc-grps = Protein Non-Protein
1000
- tau_t = 0.1 0.1
1001
- ref_t = ${params.steps.npt.temperature} ${params.steps.npt.temperature}
1002
-
1003
- ; Pressure coupling
1004
- pcoupl = ${params.couplingType}
1005
- pcoupltype = isotropic
1006
- tau_p = 2.0
1007
- ref_p = ${params.steps.npt.pressure}
1008
- compressibility = 4.5e-5
1009
-
1010
- ; Velocity generation
1011
- gen_vel = no
1012
- `;
1013
- }
1014
-
1015
- generateProductionMDPFile() {
1016
- return this.generateMDPFile(); // Same as main MDP file
1017
- }
1018
-
1019
- generatePBSScript() {
1020
- const proteinName = this.currentProtein.structureId.toLowerCase();
1021
- const totalSteps = this.simulationParams.steps.production.steps;
1022
- const timeInNs = (totalSteps * this.simulationParams.timestep) / 1000;
1023
-
1024
- return `#!/bin/bash
1025
- #PBS -N ${proteinName}_md
1026
- #PBS -l nodes=1:ppn=16
1027
- #PBS -l walltime=24:00:00
1028
- #PBS -q normal
1029
- #PBS -j oe
1030
-
1031
- # Change to the directory where the job was submitted
1032
- cd $PBS_O_WORKDIR
1033
-
1034
- # Load required modules
1035
- module load gromacs/2023.2
1036
- module load intel/2021.4.0
1037
-
1038
- # Set up environment
1039
- export OMP_NUM_THREADS=16
1040
- export GMX_MAXBACKUP=-1
1041
-
1042
- # Simulation parameters
1043
- PROTEIN=${proteinName}
1044
- STEPS=${totalSteps}
1045
- TIME_NS=${timeInNs.toFixed(2)}
1046
-
1047
- echo "Starting MD simulation for $PROTEIN"
1048
- echo "Total simulation time: $TIME_NS ns"
1049
- echo "Job started at: $(date)"
1050
-
1051
- # Run the simulation
1052
- ./run_simulation.sh $PROTEIN
1053
-
1054
- echo "Simulation completed at: $(date)"
1055
- echo "Results saved in output directory"
1056
- `;
1057
- }
1058
-
1059
- generateSetupScript() {
1060
- const proteinName = this.currentProtein.structureId.toLowerCase();
1061
- return `#!/bin/bash
1062
- # Setup script for ${proteinName} MD simulation
1063
- # Generated by MD Simulation Pipeline
1064
-
1065
- set -e
1066
-
1067
- PROTEIN=${proteinName}
1068
- FORCE_FIELD=${this.simulationParams.forceField}
1069
- WATER_MODEL=${this.simulationParams.waterModel}
1070
-
1071
- echo "Setting up MD simulation for $PROTEIN"
1072
-
1073
- # Create output directory
1074
- mkdir -p output
1075
-
1076
- # 1. Prepare protein structure
1077
- echo "Preparing protein structure..."
1078
- gmx pdb2gmx -f ${PROTEIN}.pdb -o ${PROTEIN}_processed.gro -p ${PROTEIN}.top -ff ${FORCE_FIELD} -water ${WATER_MODEL}
1079
-
1080
- # 2. Define simulation box
1081
- echo "Defining simulation box..."
1082
- gmx editconf -f ${PROTEIN}_processed.gro -o ${PROTEIN}_box.gro -c -d ${this.simulationParams.boxMargin} -bt ${this.simulationParams.boxType}
1083
-
1084
- # 3. Add solvent
1085
- echo "Adding solvent..."
1086
- gmx solvate -cp ${PROTEIN}_box.gro -cs spc216.gro -o ${PROTEIN}_solv.gro -p ${PROTEIN}.top
1087
-
1088
- # 4. Add ions
1089
- echo "Adding ions..."
1090
- gmx grompp -f ${PROTEIN}_restrained.mdp -c ${PROTEIN}_solv.gro -p ${PROTEIN}.top -o ${PROTEIN}_ions.tpr
1091
- echo "SOL" | gmx genion -s ${PROTEIN}_ions.tpr -o ${PROTEIN}_final.gro -p ${PROTEIN}.top -pname NA -nname CL -neutral
1092
-
1093
- echo "Setup completed successfully!"
1094
- echo "Ready to run simulation with: ./run_simulation.sh $PROTEIN"
1095
- `;
1096
- }
1097
-
1098
- generateAnalysisScript() {
1099
- const proteinName = this.currentProtein.structureId.toLowerCase();
1100
- return `#!/bin/bash
1101
- # Analysis script for ${proteinName} MD simulation
1102
- # Generated by MD Simulation Pipeline
1103
-
1104
- PROTEIN=${proteinName}
1105
-
1106
- echo "Analyzing MD simulation results for $PROTEIN"
1107
-
1108
- # Create analysis directory
1109
- mkdir -p analysis
1110
-
1111
- # 1. RMSD analysis
1112
- echo "Calculating RMSD..."
1113
- echo "Protein" | gmx rms -s ${PROTEIN}_final.tpr -f ${PROTEIN}_prod.xtc -o analysis/${PROTEIN}_rmsd.xvg -tu ns
1114
-
1115
- # 2. RMSF analysis
1116
- echo "Calculating RMSF..."
1117
- echo "Protein" | gmx rmsf -s ${PROTEIN}_final.tpr -f ${PROTEIN}_prod.xtc -o analysis/${PROTEIN}_rmsf.xvg -res
1118
-
1119
- # 3. Radius of gyration
1120
- echo "Calculating radius of gyration..."
1121
- echo "Protein" | gmx gyrate -s ${PROTEIN}_final.tpr -f ${PROTEIN}_prod.xtc -o analysis/${PROTEIN}_gyrate.xvg
1122
-
1123
- # 4. Hydrogen bonds
1124
- echo "Analyzing hydrogen bonds..."
1125
- echo "Protein" | gmx hbond -s ${PROTEIN}_final.tpr -f ${PROTEIN}_prod.xtc -num analysis/${PROTEIN}_hbonds.xvg
1126
-
1127
- # 5. Energy analysis
1128
- echo "Analyzing energies..."
1129
- gmx energy -f ${PROTEIN}_prod.edr -o analysis/${PROTEIN}_energy.xvg
1130
-
1131
- # 6. Generate plots
1132
- echo "Generating analysis plots..."
1133
- python3 plot_analysis.py ${PROTEIN}
1134
-
1135
- echo "Analysis completed! Results saved in analysis/ directory"
1136
- `;
1137
- }
1138
-
1139
- displayGeneratedFiles() {
1140
- const filesList = document.getElementById('files-list');
1141
- filesList.innerHTML = '';
1142
-
1143
- Object.entries(this.generatedFiles).forEach(([filename, content]) => {
1144
- const fileItem = document.createElement('div');
1145
- fileItem.className = 'file-item';
1146
-
1147
- const fileType = this.getFileType(filename);
1148
- const fileSize = this.formatFileSize(content.length);
1149
-
1150
- fileItem.innerHTML = `
1151
- <h4><i class="fas ${this.getFileIcon(filename)}"></i> ${filename}</h4>
1152
- <p><strong>Type:</strong> ${fileType}</p>
1153
- <p><strong>Size:</strong> ${fileSize}</p>
1154
- <button class="btn btn-secondary btn-sm" onclick="mdPipeline.previewFile('${filename}')">
1155
- <i class="fas fa-eye"></i> Preview
1156
- </button>
1157
- <button class="btn btn-primary btn-sm" onclick="mdPipeline.downloadFile('${filename}')">
1158
- <i class="fas fa-download"></i> Download
1159
- </button>
1160
- `;
1161
-
1162
- filesList.appendChild(fileItem);
1163
- });
1164
- }
1165
-
1166
- getFileType(filename) {
1167
- const extension = filename.split('.').pop().toLowerCase();
1168
- const types = {
1169
- 'mdp': 'GROMACS MDP',
1170
- 'pbs': 'PBS Script',
1171
- 'sh': 'Shell Script',
1172
- 'gro': 'GROMACS Structure',
1173
- 'top': 'GROMACS Topology',
1174
- 'xvg': 'GROMACS Data'
1175
- };
1176
- return types[extension] || 'Text File';
1177
- }
1178
-
1179
- getFileIcon(filename) {
1180
- const extension = filename.split('.').pop().toLowerCase();
1181
- const icons = {
1182
- 'mdp': 'fa-cogs',
1183
- 'pbs': 'fa-tasks',
1184
- 'sh': 'fa-terminal',
1185
- 'gro': 'fa-cube',
1186
- 'top': 'fa-sitemap',
1187
- 'xvg': 'fa-chart-line'
1188
- };
1189
- return icons[extension] || 'fa-file';
1190
- }
1191
-
1192
- formatFileSize(bytes) {
1193
- if (bytes < 1024) return bytes + ' B';
1194
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
1195
- return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
1196
- }
1197
-
1198
- previewFile(filename) {
1199
- const content = this.generatedFiles[filename];
1200
- const previewWindow = window.open('', '_blank', 'width=800,height=600');
1201
- previewWindow.document.write(`
1202
- <html>
1203
- <head>
1204
- <title>Preview: ${filename}</title>
1205
- <style>
1206
- body { font-family: monospace; margin: 20px; background: #f5f5f5; }
1207
- pre { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
1208
- h1 { color: #333; }
1209
- </style>
1210
- </head>
1211
- <body>
1212
- <h1>${filename}</h1>
1213
- <pre>${content}</pre>
1214
- </body>
1215
- </html>
1216
- `);
1217
- }
1218
-
1219
- downloadFile(filename) {
1220
- const content = this.generatedFiles[filename];
1221
- const blob = new Blob([content], { type: 'text/plain' });
1222
- const url = URL.createObjectURL(blob);
1223
- const a = document.createElement('a');
1224
- a.href = url;
1225
- a.download = filename;
1226
- document.body.appendChild(a);
1227
- a.click();
1228
- document.body.removeChild(a);
1229
- URL.revokeObjectURL(url);
1230
- }
1231
-
1232
- async previewFiles() {
1233
- try {
1234
- const resp = await fetch('/api/get-generated-files');
1235
- const data = await resp.json();
1236
- if (!data.success) {
1237
- alert('❌ Error: ' + (data.error || 'Unable to load files'));
1238
- return;
1239
- }
1240
- const filesList = document.getElementById('files-list');
1241
- if (!filesList) return;
1242
- filesList.innerHTML = '';
1243
-
1244
- // Store file contents for modal display
1245
- this.fileContents = data.files;
1246
-
1247
- Object.entries(data.files).forEach(([name, content]) => {
1248
- const fileItem = document.createElement('div');
1249
- fileItem.className = 'file-item';
1250
- fileItem.style.cssText = 'padding: 10px; margin: 5px 0; border: 1px solid #ddd; border-radius: 5px; cursor: pointer; background: #f9f9f9;';
1251
- fileItem.innerHTML = `<strong>${name}</strong>`;
1252
- fileItem.onclick = () => this.showFileContent(name, content);
1253
- filesList.appendChild(fileItem);
1254
- });
1255
-
1256
- // Reveal preview and download areas
1257
- const preview = document.getElementById('files-preview');
1258
- if (preview) preview.style.display = 'block';
1259
- const dl = document.getElementById('download-section');
1260
- if (dl) dl.style.display = 'block';
1261
- this.switchTab('file-generation');
1262
- } catch (e) {
1263
- console.error('Preview error:', e);
1264
- alert('❌ Failed to preview files: ' + e.message);
1265
- }
1266
- }
1267
-
1268
- showFileContent(filename, content) {
1269
- // Create modal if it doesn't exist
1270
- let modal = document.getElementById('file-content-modal');
1271
- if (!modal) {
1272
- modal = document.createElement('div');
1273
- modal.id = 'file-content-modal';
1274
- modal.style.cssText = `
1275
- position: fixed; top: 0; left: 0; width: 100%; height: 100%;
1276
- background: rgba(0,0,0,0.5); z-index: 1000; display: none;
1277
- `;
1278
- modal.innerHTML = `
1279
- <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
1280
- background: white; border-radius: 10px; padding: 20px; max-width: 80%; max-height: 80%;
1281
- overflow: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
1282
- <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
1283
- <h3 id="modal-filename" style="margin: 0; color: #333;"></h3>
1284
- <button id="close-modal" style="background: #dc3545; color: white; border: none;
1285
- border-radius: 5px; padding: 8px 15px; cursor: pointer;">Close</button>
1286
- </div>
1287
- <pre id="modal-content" style="background: #f8f9fa; padding: 15px; border-radius: 5px;
1288
- overflow: auto; max-height: 60vh; white-space: pre-wrap; font-family: monospace;"></pre>
1289
- </div>
1290
- `;
1291
- document.body.appendChild(modal);
1292
-
1293
- // Close modal handlers
1294
- document.getElementById('close-modal').onclick = () => modal.style.display = 'none';
1295
- modal.onclick = (e) => {
1296
- if (e.target === modal) modal.style.display = 'none';
1297
- };
1298
- }
1299
-
1300
- // Populate and show modal
1301
- document.getElementById('modal-filename').textContent = filename;
1302
- document.getElementById('modal-content').textContent = content;
1303
- modal.style.display = 'block';
1304
- }
1305
-
1306
- async downloadZip() {
1307
- try {
1308
- const resp = await fetch('/api/download-output-zip');
1309
- if (!resp.ok) {
1310
- const text = await resp.text();
1311
- throw new Error(text || 'Failed to create ZIP');
1312
- }
1313
- const blob = await resp.blob();
1314
- const url = window.URL.createObjectURL(blob);
1315
- const a = document.createElement('a');
1316
- a.href = url;
1317
- a.download = 'output.zip';
1318
- document.body.appendChild(a);
1319
- a.click();
1320
- a.remove();
1321
- window.URL.revokeObjectURL(url);
1322
- } catch (e) {
1323
- console.error('Download error:', e);
1324
- alert('❌ Failed to download ZIP: ' + e.message);
1325
- }
1326
- }
1327
-
1328
- async previewSolvatedProtein() {
1329
- try {
1330
- // Show loading state
1331
- const button = document.getElementById('preview-solvated');
1332
- const originalText = button.innerHTML;
1333
- button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Loading...';
1334
- button.disabled = true;
1335
-
1336
- // Fetch a single viewer PDB that marks ligands as HETATM within protein_solvated frame
1337
- const response = await fetch('/api/get-viewer-pdb');
1338
- if (!response.ok) {
1339
- throw new Error('Viewer PDB not available. Please generate files first.');
1340
- }
1341
-
1342
- const data = await response.json();
1343
- if (!data.success) {
1344
- throw new Error(data.error || 'Failed to load viewer PDB');
1345
- }
1346
-
1347
- // Open the dedicated viewer page (bypasses CSP issues)
1348
- window.open('/viewer/viewer_protein_with_ligand.pdb', '_blank');
1349
-
1350
- } catch (error) {
1351
- console.error('Error previewing solvated protein:', error);
1352
- alert('❌ Error: ' + error.message);
1353
- } finally {
1354
- // Restore button state
1355
- const button = document.getElementById('preview-solvated');
1356
- button.innerHTML = '<i class="fas fa-tint"></i> Preview Solvated Protein';
1357
- button.disabled = false;
1358
- }
1359
- }
1360
-
1361
-
1362
-
1363
- displaySimulationSummary() {
1364
- const summaryContent = document.getElementById('summary-content');
1365
- const params = this.simulationParams;
1366
- const protein = this.currentProtein;
1367
-
1368
- const totalTime = (params.steps.production.steps * params.timestep) / 1000; // Convert to ns
1369
-
1370
- summaryContent.innerHTML = `
1371
- <div class="summary-item">
1372
- <h4>Protein Information</h4>
1373
- <p><strong>Structure ID:</strong> ${protein.structureId}</p>
1374
- <p><strong>Atoms:</strong> ${protein.atomCount.toLocaleString()}</p>
1375
- <p><strong>Chains:</strong> ${protein.chains.join(', ')}</p>
1376
- <p><strong>Residues:</strong> ${protein.residueCount.toLocaleString()}</p>
1377
- </div>
1378
- <div class="summary-item">
1379
- <h4>System Components</h4>
1380
- <p><strong>Water molecules:</strong> ${protein.waterMolecules.toLocaleString()}</p>
1381
- <p><strong>Ions:</strong> ${protein.ions.toLocaleString()}</p>
1382
- <p><strong>Ligands:</strong> ${protein.ligands.length > 0 ? protein.ligands.join(', ') : 'None'}</p>
1383
- <p><strong>HETATM entries:</strong> ${protein.hetatoms.toLocaleString()}</p>
1384
- </div>
1385
- <div class="summary-item">
1386
- <h4>Simulation Box</h4>
1387
- <p><strong>Type:</strong> ${params.boxType}</p>
1388
- <p><strong>Size:</strong> ${params.boxSize} nm</p>
1389
- <p><strong>Margin:</strong> ${params.boxMargin} nm</p>
1390
- </div>
1391
- <div class="summary-item">
1392
- <h4>Force Field & Water</h4>
1393
- <p><strong>Force Field:</strong> ${params.forceField}</p>
1394
- <p><strong>Water Model:</strong> ${params.waterModel}</p>
1395
- <p><strong>Ion Conc.:</strong> ${params.ionConcentration} mM</p>
1396
- </div>
1397
- <div class="summary-item">
1398
- <h4>Simulation Parameters</h4>
1399
- <p><strong>Temperature:</strong> ${params.temperature} K</p>
1400
- <p><strong>Pressure:</strong> ${params.pressure} bar</p>
1401
- <p><strong>Time Step:</strong> ${params.timestep} ps</p>
1402
- </div>
1403
- <div class="summary-item">
1404
- <h4>Simulation Time</h4>
1405
- <p><strong>Total Time:</strong> ${totalTime.toFixed(2)} ns</p>
1406
- <p><strong>Steps:</strong> ${params.steps.production.steps.toLocaleString()}</p>
1407
- <p><strong>Output Freq:</strong> Every 5 ps</p>
1408
- </div>
1409
- <div class="summary-item">
1410
- <h4>Generated Files</h4>
1411
- <p><strong>MDP Files:</strong> 6</p>
1412
- <p><strong>Scripts:</strong> 3</p>
1413
- <p><strong>Total Size:</strong> ${this.formatFileSize(Object.values(this.generatedFiles).join('').length)}</p>
1414
- </div>
1415
- `;
1416
- }
1417
-
1418
- // 3D Visualization Methods
1419
- async load3DVisualization() {
1420
- if (!this.currentProtein) return;
1421
-
1422
- try {
1423
- // Initialize NGL stage if not already done
1424
- if (!this.nglStage) {
1425
- this.nglStage = new NGL.Stage("ngl-viewer", {
1426
- backgroundColor: "white",
1427
- quality: "medium"
1428
- });
1429
- }
1430
-
1431
- // Clear existing components
1432
- this.nglStage.removeAllComponents();
1433
-
1434
- // Create a blob from PDB content
1435
- const blob = new Blob([this.currentProtein.content], { type: 'text/plain' });
1436
- const url = URL.createObjectURL(blob);
1437
-
1438
- // Load the structure
1439
- const component = await this.nglStage.loadFile(url, {
1440
- ext: "pdb",
1441
- defaultRepresentation: false
1442
- });
1443
-
1444
- // Add cartoon representation for protein with chain-based colors
1445
- component.addRepresentation("cartoon", {
1446
- sele: "protein",
1447
- colorScheme: "chainname",
1448
- opacity: 0.9
1449
- });
1450
-
1451
- // Add ball and stick for water molecules
1452
- if (this.currentProtein.waterMolecules > 0) {
1453
- component.addRepresentation("ball+stick", {
1454
- sele: "water",
1455
- color: "cyan",
1456
- colorScheme: "uniform",
1457
- radius: 0.1
1458
- });
1459
- }
1460
-
1461
- // Add ball and stick for ions
1462
- if (this.currentProtein.ions > 0) {
1463
- component.addRepresentation("ball+stick", {
1464
- sele: "ion",
1465
- color: "element",
1466
- radius: 0.2
1467
- });
1468
- }
1469
-
1470
- // Add ball and stick for ligands
1471
- if (this.currentProtein.ligands.length > 0) {
1472
- component.addRepresentation("ball+stick", {
1473
- sele: "hetero",
1474
- color: "element",
1475
- radius: 0.15
1476
- });
1477
- }
1478
-
1479
- // Auto-fit the view
1480
- this.nglStage.autoView();
1481
-
1482
- // Show controls
1483
- document.getElementById('viewer-controls').style.display = 'flex';
1484
-
1485
- // Clean up the blob URL
1486
- URL.revokeObjectURL(url);
1487
-
1488
- } catch (error) {
1489
- console.error('Error loading 3D visualization:', error);
1490
- this.showStatus('error', 'Error loading 3D visualization: ' + error.message);
1491
- }
1492
- }
1493
-
1494
- resetView() {
1495
- if (this.nglStage) {
1496
- this.nglStage.autoView();
1497
- }
1498
- }
1499
-
1500
- toggleRepresentation() {
1501
- if (!this.nglStage) return;
1502
-
1503
- const components = this.nglStage.compList;
1504
- if (components.length === 0) return;
1505
-
1506
- const component = components[0];
1507
- component.removeAllRepresentations();
1508
-
1509
- if (this.currentRepresentation === 'cartoon') {
1510
- // Switch to ball and stick for everything
1511
- component.addRepresentation("ball+stick", {
1512
- color: "element",
1513
- radius: 0.15
1514
- });
1515
- this.currentRepresentation = 'ball+stick';
1516
- document.getElementById('style-text').textContent = 'Ball & Stick';
1517
- } else if (this.currentRepresentation === 'ball+stick') {
1518
- // Switch to surface (protein only) + ball&stick for others
1519
- component.addRepresentation("surface", {
1520
- sele: "protein",
1521
- colorScheme: "chainname",
1522
- opacity: 0.7
1523
- });
1524
-
1525
- // Add ball and stick for water molecules
1526
- if (this.currentProtein.waterMolecules > 0) {
1527
- component.addRepresentation("ball+stick", {
1528
- sele: "water",
1529
- color: "cyan",
1530
- colorScheme: "uniform",
1531
- radius: 0.1
1532
- });
1533
- }
1534
-
1535
- // Add ball and stick for ions
1536
- if (this.currentProtein.ions > 0) {
1537
- component.addRepresentation("ball+stick", {
1538
- sele: "ion",
1539
- color: "element",
1540
- radius: 0.2
1541
- });
1542
- }
1543
-
1544
- // Add ball and stick for ligands
1545
- if (this.currentProtein.ligands.length > 0) {
1546
- component.addRepresentation("ball+stick", {
1547
- sele: "hetero",
1548
- color: "element",
1549
- radius: 0.15
1550
- });
1551
- }
1552
-
1553
- this.currentRepresentation = 'surface';
1554
- document.getElementById('style-text').textContent = 'Surface';
1555
- } else {
1556
- // Switch back to mixed representation (protein ribbon + others ball&stick)
1557
- component.addRepresentation("cartoon", {
1558
- sele: "protein",
1559
- colorScheme: "chainname",
1560
- opacity: 0.8
1561
- });
1562
-
1563
- // Add ball and stick for water molecules
1564
- if (this.currentProtein.waterMolecules > 0) {
1565
- component.addRepresentation("ball+stick", {
1566
- sele: "water",
1567
- color: "cyan",
1568
- colorScheme: "uniform",
1569
- radius: 0.1
1570
- });
1571
- }
1572
-
1573
- // Add ball and stick for ions
1574
- if (this.currentProtein.ions > 0) {
1575
- component.addRepresentation("ball+stick", {
1576
- sele: "ion",
1577
- color: "element",
1578
- radius: 0.2
1579
- });
1580
- }
1581
-
1582
- // Add ball and stick for ligands
1583
- if (this.currentProtein.ligands.length > 0) {
1584
- component.addRepresentation("ball+stick", {
1585
- sele: "hetero",
1586
- color: "element",
1587
- radius: 0.15
1588
- });
1589
- }
1590
-
1591
- this.currentRepresentation = 'cartoon';
1592
- document.getElementById('style-text').textContent = 'Mixed View';
1593
- }
1594
- }
1595
-
1596
- toggleSpin() {
1597
- if (!this.nglStage) return;
1598
-
1599
- this.isSpinning = !this.isSpinning;
1600
- this.nglStage.setSpin(this.isSpinning);
1601
- }
1602
-
1603
- // Structure Preparation Methods
1604
- async prepareStructure() {
1605
- if (!this.currentProtein) {
1606
- alert('Please load a protein structure first');
1607
- return;
1608
- }
1609
-
1610
- // Get preparation options
1611
- const options = {
1612
- remove_water: document.getElementById('remove-water').checked,
1613
- remove_ions: document.getElementById('remove-ions').checked,
1614
- remove_hydrogens: document.getElementById('remove-hydrogens').checked,
1615
- add_nme: document.getElementById('add-nme').checked,
1616
- add_ace: document.getElementById('add-ace').checked,
1617
- preserve_ligands: document.getElementById('preserve-ligands').checked,
1618
- separate_ligands: document.getElementById('separate-ligands').checked,
1619
- selected_chains: this.getSelectedChains(),
1620
- selected_ligands: this.getSelectedLigands()
1621
- };
1622
-
1623
- // Show status
1624
- document.getElementById('prep-status').style.display = 'block';
1625
- document.getElementById('prep-status-content').innerHTML = `
1626
- <p><i class="fas fa-spinner fa-spin"></i> Preparing structure...</p>
1627
- `;
1628
-
1629
- try {
1630
- // Call Python backend
1631
- const response = await fetch('/api/prepare-structure', {
1632
- method: 'POST',
1633
- headers: {
1634
- 'Content-Type': 'application/json',
1635
- },
1636
- body: JSON.stringify({
1637
- pdb_content: this.currentProtein.content,
1638
- options: options
1639
- })
1640
- });
1641
-
1642
- const result = await response.json();
1643
-
1644
- if (result.success) {
1645
- // Store prepared structure
1646
- this.preparedProtein = {
1647
- content: result.prepared_structure,
1648
- original_atoms: result.original_atoms,
1649
- prepared_atoms: result.prepared_atoms,
1650
- removed_components: result.removed_components,
1651
- added_capping: result.added_capping,
1652
- preserved_ligands: result.preserved_ligands,
1653
- ligand_present: result.ligand_present,
1654
- separate_ligands: result.separate_ligands,
1655
- ligand_content: result.ligand_content || ''
1656
- };
1657
-
1658
- // Format removed components
1659
- const removedText = result.removed_components ?
1660
- Object.entries(result.removed_components)
1661
- .filter(([key, value]) => value > 0)
1662
- .map(([key, value]) => `${key}: ${value}`)
1663
- .join(', ') || 'None' : 'None';
1664
-
1665
- // Format added capping
1666
- const addedText = result.added_capping ?
1667
- Object.entries(result.added_capping)
1668
- .filter(([key, value]) => value > 0)
1669
- .map(([key, value]) => `${key}: ${value}`)
1670
- .join(', ') || 'None' : 'None';
1671
-
1672
- // Update status
1673
- document.getElementById('prep-status-content').innerHTML = `
1674
- <p><i class="fas fa-check-circle"></i> Structure preparation completed!</p>
1675
- <p><strong>Original atoms:</strong> ${result.original_atoms.toLocaleString()}</p>
1676
- <p><strong>Prepared atoms:</strong> ${result.prepared_atoms.toLocaleString()}</p>
1677
- <p><strong>Removed:</strong> ${removedText}</p>
1678
- <p><strong>Added:</strong> ${addedText}</p>
1679
- <p><strong>Ligands:</strong> ${result.preserved_ligands}</p>
1680
- <p>Ready for AMBER force field generation!</p>
1681
- `;
1682
-
1683
- // Enable preview and download buttons
1684
- document.getElementById('preview-prepared').disabled = false;
1685
- document.getElementById('download-prepared').disabled = false;
1686
-
1687
- // Enable ligand download button if ligands are present and separate ligands is checked
1688
- const separateLigandsChecked = document.getElementById('separate-ligands').checked;
1689
- const downloadLigandBtn = document.getElementById('download-ligand');
1690
- if (result.ligand_present && separateLigandsChecked && result.ligand_content) {
1691
- downloadLigandBtn.disabled = false;
1692
- downloadLigandBtn.classList.remove('btn-outline-secondary');
1693
- downloadLigandBtn.classList.add('btn-outline-primary');
1694
- } else {
1695
- downloadLigandBtn.disabled = true;
1696
- downloadLigandBtn.classList.remove('btn-outline-primary');
1697
- downloadLigandBtn.classList.add('btn-outline-secondary');
1698
- }
1699
-
1700
- // Show ligand force field group if preserve ligands is checked
1701
- const preserveLigandsChecked = document.getElementById('preserve-ligands').checked;
1702
- if (preserveLigandsChecked && result.ligand_present) {
1703
- this.toggleLigandForceFieldGroup(true);
1704
- }
1705
- } else {
1706
- throw new Error(result.error || 'Structure preparation failed');
1707
- }
1708
- } catch (error) {
1709
- console.error('Error preparing structure:', error);
1710
- document.getElementById('prep-status-content').innerHTML = `
1711
- <p><i class="fas fa-exclamation-triangle"></i> Error preparing structure</p>
1712
- <p>${error.message}</p>
1713
- `;
1714
- }
1715
- }
1716
-
1717
- renderChainAndLigandSelections() {
1718
- if (!this.currentProtein) return;
1719
- // Render chains
1720
- const chainContainer = document.getElementById('chain-selection');
1721
- if (chainContainer) {
1722
- chainContainer.innerHTML = '';
1723
- this.currentProtein.chains.forEach(chainId => {
1724
- const id = `chain-${chainId}`;
1725
- const wrapper = document.createElement('div');
1726
- wrapper.className = 'checkbox-inline';
1727
- wrapper.innerHTML = `
1728
- <label class="checkbox-container">
1729
- <input type="checkbox" id="${id}" data-chain="${chainId}">
1730
- <span class="checkmark"></span>
1731
- Chain ${chainId}
1732
- </label>`;
1733
- chainContainer.appendChild(wrapper);
1734
- });
1735
- }
1736
-
1737
- // Render ligands (RESN-CHAIN groups)
1738
- const ligandContainer = document.getElementById('ligand-selection');
1739
- if (ligandContainer) {
1740
- ligandContainer.innerHTML = '';
1741
- if (Array.isArray(this.currentProtein.ligandGroups) && this.currentProtein.ligandGroups.length > 0) {
1742
- this.currentProtein.ligandGroups.forEach(l => {
1743
- const key = `${l.resn}-${l.chain}`;
1744
- const id = `lig-${key}`;
1745
- const wrapper = document.createElement('div');
1746
- wrapper.className = 'checkbox-inline';
1747
- wrapper.innerHTML = `
1748
- <label class="checkbox-container">
1749
- <input type="checkbox" id="${id}" data-resn="${l.resn}" data-chain="${l.chain}">
1750
- <span class="checkmark"></span>
1751
- ${key}
1752
- </label>`;
1753
- ligandContainer.appendChild(wrapper);
1754
- });
1755
- } else {
1756
- // Fallback: show unique ligand names if detailed positions not parsed
1757
- if (Array.isArray(this.currentProtein.ligands) && this.currentProtein.ligands.length > 0) {
1758
- this.currentProtein.ligands.forEach(resn => {
1759
- const id = `lig-${resn}`;
1760
- const wrapper = document.createElement('div');
1761
- wrapper.className = 'checkbox-inline';
1762
- wrapper.innerHTML = `
1763
- <label class="checkbox-container">
1764
- <input type="checkbox" id="${id}" data-resn="${resn}">
1765
- <span class="checkmark"></span>
1766
- ${resn}
1767
- </label>`;
1768
- ligandContainer.appendChild(wrapper);
1769
- });
1770
- } else {
1771
- ligandContainer.innerHTML = '<small>No ligands detected</small>';
1772
- }
1773
- }
1774
- }
1775
- }
1776
-
1777
- getSelectedChains() {
1778
- const container = document.getElementById('chain-selection');
1779
- if (!container) return [];
1780
- return Array.from(container.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.getAttribute('data-chain'));
1781
- }
1782
-
1783
- getSelectedLigands() {
1784
- const container = document.getElementById('ligand-selection');
1785
- if (!container) return [];
1786
- return Array.from(container.querySelectorAll('input[type="checkbox"]:checked')).map(cb => ({
1787
- resn: cb.getAttribute('data-resn') || '',
1788
- chain: cb.getAttribute('data-chain') || ''
1789
- }));
1790
- }
1791
-
1792
- previewPreparedStructure() {
1793
- if (!this.preparedProtein) {
1794
- alert('Please prepare a protein structure first');
1795
- return;
1796
- }
1797
-
1798
- // Show prepared structure preview
1799
- document.getElementById('prepared-structure-preview').style.display = 'block';
1800
-
1801
- // Format removed components
1802
- const removedText = this.preparedProtein.removed_components ?
1803
- Object.entries(this.preparedProtein.removed_components)
1804
- .filter(([key, value]) => value > 0)
1805
- .map(([key, value]) => `${key}: ${value}`)
1806
- .join(', ') || 'None' : 'None';
1807
-
1808
- // Format added capping
1809
- const addedText = this.preparedProtein.added_capping ?
1810
- Object.entries(this.preparedProtein.added_capping)
1811
- .filter(([key, value]) => value > 0)
1812
- .map(([key, value]) => `${key}: ${value}`)
1813
- .join(', ') || 'None' : 'None';
1814
-
1815
- // Update structure info
1816
- document.getElementById('original-atoms').textContent = this.preparedProtein.original_atoms.toLocaleString();
1817
- document.getElementById('prepared-atoms').textContent = this.preparedProtein.prepared_atoms.toLocaleString();
1818
- document.getElementById('removed-components').textContent = removedText;
1819
- document.getElementById('added-capping').textContent = addedText;
1820
- document.getElementById('preserved-ligands').textContent = this.preparedProtein.preserved_ligands;
1821
-
1822
- // Load 3D visualization of prepared structure
1823
- this.loadPrepared3DVisualization();
1824
- }
1825
-
1826
- downloadPreparedStructure() {
1827
- if (!this.preparedProtein) {
1828
- alert('Please prepare a structure first');
1829
- return;
1830
- }
1831
-
1832
- // Download prepared structure
1833
- const blob = new Blob([this.preparedProtein.content], { type: 'text/plain' });
1834
- const url = URL.createObjectURL(blob);
1835
- const a = document.createElement('a');
1836
- a.href = url;
1837
- a.download = `tleap_ready.pdb`;
1838
- document.body.appendChild(a);
1839
- a.click();
1840
- document.body.removeChild(a);
1841
- URL.revokeObjectURL(url);
1842
- }
1843
-
1844
- downloadLigandFile() {
1845
- if (!this.preparedProtein || !this.preparedProtein.ligand_present || !this.preparedProtein.ligand_content) {
1846
- alert('No ligand file available. Please prepare structure with separate ligands enabled.');
1847
- return;
1848
- }
1849
-
1850
- // Download ligand file
1851
- const ligandBlob = new Blob([this.preparedProtein.ligand_content], { type: 'text/plain' });
1852
- const ligandUrl = URL.createObjectURL(ligandBlob);
1853
- const ligandA = document.createElement('a');
1854
- ligandA.href = ligandUrl;
1855
- ligandA.download = `4_ligands_corrected.pdb`;
1856
- document.body.appendChild(ligandA);
1857
- ligandA.click();
1858
- document.body.removeChild(ligandA);
1859
- URL.revokeObjectURL(ligandUrl);
1860
- }
1861
-
1862
- // 3D Visualization for prepared structure
1863
- async loadPrepared3DVisualization() {
1864
- if (!this.preparedProtein) return;
1865
-
1866
- try {
1867
- // Initialize NGL stage for prepared structure if not already done
1868
- if (!this.preparedNglStage) {
1869
- this.preparedNglStage = new NGL.Stage("prepared-ngl-viewer", {
1870
- backgroundColor: "white",
1871
- quality: "medium"
1872
- });
1873
- }
1874
-
1875
- // Clear existing components
1876
- this.preparedNglStage.removeAllComponents();
1877
-
1878
- // Create a blob from prepared PDB content
1879
- const blob = new Blob([this.preparedProtein.content], { type: 'text/plain' });
1880
- const url = URL.createObjectURL(blob);
1881
-
1882
- // Load the prepared structure
1883
- const component = await this.preparedNglStage.loadFile(url, {
1884
- ext: "pdb",
1885
- defaultRepresentation: false
1886
- });
1887
-
1888
- // Add cartoon representation for protein with chain-based colors
1889
- component.addRepresentation("cartoon", {
1890
- sele: "protein",
1891
- colorScheme: "chainname",
1892
- opacity: 0.9
1893
- });
1894
-
1895
- // Add ball and stick for ligands (if any) - check for HETATM records
1896
- component.addRepresentation("ball+stick", {
1897
- sele: "hetero",
1898
- color: "element",
1899
- radius: 0.2,
1900
- opacity: 0.8
1901
- });
1902
-
1903
- // Auto-fit the view
1904
- this.preparedNglStage.autoView();
1905
-
1906
- // Show controls
1907
- document.getElementById('prepared-viewer-controls').style.display = 'flex';
1908
-
1909
- // Clean up the blob URL
1910
- URL.revokeObjectURL(url);
1911
-
1912
- } catch (error) {
1913
- console.error('Error loading prepared 3D visualization:', error);
1914
- }
1915
- }
1916
-
1917
- resetPreparedView() {
1918
- if (this.preparedNglStage) {
1919
- this.preparedNglStage.autoView();
1920
- }
1921
- }
1922
-
1923
- togglePreparedRepresentation() {
1924
- if (!this.preparedNglStage) return;
1925
-
1926
- const components = this.preparedNglStage.compList;
1927
- if (components.length === 0) return;
1928
-
1929
- const component = components[0];
1930
- component.removeAllRepresentations();
1931
-
1932
- if (this.preparedRepresentation === 'cartoon') {
1933
- // Switch to ball and stick
1934
- component.addRepresentation("ball+stick", {
1935
- color: "element",
1936
- radius: 0.15
1937
- });
1938
- this.preparedRepresentation = 'ball+stick';
1939
- document.getElementById('prepared-style-text').textContent = 'Ball & Stick';
1940
- } else if (this.preparedRepresentation === 'ball+stick') {
1941
- // Switch to surface
1942
- component.addRepresentation("surface", {
1943
- sele: "protein",
1944
- colorScheme: "chainname",
1945
- opacity: 0.7
1946
- });
1947
- this.preparedRepresentation = 'surface';
1948
- document.getElementById('prepared-style-text').textContent = 'Surface';
1949
- } else {
1950
- // Switch back to cartoon
1951
- component.addRepresentation("cartoon", {
1952
- sele: "protein",
1953
- colorScheme: "chainname",
1954
- opacity: 0.8
1955
- });
1956
-
1957
- // Add ball and stick for ligands
1958
- if (this.preparedProtein.preserved_ligands !== 'None') {
1959
- component.addRepresentation("ball+stick", {
1960
- sele: "hetero",
1961
- color: "element",
1962
- radius: 0.15
1963
- });
1964
- }
1965
-
1966
- this.preparedRepresentation = 'cartoon';
1967
- document.getElementById('prepared-style-text').textContent = 'Mixed View';
1968
- }
1969
- }
1970
-
1971
- togglePreparedSpin() {
1972
- if (!this.preparedNglStage) return;
1973
-
1974
- this.preparedIsSpinning = !this.preparedIsSpinning;
1975
- this.preparedNglStage.setSpin(this.preparedIsSpinning);
1976
- }
1977
- }
1978
-
1979
- // Initialize the application when the page loads
1980
- function initializeApp() {
1981
- console.log('Initializing mdPipeline...'); // Debug log
1982
- window.mdPipeline = new MDSimulationPipeline();
1983
- console.log('mdPipeline initialized:', window.mdPipeline); // Debug log
1984
- }
1985
-
1986
- // Try to initialize immediately if DOM is already loaded
1987
- if (document.readyState === 'loading') {
1988
- document.addEventListener('DOMContentLoaded', initializeApp);
1989
- } else {
1990
- // DOM is already loaded
1991
- initializeApp();
1992
- }
1993
-
1994
- // Add some utility functions for better UX
1995
- function formatNumber(num) {
1996
- return num.toLocaleString();
1997
- }
1998
-
1999
- function formatTime(seconds) {
2000
- const hours = Math.floor(seconds / 3600);
2001
- const minutes = Math.floor((seconds % 3600) / 60);
2002
- const secs = seconds % 60;
2003
- return `${hours}h ${minutes}m ${secs}s`;
2004
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
python/__pycache__/app.cpython-310.pyc DELETED
Binary file (37.7 kB)
 
python/__pycache__/app.cpython-312.pyc DELETED
Binary file (54.5 kB)
 
python/__pycache__/structure_preparation.cpython-310.pyc DELETED
Binary file (21.5 kB)
 
python/app.py DELETED
@@ -1,1706 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- MD Simulation Pipeline - Flask Backend
4
- Provides API endpoints for protein processing and file generation
5
- """
6
-
7
- from flask import Flask, request, jsonify, send_file, render_template, send_from_directory
8
- from flask_cors import CORS
9
- import os
10
- import json
11
- import tempfile
12
- import zipfile
13
- from pathlib import Path
14
- import requests
15
- import subprocess
16
- from Bio.PDB import PDBParser, PDBList
17
- import logging
18
- from structure_preparation import prepare_structure, parse_structure_info
19
-
20
- app = Flask(__name__,
21
- template_folder='../html',
22
- static_folder='../',
23
- static_url_path='')
24
- CORS(app)
25
-
26
- # Configure logging
27
- logging.basicConfig(level=logging.INFO)
28
- logger = logging.getLogger(__name__)
29
-
30
- # Create output directory
31
- OUTPUT_DIR = Path(__file__).parent.parent / "output"
32
-
33
- def clean_and_create_output_folder():
34
- """Clean existing output folder and create a new one"""
35
- try:
36
- print(f"DEBUG: Starting cleanup. OUTPUT_DIR = {OUTPUT_DIR}")
37
- print(f"DEBUG: OUTPUT_DIR.exists() = {OUTPUT_DIR.exists()}")
38
-
39
- # Remove existing output folder if it exists
40
- if OUTPUT_DIR.exists():
41
- import shutil
42
- print(f"DEBUG: Removing existing output folder: {OUTPUT_DIR}")
43
- shutil.rmtree(OUTPUT_DIR)
44
- print(f"DEBUG: Successfully removed output folder")
45
- logger.info(f"Removed existing output folder: {OUTPUT_DIR}")
46
-
47
- # Create new output folder
48
- print(f"DEBUG: Creating new output folder: {OUTPUT_DIR}")
49
- OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
50
- print(f"DEBUG: Successfully created output folder")
51
- logger.info(f"Created new output folder: {OUTPUT_DIR}")
52
-
53
- return True
54
- except Exception as e:
55
- print(f"DEBUG: Error in cleanup: {str(e)}")
56
- logger.error(f"Error cleaning output folder: {str(e)}")
57
- return False
58
-
59
- class MDSimulationGenerator:
60
- """Handles MD simulation file generation and protein processing"""
61
-
62
- def __init__(self):
63
- self.pdb_parser = PDBParser(QUIET=True)
64
- self.pdb_list = PDBList()
65
-
66
- def fetch_pdb_structure(self, pdb_id):
67
- """Fetch PDB structure from RCSB"""
68
- try:
69
- # Download PDB file
70
- pdb_file = self.pdb_list.retrieve_pdb_file(pdb_id, pdir=OUTPUT_DIR, file_format='pdb')
71
- return str(pdb_file)
72
- except Exception as e:
73
- logger.error(f"Error fetching PDB {pdb_id}: {str(e)}")
74
- raise
75
-
76
- def parse_pdb_structure(self, pdb_file):
77
- """Parse PDB file and extract structure information"""
78
- try:
79
- structure = self.pdb_parser.get_structure('protein', pdb_file)
80
-
81
- # Extract basic information
82
- atom_count = 0
83
- chains = set()
84
- residues = set()
85
-
86
- for model in structure:
87
- for chain in model:
88
- chains.add(chain.id)
89
- for residue in chain:
90
- if residue.id[0] == ' ': # Standard residues
91
- residues.add(f"{residue.resname}{residue.id[1]}")
92
- for atom in residue:
93
- atom_count += 1
94
-
95
- return {
96
- 'atom_count': atom_count,
97
- 'chains': list(chains),
98
- 'residue_count': len(residues),
99
- 'structure_id': Path(pdb_file).stem.upper()
100
- }
101
- except Exception as e:
102
- logger.error(f"Error parsing PDB file: {str(e)}")
103
- raise
104
-
105
- def generate_mdp_file(self, params, step_type='production'):
106
- """Generate GROMACS MDP file for different simulation steps"""
107
-
108
- if step_type == 'restrained_min':
109
- return f"""; Restrained Minimization Parameters
110
- integrator = steep
111
- nsteps = {params['steps']['restrainedMin']['steps']}
112
- emstep = 0.01
113
- emtol = 1000
114
-
115
- ; Position restraints
116
- define = -DPOSRES
117
- refcoord_scaling = com
118
-
119
- ; Output control
120
- nstxout = 100
121
- nstenergy = 100
122
- nstlog = 100
123
-
124
- ; Bond parameters
125
- constraint_algorithm = lincs
126
- constraints = h-bonds
127
-
128
- ; Neighbor searching
129
- cutoff-scheme = Verlet
130
- ns_type = grid
131
- nstlist = 10
132
- rlist = {params['cutoff']}
133
-
134
- ; Electrostatics
135
- coulombtype = PME
136
- rcoulomb = {params['cutoff']}
137
- pme_order = {params['pmeOrder']}
138
-
139
- ; Van der Waals
140
- vdwtype = Cut-off
141
- rvdw = {params['cutoff']}
142
- """
143
-
144
- elif step_type == 'minimization':
145
- return f"""; Minimization Parameters
146
- integrator = {params['steps']['minimization']['algorithm']}
147
- nsteps = {params['steps']['minimization']['steps']}
148
- emstep = 0.01
149
- emtol = 1000
150
-
151
- ; Output control
152
- nstxout = 100
153
- nstenergy = 100
154
- nstlog = 100
155
-
156
- ; Bond parameters
157
- constraint_algorithm = lincs
158
- constraints = h-bonds
159
-
160
- ; Neighbor searching
161
- cutoff-scheme = Verlet
162
- ns_type = grid
163
- nstlist = 10
164
- rlist = {params['cutoff']}
165
-
166
- ; Electrostatics
167
- coulombtype = PME
168
- rcoulomb = {params['cutoff']}
169
- pme_order = {params['pmeOrder']}
170
-
171
- ; Van der Waals
172
- vdwtype = Cut-off
173
- rvdw = {params['cutoff']}
174
- """
175
-
176
- elif step_type == 'nvt':
177
- return f"""; NVT Equilibration Parameters
178
- integrator = md
179
- dt = {params['timestep']}
180
- nsteps = {params['steps']['nvt']['steps']}
181
-
182
- ; Output control
183
- nstxout = 5000
184
- nstvout = 5000
185
- nstenergy = 1000
186
- nstlog = 1000
187
-
188
- ; Bond parameters
189
- constraint_algorithm = lincs
190
- constraints = h-bonds
191
- lincs_iter = 1
192
- lincs_order = 4
193
-
194
- ; Neighbor searching
195
- cutoff-scheme = Verlet
196
- ns_type = grid
197
- nstlist = 40
198
- rlist = {params['cutoff']}
199
-
200
- ; Electrostatics
201
- coulombtype = PME
202
- rcoulomb = {params['cutoff']}
203
- pme_order = {params['pmeOrder']}
204
-
205
- ; Van der Waals
206
- vdwtype = Cut-off
207
- rvdw = {params['cutoff']}
208
-
209
- ; Temperature coupling
210
- tcoupl = {params['couplingType']}
211
- tc-grps = Protein Non-Protein
212
- tau_t = 0.1 0.1
213
- ref_t = {params['steps']['nvt']['temperature']} {params['steps']['nvt']['temperature']}
214
-
215
- ; Pressure coupling (disabled for NVT)
216
- pcoupl = no
217
-
218
- ; Velocity generation
219
- gen_vel = yes
220
- gen_temp = {params['steps']['nvt']['temperature']}
221
- gen_seed = -1
222
- """
223
-
224
- elif step_type == 'npt':
225
- return f"""; NPT Equilibration Parameters
226
- integrator = md
227
- dt = {params['timestep']}
228
- nsteps = {params['steps']['npt']['steps']}
229
-
230
- ; Output control
231
- nstxout = 5000
232
- nstvout = 5000
233
- nstenergy = 1000
234
- nstlog = 1000
235
-
236
- ; Bond parameters
237
- constraint_algorithm = lincs
238
- constraints = h-bonds
239
- lincs_iter = 1
240
- lincs_order = 4
241
-
242
- ; Neighbor searching
243
- cutoff-scheme = Verlet
244
- ns_type = grid
245
- nstlist = 40
246
- rlist = {params['cutoff']}
247
-
248
- ; Electrostatics
249
- coulombtype = PME
250
- rcoulomb = {params['cutoff']}
251
- pme_order = {params['pmeOrder']}
252
-
253
- ; Van der Waals
254
- vdwtype = Cut-off
255
- rvdw = {params['cutoff']}
256
-
257
- ; Temperature coupling
258
- tcoupl = {params['couplingType']}
259
- tc-grps = Protein Non-Protein
260
- tau_t = 0.1 0.1
261
- ref_t = {params['steps']['npt']['temperature']} {params['steps']['npt']['temperature']}
262
-
263
- ; Pressure coupling
264
- pcoupl = {params['couplingType']}
265
- pcoupltype = isotropic
266
- tau_p = 2.0
267
- ref_p = {params['steps']['npt']['pressure']}
268
- compressibility = 4.5e-5
269
-
270
- ; Velocity generation
271
- gen_vel = no
272
- """
273
-
274
- else: # production
275
- return f"""; MD Simulation Parameters
276
- ; Generated by MD Simulation Pipeline
277
-
278
- ; Run parameters
279
- integrator = md
280
- dt = {params['timestep']}
281
- nsteps = {params['steps']['production']['steps']}
282
-
283
- ; Output control
284
- nstxout = 5000
285
- nstvout = 5000
286
- nstenergy = 1000
287
- nstlog = 1000
288
-
289
- ; Bond parameters
290
- constraint_algorithm = lincs
291
- constraints = h-bonds
292
- lincs_iter = 1
293
- lincs_order = 4
294
-
295
- ; Neighbor searching
296
- cutoff-scheme = Verlet
297
- ns_type = grid
298
- nstlist = 40
299
- rlist = {params['cutoff']}
300
-
301
- ; Electrostatics
302
- coulombtype = PME
303
- rcoulomb = {params['cutoff']}
304
- pme_order = {params['pmeOrder']}
305
- fourierspacing = 0.16
306
-
307
- ; Van der Waals
308
- vdwtype = Cut-off
309
- rvdw = {params['cutoff']}
310
-
311
- ; Temperature coupling
312
- tcoupl = {params['couplingType']}
313
- tc-grps = Protein Non-Protein
314
- tau_t = 0.1 0.1
315
- ref_t = {params['temperature']} {params['temperature']}
316
-
317
- ; Pressure coupling
318
- pcoupl = {params['couplingType']}
319
- pcoupltype = isotropic
320
- tau_p = 2.0
321
- ref_p = {params['pressure']}
322
- compressibility = 4.5e-5
323
-
324
- ; Dispersion correction
325
- DispCorr = EnerPres
326
-
327
- ; Velocity generation
328
- gen_vel = yes
329
- gen_temp = {params['temperature']}
330
- gen_seed = -1
331
- """
332
-
333
- def generate_pbs_script(self, protein_name, params):
334
- """Generate PBS script for HPC submission"""
335
- total_steps = params['steps']['production']['steps']
336
- time_in_ns = (total_steps * params['timestep']) / 1000
337
-
338
- return f"""#!/bin/bash
339
- #PBS -N {protein_name}_md
340
- #PBS -l nodes=1:ppn=16
341
- #PBS -l walltime=24:00:00
342
- #PBS -q normal
343
- #PBS -j oe
344
-
345
- # Change to the directory where the job was submitted
346
- cd $PBS_O_WORKDIR
347
-
348
- # Load required modules
349
- module load gromacs/2023.2
350
- module load intel/2021.4.0
351
-
352
- # Set up environment
353
- export OMP_NUM_THREADS=16
354
- export GMX_MAXBACKUP=-1
355
-
356
- # Simulation parameters
357
- PROTEIN={protein_name}
358
- STEPS={total_steps}
359
- TIME_NS={time_in_ns:.2f}
360
-
361
- echo "Starting MD simulation for $PROTEIN"
362
- echo "Total simulation time: $TIME_NS ns"
363
- echo "Job started at: $(date)"
364
-
365
- # Run the simulation
366
- ./run_simulation.sh $PROTEIN
367
-
368
- echo "Simulation completed at: $(date)"
369
- echo "Results saved in output directory"
370
- """
371
-
372
- def generate_setup_script(self, protein_name, params):
373
- """Generate setup script for MD simulation"""
374
- return f"""#!/bin/bash
375
- # Setup script for {protein_name} MD simulation
376
- # Generated by MD Simulation Pipeline
377
-
378
- set -e
379
-
380
- PROTEIN={protein_name}
381
- FORCE_FIELD={params['forceField']}
382
- WATER_MODEL={params['waterModel']}
383
-
384
- echo "Setting up MD simulation for $PROTEIN"
385
-
386
- # Create output directory
387
- mkdir -p output
388
-
389
- # 1. Prepare protein structure
390
- echo "Preparing protein structure..."
391
- gmx pdb2gmx -f $PROTEIN.pdb -o $PROTEIN_processed.gro -p $PROTEIN.top -ff $FORCE_FIELD -water $WATER_MODEL
392
-
393
- # 2. Define simulation box
394
- echo "Defining simulation box..."
395
- gmx editconf -f $PROTEIN_processed.gro -o $PROTEIN_box.gro -c -d {params['boxMargin']} -bt {params['boxType']}
396
-
397
- # 3. Add solvent
398
- echo "Adding solvent..."
399
- gmx solvate -cp $PROTEIN_box.gro -cs spc216.gro -o $PROTEIN_solv.gro -p $PROTEIN.top
400
-
401
- # 4. Add ions
402
- echo "Adding ions..."
403
- gmx grompp -f $PROTEIN_restrained.mdp -c $PROTEIN_solv.gro -p $PROTEIN.top -o $PROTEIN_ions.tpr
404
- echo "SOL" | gmx genion -s $PROTEIN_ions.tpr -o $PROTEIN_final.gro -p $PROTEIN.top -pname NA -nname CL -neutral
405
-
406
- echo "Setup completed successfully!"
407
- echo "Ready to run simulation with: ./run_simulation.sh $PROTEIN"
408
- """
409
-
410
- def generate_analysis_script(self, protein_name):
411
- """Generate analysis script for MD simulation results"""
412
- return f"""#!/bin/bash
413
- # Analysis script for {protein_name} MD simulation
414
- # Generated by MD Simulation Pipeline
415
-
416
- PROTEIN={protein_name}
417
-
418
- echo "Analyzing MD simulation results for $PROTEIN"
419
-
420
- # Create analysis directory
421
- mkdir -p analysis
422
-
423
- # 1. RMSD analysis
424
- echo "Calculating RMSD..."
425
- echo "Protein" | gmx rms -s $PROTEIN_final.tpr -f $PROTEIN_prod.xtc -o analysis/$PROTEIN_rmsd.xvg -tu ns
426
-
427
- # 2. RMSF analysis
428
- echo "Calculating RMSF..."
429
- echo "Protein" | gmx rmsf -s $PROTEIN_final.tpr -f $PROTEIN_prod.xtc -o analysis/$PROTEIN_rmsf.xvg -res
430
-
431
- # 3. Radius of gyration
432
- echo "Calculating radius of gyration..."
433
- echo "Protein" | gmx gyrate -s $PROTEIN_final.tpr -f $PROTEIN_prod.xtc -o analysis/$PROTEIN_gyrate.xvg
434
-
435
- # 4. Hydrogen bonds
436
- echo "Analyzing hydrogen bonds..."
437
- echo "Protein" | gmx hbond -s $PROTEIN_final.tpr -f $PROTEIN_prod.xtc -num analysis/$PROTEIN_hbonds.xvg
438
-
439
- # 5. Energy analysis
440
- echo "Analyzing energies..."
441
- gmx energy -f $PROTEIN_prod.edr -o analysis/$PROTEIN_energy.xvg
442
-
443
- # 6. Generate plots
444
- echo "Generating analysis plots..."
445
- python3 plot_analysis.py $PROTEIN
446
-
447
- echo "Analysis completed! Results saved in analysis/ directory"
448
- """
449
-
450
- # Initialize the MD simulation generator
451
- md_generator = MDSimulationGenerator()
452
-
453
- @app.route('/api/fetch-pdb', methods=['POST'])
454
- def fetch_pdb():
455
- """Fetch PDB structure from RCSB"""
456
- try:
457
- print("DEBUG: fetch-pdb endpoint called")
458
- data = request.get_json()
459
- pdb_id = data.get('pdb_id', '').upper()
460
- print(f"DEBUG: pdb_id = {pdb_id}")
461
-
462
- if not pdb_id or len(pdb_id) != 4:
463
- return jsonify({'error': 'Invalid PDB ID'}), 400
464
-
465
- # Clean and create new output folder for fresh start
466
- print("DEBUG: Calling clean_and_create_output_folder()")
467
- if not clean_and_create_output_folder():
468
- return jsonify({'error': 'Failed to clean output folder'}), 500
469
- print("DEBUG: Output folder cleanup completed successfully")
470
-
471
- # Fetch PDB structure
472
- pdb_file = md_generator.fetch_pdb_structure(pdb_id)
473
-
474
- # Parse structure information
475
- structure_info = md_generator.parse_pdb_structure(pdb_file)
476
-
477
- return jsonify({
478
- 'success': True,
479
- 'structure_info': structure_info,
480
- 'pdb_file': pdb_file
481
- })
482
-
483
- except Exception as e:
484
- logger.error(f"Error fetching PDB: {str(e)}")
485
- return jsonify({'error': str(e)}), 500
486
-
487
- @app.route('/api/parse-pdb', methods=['POST'])
488
- def parse_pdb():
489
- """Parse uploaded PDB file"""
490
- try:
491
- print("DEBUG: parse-pdb endpoint called")
492
- if 'file' not in request.files:
493
- return jsonify({'error': 'No file uploaded'}), 400
494
-
495
- file = request.files['file']
496
- if file.filename == '':
497
- return jsonify({'error': 'No file selected'}), 400
498
-
499
- print(f"DEBUG: Processing uploaded file: {file.filename}")
500
-
501
- # Clean and create new output folder for fresh start
502
- print("DEBUG: Calling clean_and_create_output_folder()")
503
- if not clean_and_create_output_folder():
504
- return jsonify({'error': 'Failed to clean output folder'}), 500
505
- print("DEBUG: Output folder cleanup completed successfully")
506
-
507
- # Save uploaded file temporarily
508
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pdb')
509
- file.save(temp_file.name)
510
-
511
- # Parse structure information
512
- structure_info = md_generator.parse_pdb_structure(temp_file.name)
513
-
514
- # Clean up temporary file
515
- os.unlink(temp_file.name)
516
-
517
- return jsonify({
518
- 'success': True,
519
- 'structure_info': structure_info
520
- })
521
-
522
- except Exception as e:
523
- logger.error(f"Error parsing PDB: {str(e)}")
524
- return jsonify({'error': str(e)}), 500
525
-
526
- @app.route('/api/generate-files', methods=['POST'])
527
- def generate_files():
528
- """Generate MD simulation files"""
529
- try:
530
- data = request.get_json()
531
- protein_name = data.get('protein_name', 'protein')
532
- simulation_params = data.get('simulation_params', {})
533
-
534
- # Generate all files
535
- files = {}
536
-
537
- # MDP files
538
- files[f'{protein_name}.mdp'] = md_generator.generate_mdp_file(simulation_params, 'production')
539
- files[f'{protein_name}_restrained.mdp'] = md_generator.generate_mdp_file(simulation_params, 'restrained_min')
540
- files[f'{protein_name}_min.mdp'] = md_generator.generate_mdp_file(simulation_params, 'minimization')
541
- files[f'{protein_name}_nvt.mdp'] = md_generator.generate_mdp_file(simulation_params, 'nvt')
542
- files[f'{protein_name}_npt.mdp'] = md_generator.generate_mdp_file(simulation_params, 'npt')
543
- files[f'{protein_name}_prod.mdp'] = md_generator.generate_mdp_file(simulation_params, 'production')
544
-
545
- # Scripts
546
- files[f'{protein_name}_simulation.pbs'] = md_generator.generate_pbs_script(protein_name, simulation_params)
547
- files[f'setup_{protein_name}.sh'] = md_generator.generate_setup_script(protein_name, simulation_params)
548
- files[f'analyze_{protein_name}.sh'] = md_generator.generate_analysis_script(protein_name)
549
-
550
- return jsonify({
551
- 'success': True,
552
- 'files': files
553
- })
554
-
555
- except Exception as e:
556
- logger.error(f"Error generating files: {str(e)}")
557
- return jsonify({'error': str(e)}), 500
558
-
559
- @app.route('/api/download-zip', methods=['POST'])
560
- def download_zip():
561
- """Download all generated files as a ZIP archive"""
562
- try:
563
- data = request.get_json()
564
- files = data.get('files', {})
565
-
566
- # Create temporary ZIP file
567
- temp_zip = tempfile.NamedTemporaryFile(delete=False, suffix='.zip')
568
-
569
- with zipfile.ZipFile(temp_zip.name, 'w') as zip_file:
570
- for filename, content in files.items():
571
- zip_file.writestr(filename, content)
572
-
573
- return send_file(
574
- temp_zip.name,
575
- as_attachment=True,
576
- download_name='md_simulation_files.zip',
577
- mimetype='application/zip'
578
- )
579
-
580
- except Exception as e:
581
- logger.error(f"Error creating ZIP file: {str(e)}")
582
- return jsonify({'error': str(e)}), 500
583
-
584
- @app.route('/api/get-solvated-protein', methods=['GET'])
585
- def get_solvated_protein():
586
- """Get the solvated protein PDB file content"""
587
- try:
588
- solvated_file = os.path.join(OUTPUT_DIR, 'protein_solvated.pdb')
589
-
590
- if not os.path.exists(solvated_file):
591
- return jsonify({'success': False, 'error': 'Solvated protein file not found. Please generate files first.'})
592
-
593
- with open(solvated_file, 'r') as f:
594
- content = f.read()
595
-
596
- return jsonify({'success': True, 'content': content})
597
- except Exception as e:
598
- logger.error(f"Error reading solvated protein file: {str(e)}")
599
- return jsonify({'success': False, 'error': str(e)})
600
-
601
- @app.route('/api/get-viewer-pdb', methods=['GET'])
602
- def get_viewer_pdb():
603
- """Return a single PDB for viewer: start from protein_solvated.pdb and mark ligand residues as HETATM.
604
- Ligand residues are detected from 4_ligands_corrected.pdb by (resname, chain, resi) tuples; if chains/resi not present, fallback to resname matching.
605
- """
606
- try:
607
- solvated_path = OUTPUT_DIR / 'protein_solvated.pdb'
608
- lig_path = OUTPUT_DIR / '4_ligands_corrected.pdb'
609
- viewer_out = OUTPUT_DIR / 'viewer_protein_with_ligand.pdb'
610
-
611
- if not solvated_path.exists():
612
- return jsonify({'success': False, 'error': 'protein_solvated.pdb not found'}), 400
613
-
614
- # Build ligand index from corrected ligand PDB if present
615
- ligand_keys = set()
616
- ligand_resnames = set()
617
- if lig_path.exists():
618
- with open(lig_path, 'r') as lf:
619
- for line in lf:
620
- if line.startswith(('ATOM', 'HETATM')):
621
- resn = line[17:20].strip()
622
- chain = line[21:22].strip()
623
- resi = line[22:26].strip()
624
- ligand_resnames.add(resn)
625
- if chain and resi:
626
- ligand_keys.add((resn, chain, resi))
627
-
628
- # Rewrite solvated file marking matching ligand residues and ions (NA/CL) as HETATM
629
- out_lines = []
630
- with open(solvated_path, 'r') as sf:
631
- for line in sf:
632
- if line.startswith(('ATOM', 'HETATM')):
633
- resn = line[17:20].strip()
634
- chain = line[21:22].strip()
635
- resi = line[22:26].strip()
636
- is_match = False
637
- is_ion = resn in { 'NA', 'CL' }
638
- if (resn, chain, resi) in ligand_keys:
639
- is_match = True
640
- elif resn in ligand_resnames:
641
- # Fallback by residue name only
642
- is_match = True
643
- if is_match or is_ion:
644
- # Force to HETATM
645
- out_lines.append('HETATM' + line[6:])
646
- else:
647
- out_lines.append(line)
648
- else:
649
- out_lines.append(line)
650
-
651
- # Save combined viewer file (optional but useful for debugging)
652
- try:
653
- with open(viewer_out, 'w') as vf:
654
- vf.writelines(out_lines)
655
- except Exception:
656
- pass
657
-
658
- return jsonify({'success': True, 'content': ''.join(out_lines)})
659
- except Exception as e:
660
- logger.error(f"Error generating viewer PDB: {str(e)}")
661
- return jsonify({'success': False, 'error': str(e)})
662
-
663
- @app.route('/api/get-corrected-ligands', methods=['GET'])
664
- def get_corrected_ligands():
665
- """Get the corrected ligand PDB file content if present"""
666
- try:
667
- ligand_file = OUTPUT_DIR / '4_ligands_corrected.pdb'
668
- if not ligand_file.exists():
669
- # Return success with exists flag false so frontend can decide gracefully
670
- return jsonify({'success': True, 'exists': False, 'content': ''})
671
- # Read and normalize records to HETATM for viewer compatibility
672
- normalized_lines = []
673
- with open(ligand_file, 'r') as f:
674
- for line in f:
675
- if line.startswith('ATOM'):
676
- # Replace record name to HETATM, preserve fixed-width columns
677
- normalized_lines.append('HETATM' + line[6:])
678
- else:
679
- normalized_lines.append(line)
680
- content = ''.join(normalized_lines)
681
- return jsonify({'success': True, 'exists': True, 'content': content})
682
- except Exception as e:
683
- logger.error(f"Error reading corrected ligand file: {str(e)}")
684
- return jsonify({'success': False, 'error': str(e)})
685
-
686
- @app.route('/api/get-aligned-ligands', methods=['GET'])
687
- def get_aligned_ligands():
688
- """Return ligand coordinates aligned to protein_solvated.pdb frame using PyMOL transforms."""
689
- try:
690
- solvated_file = OUTPUT_DIR / 'protein_solvated.pdb'
691
- tleap_ready = OUTPUT_DIR / 'tleap_ready.pdb'
692
- ligand_file = OUTPUT_DIR / '4_ligands_corrected.pdb'
693
-
694
- if not solvated_file.exists():
695
- return jsonify({'success': False, 'error': 'protein_solvated.pdb not found'}), 400
696
- if not tleap_ready.exists():
697
- return jsonify({'success': False, 'error': 'tleap_ready.pdb not found'}), 400
698
- if not ligand_file.exists():
699
- return jsonify({'success': True, 'exists': False, 'content': ''})
700
-
701
- # Create temp output path
702
- aligned_lig = OUTPUT_DIR / 'ligand_aligned_for_preview.pdb'
703
- try:
704
- if aligned_lig.exists():
705
- aligned_lig.unlink()
706
- except Exception:
707
- pass
708
-
709
- # PyMOL script: load solvated, load tlready (protein+lig), align tlready protein to solvated protein, then save transformed ligand
710
- pymol_script = f"""
711
- import pymol
712
- pymol.finish_launching(['pymol','-qc'])
713
- from pymol import cmd
714
- cmd.load('{solvated_file.as_posix()}', 'solv')
715
- cmd.load('{tleap_ready.as_posix()}', 'prep')
716
- cmd.load('{ligand_file.as_posix()}', 'lig')
717
- # Align prepared protein to solvated protein; use CA atoms to be robust
718
- cmd.align('prep and polymer.protein and name CA', 'solv and polymer.protein and name CA')
719
- # Apply same transform implicitly affects 'prep' object; we saved ligand as separate object, so match matrices
720
- mat = cmd.get_object_matrix('prep')
721
- cmd.set_object_matrix('lig', mat)
722
- # Save ligand in aligned frame, as HETATM
723
- cmd.alter('lig', 'type="HETATM"')
724
- cmd.save('{aligned_lig.as_posix()}', 'lig')
725
- cmd.quit()
726
- """
727
-
728
- # Run PyMOL inline
729
- result = subprocess.run(['python3', '-c', pymol_script], capture_output=True, text=True, cwd=str(OUTPUT_DIR))
730
- if result.returncode != 0:
731
- return jsonify({'success': False, 'error': f'PyMOL alignment failed: {result.stderr}'}), 500
732
-
733
- if not aligned_lig.exists():
734
- return jsonify({'success': False, 'error': 'Aligned ligand file was not produced'}), 500
735
-
736
- # Read and return content
737
- normalized_lines = []
738
- with open(aligned_lig, 'r') as f:
739
- for line in f:
740
- if line.startswith('ATOM'):
741
- normalized_lines.append('HETATM' + line[6:])
742
- else:
743
- normalized_lines.append(line)
744
- content = ''.join(normalized_lines)
745
- return jsonify({'success': True, 'exists': True, 'content': content})
746
- except Exception as e:
747
- logger.error(f"Error aligning ligands: {str(e)}")
748
- return jsonify({'success': False, 'error': str(e)}), 500
749
-
750
- @app.route('/viewer/<filename>')
751
- def viewer(filename):
752
- """Serve NGL viewer page"""
753
- # Check if the file exists, if not, try to generate it
754
- file_path = OUTPUT_DIR / filename
755
- if not file_path.exists():
756
- # Try to generate the viewer PDB if it's the specific file we need
757
- if filename == 'viewer_protein_with_ligand.pdb':
758
- try:
759
- # Call the get_viewer_pdb function to generate the file
760
- result = get_viewer_pdb()
761
- if result[1] == 200: # Success
762
- pass # File should now exist
763
- except:
764
- pass # Continue anyway
765
-
766
- return f"""
767
- <!DOCTYPE html>
768
- <html>
769
- <head>
770
- <title>NGL Viewer - {filename}</title>
771
- <script src="https://cdn.jsdelivr.net/npm/ngl@2.0.0-dev.37/dist/ngl.js"></script>
772
- <style>
773
- body {{ margin: 0; padding: 0; font-family: Arial, sans-serif; }}
774
- #viewport {{ width: 100%; height: 100vh; }}
775
- .header {{ background: #f8f9fa; padding: 10px; border-bottom: 1px solid #ddd; }}
776
- .controls {{ padding: 10px; background: #f8f9fa; }}
777
- .btn {{ padding: 8px 16px; margin: 5px; border: none; border-radius: 4px; cursor: pointer; }}
778
- .btn-primary {{ background: #007bff; color: white; }}
779
- .btn-secondary {{ background: #6c757d; color: white; }}
780
- </style>
781
- </head>
782
- <body>
783
- <div class="header">
784
- <h3>🧬 3D Structure Viewer - {filename}</h3>
785
- </div>
786
- <div id="viewport"></div>
787
- <div class="controls">
788
- <button class="btn btn-primary" onclick="resetView()">Reset View</button>
789
- <button class="btn btn-secondary" onclick="toggleRepresentation()">Toggle Style</button>
790
- <button class="btn btn-secondary" onclick="toggleSpin()">Toggle Spin</button>
791
- </div>
792
- <script>
793
- let stage;
794
- let currentRepresentation = 'cartoon';
795
- let isSpinning = false;
796
-
797
- async function initViewer() {{
798
- try {{
799
- // Check if file exists first
800
- const response = await fetch("/output/{filename}");
801
- if (!response.ok) {{
802
- throw new Error(`File not found: ${{response.status}} ${{response.statusText}}`);
803
- }}
804
-
805
- stage = new NGL.Stage("viewport", {{ backgroundColor: "white" }});
806
-
807
- const component = await stage.loadFile("/output/{filename}");
808
-
809
- // Add cartoon representation for protein
810
- component.addRepresentation("cartoon", {{
811
- sele: "protein",
812
- colorScheme: "chainname",
813
- opacity: 0.9
814
- }});
815
-
816
- // Add ball and stick for water molecules
817
- component.addRepresentation("ball+stick", {{
818
- sele: "water",
819
- color: "cyan",
820
- colorScheme: "uniform",
821
- radius: 0.1
822
- }});
823
-
824
- // Add ball and stick for ligands
825
- component.addRepresentation("ball+stick", {{
826
- sele: "hetero",
827
- color: "element",
828
- radius: 0.15
829
- }});
830
-
831
- stage.autoView();
832
- }} catch (error) {{
833
- console.error('Error loading structure:', error);
834
- document.getElementById('viewport').innerHTML =
835
- '<div style="padding: 50px; text-align: center; color: #dc3545;">' +
836
- '<h3>Error loading structure</h3><p>' + error.message + '</p>' +
837
- '<p>Make sure the file exists in the output directory.</p></div>';
838
- }}
839
- }}
840
-
841
- function resetView() {{
842
- if (stage) stage.autoView();
843
- }}
844
-
845
- function toggleRepresentation() {{
846
- if (!stage) return;
847
- const components = stage.compList;
848
- if (components.length === 0) return;
849
-
850
- const component = components[0];
851
- component.removeAllRepresentations();
852
-
853
- if (currentRepresentation === 'cartoon') {{
854
- component.addRepresentation("ball+stick", {{
855
- color: "element",
856
- radius: 0.15
857
- }});
858
- currentRepresentation = 'ball+stick';
859
- }} else {{
860
- component.addRepresentation("cartoon", {{
861
- sele: "protein",
862
- colorScheme: "chainname",
863
- opacity: 0.9
864
- }});
865
- component.addRepresentation("ball+stick", {{
866
- sele: "water",
867
- color: "cyan",
868
- colorScheme: "uniform",
869
- radius: 0.1
870
- }});
871
- component.addRepresentation("ball+stick", {{
872
- sele: "hetero",
873
- color: "element",
874
- radius: 0.15
875
- }});
876
- currentRepresentation = 'cartoon';
877
- }}
878
- }}
879
-
880
- function toggleSpin() {{
881
- if (!stage) return;
882
- isSpinning = !isSpinning;
883
- stage.setSpin(isSpinning);
884
- }}
885
-
886
- // Initialize when page loads
887
- document.addEventListener('DOMContentLoaded', initViewer);
888
- </script>
889
- </body>
890
- </html>
891
- """
892
-
893
- @app.route('/output/<path:filename>')
894
- def serve_output(filename):
895
- """Serve output files"""
896
- # Debug: print available files
897
- print(f"Requested file: {filename}")
898
- print(f"Full path: {OUTPUT_DIR / filename}")
899
- print(f"File exists: {(OUTPUT_DIR / filename).exists()}")
900
- print(f"Files in output dir: {list(OUTPUT_DIR.iterdir()) if OUTPUT_DIR.exists() else 'Directory not found'}")
901
-
902
- if not (OUTPUT_DIR / filename).exists():
903
- abort(404)
904
-
905
- return send_from_directory(OUTPUT_DIR, filename)
906
-
907
- @app.route('/')
908
- def index():
909
- """Serve the main HTML page"""
910
- return render_template('index.html')
911
-
912
- @app.route('/<path:filename>')
913
- def serve_static(filename):
914
- """Serve static files (CSS, JS, etc.)"""
915
- return send_from_directory('../', filename)
916
-
917
- @app.route('/api/prepare-structure', methods=['POST'])
918
- def prepare_structure_endpoint():
919
- """Prepare protein structure for AMBER"""
920
- try:
921
- data = request.get_json()
922
- pdb_content = data.get('pdb_content', '')
923
- options = data.get('options', {})
924
-
925
- if not pdb_content:
926
- return jsonify({'error': 'No PDB content provided'}), 400
927
-
928
- # Prepare structure
929
- result = prepare_structure(pdb_content, options)
930
-
931
- return jsonify({
932
- 'success': True,
933
- 'prepared_structure': result['prepared_structure'],
934
- 'original_atoms': result['original_atoms'],
935
- 'prepared_atoms': result['prepared_atoms'],
936
- 'removed_components': result['removed_components'],
937
- 'added_capping': result['added_capping'],
938
- 'preserved_ligands': result['preserved_ligands'],
939
- 'ligand_present': result.get('ligand_present', False),
940
- 'separate_ligands': result.get('separate_ligands', False),
941
- 'ligand_content': result.get('ligand_content', '')
942
- })
943
-
944
- except Exception as e:
945
- logger.error(f"Error preparing structure: {str(e)}")
946
- return jsonify({'error': str(e)}), 500
947
-
948
- @app.route('/api/parse-structure', methods=['POST'])
949
- def parse_structure_endpoint():
950
- """Parse structure information"""
951
- try:
952
- data = request.get_json()
953
- pdb_content = data.get('pdb_content', '')
954
-
955
- if not pdb_content:
956
- return jsonify({'error': 'No PDB content provided'}), 400
957
-
958
- # Parse structure
959
- structure_info = parse_structure_info(pdb_content)
960
-
961
- return jsonify({
962
- 'success': True,
963
- 'structure_info': structure_info
964
- })
965
-
966
- except Exception as e:
967
- logger.error(f"Error parsing structure: {str(e)}")
968
- return jsonify({'error': str(e)}), 500
969
-
970
- @app.route('/api/generate-ligand-ff', methods=['POST'])
971
- def generate_ligand_ff():
972
- """Generate force field parameters for ligand"""
973
- try:
974
- data = request.get_json()
975
- force_field = data.get('force_field', 'gaff2')
976
-
977
- # Determine the s parameter based on force field
978
- s_param = 2 if force_field == 'gaff2' else 1
979
-
980
- # Paths for ligand files in output directory
981
- ligand_pdb = OUTPUT_DIR / "4_ligands_corrected.pdb"
982
- ligand_mol2 = OUTPUT_DIR / "4_ligands_corrected.mol2"
983
- ligand_frcmod = OUTPUT_DIR / "4_ligands_corrected.frcmod"
984
-
985
- print(f"Working directory: {os.getcwd()}")
986
- print(f"Output directory: {OUTPUT_DIR}")
987
- print(f"Ligand PDB path: {ligand_pdb}")
988
- print(f"Ligand MOL2 path: {ligand_mol2}")
989
- print(f"Ligand FRCMOD path: {ligand_frcmod}")
990
-
991
- if not ligand_pdb.exists():
992
- return jsonify({'error': 'Ligand PDB file not found. Please prepare structure with ligands first.'}), 400
993
-
994
- import re
995
-
996
- # Command 1: Calculate net charge using awk
997
- print("Step 1: Calculating net charge from PDB file...")
998
- # Look for charge in the last field (field 12) - pattern is letter+number+charge
999
- awk_cmd = "awk '/^HETATM/ {if($NF ~ /[A-Z][0-9]-$/) charge--; if($NF ~ /[A-Z][0-9]\\+$/) charge++} END {print \"Net charge:\", charge+0}'"
1000
- cmd1 = f"{awk_cmd} {ligand_pdb}"
1001
-
1002
- try:
1003
- # Run awk command from the main directory, not output directory
1004
- result = subprocess.run(cmd1, shell=True, capture_output=True, text=True)
1005
- output = result.stdout.strip()
1006
- print(f"Awk output: '{output}'")
1007
- print(f"Awk stderr: '{result.stderr}'")
1008
-
1009
- # Extract net charge from awk output
1010
- net_charge_match = re.search(r'Net charge:\s*(-?\d+)', output)
1011
- if net_charge_match:
1012
- net_charge = int(net_charge_match.group(1))
1013
- print(f"Calculated net charge: {net_charge}")
1014
- else:
1015
- print("Could not extract net charge from awk output, using 0")
1016
- net_charge = 0
1017
- except Exception as e:
1018
- print(f"Error running awk command: {e}, using net charge 0")
1019
- net_charge = 0
1020
-
1021
- # Command 2: antechamber with calculated net charge
1022
- print(f"Step 2: Running antechamber with net charge {net_charge}...")
1023
- # Use relative paths and run in output directory
1024
- cmd2 = f"antechamber -i 4_ligands_corrected.pdb -fi pdb -o 4_ligands_corrected.mol2 -fo mol2 -c bcc -at {force_field} -nc {net_charge}"
1025
- print(f"Running command: {cmd2}")
1026
- result2 = subprocess.run(cmd2, shell=True, cwd=str(OUTPUT_DIR), capture_output=True, text=True)
1027
-
1028
- print(f"antechamber return code: {result2.returncode}")
1029
- print(f"antechamber stdout: {result2.stdout}")
1030
- print(f"antechamber stderr: {result2.stderr}")
1031
-
1032
- if result2.returncode != 0:
1033
- return jsonify({'error': f'antechamber failed with net charge {net_charge}. Error: {result2.stderr}'}), 500
1034
-
1035
- # Command 3: parmchk2
1036
- print("Step 3: Running parmchk2...")
1037
- # Use relative paths and run in output directory
1038
- cmd3 = f"parmchk2 -i 4_ligands_corrected.mol2 -f mol2 -o 4_ligands_corrected.frcmod -a Y -s {s_param}"
1039
- print(f"Running command: {cmd3}")
1040
- result3 = subprocess.run(cmd3, shell=True, cwd=str(OUTPUT_DIR), capture_output=True, text=True)
1041
-
1042
- print(f"parmchk2 return code: {result3.returncode}")
1043
- print(f"parmchk2 stdout: {result3.stdout}")
1044
- print(f"parmchk2 stderr: {result3.stderr}")
1045
-
1046
- if result3.returncode != 0:
1047
- return jsonify({'error': f'parmchk2 failed to generate force field parameters. Error: {result3.stderr}'}), 500
1048
-
1049
- # Check if files were generated successfully
1050
- print(f"After commands - MOL2 exists: {ligand_mol2.exists()}")
1051
- print(f"After commands - FRCMOD exists: {ligand_frcmod.exists()}")
1052
- print(f"Output directory contents: {list(OUTPUT_DIR.glob('*'))}")
1053
-
1054
- if not ligand_mol2.exists() or not ligand_frcmod.exists():
1055
- return jsonify({'error': 'Force field generation failed - output files not created'}), 500
1056
-
1057
- return jsonify({
1058
- 'success': True,
1059
- 'message': f'Ligand force field ({force_field}) generated successfully with net charge {net_charge}',
1060
- 'net_charge': net_charge,
1061
- 'files': {
1062
- 'mol2': str(ligand_mol2),
1063
- 'frcmod': str(ligand_frcmod)
1064
- }
1065
- })
1066
-
1067
- except Exception as e:
1068
- logger.error(f"Error generating ligand force field: {str(e)}")
1069
- return jsonify({'error': f'Internal server error: {str(e)}'}), 500
1070
-
1071
- @app.route('/api/calculate-net-charge', methods=['POST'])
1072
- def calculate_net_charge():
1073
- """Calculate net charge of the system using tleap"""
1074
- try:
1075
- # Check if structure is prepared
1076
- tleap_ready_file = OUTPUT_DIR / "tleap_ready.pdb"
1077
- if not tleap_ready_file.exists():
1078
- return jsonify({'error': 'Structure not prepared. Please prepare structure first.'}), 400
1079
-
1080
- # Check if ligand is present
1081
- ligand_pdb = OUTPUT_DIR / "4_ligands_corrected.pdb"
1082
- ligand_present = ligand_pdb.exists()
1083
-
1084
- # Create dynamic tleap input file
1085
- tleap_input = OUTPUT_DIR / "calc_charge_on_system.in"
1086
-
1087
- # Get the selected force field from the request
1088
- data = request.get_json() if request.get_json() else {}
1089
- selected_force_field = data.get('force_field', 'ff14SB')
1090
-
1091
- with open(tleap_input, 'w') as f:
1092
- f.write(f"source leaprc.protein.{selected_force_field}\n")
1093
- f.write("source leaprc.gaff2\n\n")
1094
-
1095
- if ligand_present:
1096
- # Load ligand parameters and structure
1097
- f.write("loadamberparams 4_ligands_corrected.frcmod\n\n")
1098
- f.write("COB = loadmol2 4_ligands_corrected.mol2\n\n")
1099
-
1100
- f.write("x = loadpdb tleap_ready.pdb\n\n")
1101
- f.write("charge x\n\n")
1102
-
1103
- # Run tleap command
1104
- print("Running tleap to calculate system charge...")
1105
- # Find tleap executable dynamically
1106
- try:
1107
- # First try to find tleap in PATH
1108
- which_result = subprocess.run(['which', 'tleap'], capture_output=True, text=True)
1109
- if which_result.returncode == 0:
1110
- tleap_path = which_result.stdout.strip()
1111
- else:
1112
- # Fallback: try common conda environment paths
1113
- conda_env = os.environ.get('CONDA_DEFAULT_ENV', 'MD_pipeline')
1114
- conda_prefix = os.environ.get('CONDA_PREFIX', '')
1115
- if conda_prefix:
1116
- tleap_path = os.path.join(conda_prefix, 'bin', 'tleap')
1117
- else:
1118
- # Last resort: assume it's in PATH
1119
- tleap_path = 'tleap'
1120
-
1121
- cmd = f"{tleap_path} -f calc_charge_on_system.in"
1122
- result = subprocess.run(cmd, shell=True, cwd=str(OUTPUT_DIR), capture_output=True, text=True)
1123
- except Exception as e:
1124
- # Fallback to simple tleap command
1125
- cmd = f"tleap -f calc_charge_on_system.in"
1126
- result = subprocess.run(cmd, shell=True, cwd=str(OUTPUT_DIR), capture_output=True, text=True)
1127
-
1128
- print(f"tleap return code: {result.returncode}")
1129
- print(f"tleap stdout: {result.stdout}")
1130
- print(f"tleap stderr: {result.stderr}")
1131
-
1132
- # Check if we got the charge information even if tleap had a non-zero exit code
1133
- # (tleap often returns non-zero when run non-interactively but still calculates charge)
1134
- if 'Total unperturbed charge' not in result.stdout and 'Total charge' not in result.stdout:
1135
- return jsonify({'error': f'tleap failed to calculate charge. Error: {result.stderr}'}), 500
1136
-
1137
- # Parse the output to find the net charge
1138
- output_lines = result.stdout.split('\n')
1139
- net_charge = None
1140
-
1141
- for line in output_lines:
1142
- if 'Total unperturbed charge' in line or 'Total charge' in line:
1143
- # Look for patterns like "Total charge: -3.0000" or "Total unperturbed charge: -3.0000"
1144
- import re
1145
- charge_match = re.search(r'charge[:\s]+(-?\d+\.?\d*)', line)
1146
- if charge_match:
1147
- net_charge = float(charge_match.group(1))
1148
- break
1149
-
1150
- if net_charge is None:
1151
- return jsonify({'error': 'Could not extract net charge from tleap output'}), 500
1152
-
1153
- # Suggest ion addition
1154
- if net_charge > 0:
1155
- suggestion = f"Add {int(net_charge)} Cl- ions to neutralize the system"
1156
- ion_type = "Cl-"
1157
- ion_count = int(net_charge)
1158
- elif net_charge < 0:
1159
- suggestion = f"Add {int(abs(net_charge))} Na+ ions to neutralize the system"
1160
- ion_type = "Na+"
1161
- ion_count = int(abs(net_charge))
1162
- else:
1163
- suggestion = "System is already neutral, no ions needed"
1164
- ion_type = "None"
1165
- ion_count = 0
1166
-
1167
- return jsonify({
1168
- 'success': True,
1169
- 'net_charge': net_charge,
1170
- 'suggestion': suggestion,
1171
- 'ion_type': ion_type,
1172
- 'ion_count': ion_count,
1173
- 'ligand_present': ligand_present
1174
- })
1175
-
1176
- except Exception as e:
1177
- logger.error(f"Error calculating net charge: {str(e)}")
1178
- return jsonify({'error': f'Internal server error: {str(e)}'}), 500
1179
-
1180
- @app.route('/api/generate-all-files', methods=['POST'])
1181
- def generate_all_files():
1182
- """Generate all simulation input files based on UI parameters"""
1183
- try:
1184
- data = request.get_json()
1185
-
1186
- # Get simulation parameters from UI
1187
- cutoff_distance = data.get('cutoff_distance', 10.0)
1188
- temperature = data.get('temperature', 310.0)
1189
- pressure = data.get('pressure', 1.0)
1190
-
1191
- # Get step parameters
1192
- restrained_steps = data.get('restrained_steps', 10000)
1193
- restrained_force = data.get('restrained_force', 10.0)
1194
- min_steps = data.get('min_steps', 20000)
1195
- npt_heating_steps = data.get('npt_heating_steps', 50000)
1196
- npt_equilibration_steps = data.get('npt_equilibration_steps', 100000)
1197
- production_steps = data.get('production_steps', 1000000)
1198
- # Integration time step (ps)
1199
- dt = data.get('timestep', 0.002)
1200
-
1201
- # Get force field parameters
1202
- force_field = data.get('force_field', 'ff14SB')
1203
- water_model = data.get('water_model', 'TIP3P')
1204
- add_ions = data.get('add_ions', 'None')
1205
- distance = data.get('distance', 10.0)
1206
-
1207
- # Validation warnings
1208
- warnings = []
1209
- if restrained_steps < 5000:
1210
- warnings.append("Restrained minimization steps should be at least 5000")
1211
- if min_steps < 10000:
1212
- warnings.append("Minimization steps should be at least 10000")
1213
-
1214
- # Count total residues in tleap_ready.pdb
1215
- tleap_ready_file = OUTPUT_DIR / "tleap_ready.pdb"
1216
- if not tleap_ready_file.exists():
1217
- return jsonify({'error': 'tleap_ready.pdb not found. Please prepare structure first.'}), 400
1218
-
1219
- total_residues = count_residues_in_pdb(str(tleap_ready_file))
1220
-
1221
- # Generate min_restrained.in
1222
- generate_min_restrained_file(restrained_steps, restrained_force, total_residues, cutoff_distance)
1223
-
1224
- # Generate min.in
1225
- generate_min_file(min_steps, cutoff_distance)
1226
-
1227
- # Generate HeatNPT.in
1228
- generate_heat_npt_file(npt_heating_steps, temperature, pressure, cutoff_distance, dt)
1229
-
1230
- # Generate mdin_equi.in (NPT Equilibration)
1231
- generate_npt_equilibration_file(npt_equilibration_steps, temperature, pressure, cutoff_distance, dt)
1232
-
1233
- # Generate mdin_prod.in (Production)
1234
- generate_production_file(production_steps, temperature, pressure, cutoff_distance, dt)
1235
-
1236
- # Generate force field parameters
1237
- ff_files_generated = []
1238
- try:
1239
- generate_ff_parameters_file(force_field, water_model, add_ions, distance)
1240
-
1241
- # Find tleap executable
1242
- tleap_path = None
1243
- try:
1244
- result = subprocess.run(['which', 'tleap'], capture_output=True, text=True)
1245
- if result.returncode == 0:
1246
- tleap_path = result.stdout.strip()
1247
- except:
1248
- pass
1249
-
1250
- if not tleap_path:
1251
- conda_prefix = os.environ.get('CONDA_PREFIX')
1252
- if conda_prefix:
1253
- tleap_path = os.path.join(conda_prefix, 'bin', 'tleap')
1254
- else:
1255
- tleap_path = '/home/hn533621/.conda/envs/MD_pipeline/bin/tleap'
1256
-
1257
- # Run tleap to generate force field parameters
1258
- cmd = f"{tleap_path} -f generate_ff_parameters.in"
1259
- result = subprocess.run(cmd, shell=True, cwd=str(OUTPUT_DIR),
1260
- capture_output=True, text=True, timeout=300)
1261
-
1262
- if result.returncode != 0:
1263
- warnings.append(f"Force field generation failed: {result.stderr}")
1264
- else:
1265
- # Check if key output files were created
1266
- ff_output_files = ['protein.prmtop', 'protein.inpcrd', 'protein_solvated.pdb']
1267
- for ff_file in ff_output_files:
1268
- if (OUTPUT_DIR / ff_file).exists():
1269
- ff_files_generated.append(ff_file)
1270
-
1271
- if len(ff_files_generated) == 0:
1272
- warnings.append("Force field parameter files were not generated")
1273
-
1274
- except Exception as ff_error:
1275
- warnings.append(f"Force field generation error: {str(ff_error)}")
1276
-
1277
- # Generate PBS submit script into output
1278
- pbs_generated = generate_submit_pbs_file()
1279
-
1280
- all_files = [
1281
- 'min_restrained.in',
1282
- 'min.in',
1283
- 'HeatNPT.in',
1284
- 'mdin_equi.in',
1285
- 'mdin_prod.in'
1286
- ] + ff_files_generated
1287
-
1288
- if pbs_generated:
1289
- all_files.append('submit_jobs.pbs')
1290
-
1291
- return jsonify({
1292
- 'success': True,
1293
- 'message': f'All simulation files generated successfully ({len(all_files)} files)',
1294
- 'warnings': warnings,
1295
- 'files_generated': all_files
1296
- })
1297
-
1298
- except Exception as e:
1299
- logger.error(f"Error generating simulation files: {str(e)}")
1300
- return jsonify({'error': f'Internal server error: {str(e)}'}), 500
1301
-
1302
- def count_residues_in_pdb(pdb_file):
1303
- """Count total number of residues in PDB file"""
1304
- try:
1305
- with open(pdb_file, 'r') as f:
1306
- lines = f.readlines()
1307
-
1308
- residues = set()
1309
- for line in lines:
1310
- if line.startswith(('ATOM', 'HETATM')):
1311
- # Extract residue number (columns 23-26)
1312
- residue_num = line[22:26].strip()
1313
- if residue_num:
1314
- residues.add(residue_num)
1315
-
1316
- return len(residues)
1317
- except Exception as e:
1318
- logger.error(f"Error counting residues: {str(e)}")
1319
- return 607 # Default fallback
1320
-
1321
- def generate_min_restrained_file(steps, force_constant, total_residues, cutoff):
1322
- """Generate min_restrained.in file"""
1323
- content = f"""initial minimization solvent + ions
1324
- &cntrl
1325
- imin = 1,
1326
- maxcyc = {steps},
1327
- ncyc = {steps // 2},
1328
- ntb = 1,
1329
- ntr = 1,
1330
- ntxo = 1,
1331
- cut = {cutoff}
1332
- /
1333
- Restrain
1334
- {force_constant}
1335
- RES 1 {total_residues}
1336
- END
1337
- END
1338
-
1339
- """
1340
-
1341
- with open(OUTPUT_DIR / "min_restrained.in", 'w') as f:
1342
- f.write(content)
1343
-
1344
- def generate_min_file(steps, cutoff):
1345
- """Generate min.in file"""
1346
- content = f"""Minimization
1347
- &cntrl
1348
- imin=1,
1349
- maxcyc={steps},
1350
- ncyc={steps // 4},
1351
- ntb=1,
1352
- cut={cutoff},
1353
- igb=0,
1354
- ntr=0,
1355
- /
1356
-
1357
- """
1358
-
1359
- with open(OUTPUT_DIR / "min.in", 'w') as f:
1360
- f.write(content)
1361
-
1362
- def generate_heat_npt_file(steps, temperature, pressure, cutoff, dt=0.002):
1363
- """Generate HeatNPT.in file with temperature ramping"""
1364
- # Calculate step divisions: 20%, 20%, 20%, 40%
1365
- step1 = int(steps * 0.2)
1366
- step2 = int(steps * 0.2)
1367
- step3 = int(steps * 0.2)
1368
- step4 = int(steps * 0.4)
1369
-
1370
- # Calculate temperature values: 3%, 66%, 100%
1371
- temp1 = temperature * 0.03
1372
- temp2 = temperature * 0.66
1373
- temp3 = temperature
1374
- temp4 = temperature
1375
-
1376
- content = f"""Heat
1377
- &cntrl
1378
- imin = 0, irest = 0, ntx = 1,
1379
- ntb = 2, pres0 = {pressure}, ntp = 1,
1380
- taup = 2.0,
1381
- cut = {cutoff}, ntr = 0,
1382
- ntc = 2, ntf = 2,
1383
- tempi = 0, temp0 = {temperature},
1384
- ntt = 3, gamma_ln = 1.0,
1385
- nstlim = {steps}, dt = {dt},
1386
- ntpr = 2000, ntwx = 2000, ntwr = 2000
1387
- /
1388
- &wt type='TEMP0', istep1=0, istep2={step1}, value1=0.0, value2={temp1} /
1389
- &wt type='TEMP0', istep1={step1+1}, istep2={step1+step2}, value1={temp1}, value2={temp2} /
1390
- &wt type='TEMP0', istep1={step1+step2+1}, istep2={step1+step2+step3}, value1={temp2}, value2={temp3} /
1391
- &wt type='TEMP0', istep1={step1+step2+step3+1}, istep2={steps}, value1={temp3}, value2={temp4} /
1392
- &wt type='END' /
1393
-
1394
- """
1395
-
1396
- with open(OUTPUT_DIR / "HeatNPT.in", 'w') as f:
1397
- f.write(content)
1398
-
1399
- def generate_npt_equilibration_file(steps, temperature, pressure, cutoff, dt=0.002):
1400
- """Generate mdin_equi.in file for NPT equilibration"""
1401
- content = f"""NPT Equilibration
1402
- &cntrl
1403
- imin=0,
1404
- ntx=1,
1405
- irest=0,
1406
- pres0={pressure},
1407
- taup=1.0,
1408
- temp0={temperature},
1409
- tempi={temperature},
1410
- nstlim={steps},
1411
- dt={dt},
1412
- ntf=2,
1413
- ntc=2,
1414
- ntpr=500,
1415
- ntwx=500,
1416
- ntwr=500,
1417
- cut={cutoff},
1418
- ntb=2,
1419
- ntp=1,
1420
- ntt=3,
1421
- gamma_ln=3.0,
1422
- ig=-1,
1423
- iwrap=1,
1424
- ntr=0,
1425
- /
1426
-
1427
- """
1428
-
1429
- with open(OUTPUT_DIR / "mdin_equi.in", 'w') as f:
1430
- f.write(content)
1431
-
1432
- def generate_production_file(steps, temperature, pressure, cutoff, dt=0.002):
1433
- """Generate mdin_prod.in file for production run"""
1434
- content = f"""Production Run
1435
- &cntrl
1436
- imin=0,
1437
- ntx=1,
1438
- irest=0,
1439
- pres0={pressure},
1440
- taup=1.0,
1441
- temp0={temperature},
1442
- tempi={temperature},
1443
- nstlim={steps},
1444
- dt={dt},
1445
- ntf=2,
1446
- ntc=2,
1447
- ntpr=1000,
1448
- ntwx=1000,
1449
- ntwr=1000,
1450
- cut={cutoff},
1451
- ntb=2,
1452
- ntp=1,
1453
- ntt=3,
1454
- gamma_ln=3.0,
1455
- ig=-1,
1456
- iwrap=1,
1457
- ntr=0,
1458
- /
1459
-
1460
- """
1461
-
1462
- with open(OUTPUT_DIR / "mdin_prod.in", 'w') as f:
1463
- f.write(content)
1464
-
1465
- def generate_submit_pbs_file():
1466
- """Copy submit_jobs.pbs template into output folder"""
1467
- try:
1468
- templates_dir = Path("templates")
1469
- template_path = templates_dir / "submit_jobs.pbs"
1470
- if not template_path.exists():
1471
- logger.warning("submit_jobs.pbs template not found; skipping PBS generation")
1472
- return False
1473
- with open(template_path, 'r') as tf:
1474
- content = tf.read()
1475
- with open(OUTPUT_DIR / "submit_jobs.pbs", 'w') as outf:
1476
- outf.write(content)
1477
- return True
1478
- except Exception as e:
1479
- logger.error(f"Error generating submit_jobs.pbs: {e}")
1480
- return False
1481
-
1482
- @app.route('/api/health', methods=['GET'])
1483
- def health_check():
1484
- """Health check endpoint"""
1485
- return jsonify({'status': 'healthy', 'message': 'MD Simulation Pipeline API is running'})
1486
-
1487
- @app.route('/api/clean-output', methods=['POST'])
1488
- def clean_output():
1489
- """Clean output folder endpoint"""
1490
- try:
1491
- print("DEBUG: clean-output endpoint called")
1492
- if clean_and_create_output_folder():
1493
- return jsonify({'success': True, 'message': 'Output folder cleaned successfully'})
1494
- else:
1495
- return jsonify({'success': False, 'error': 'Failed to clean output folder'}), 500
1496
- except Exception as e:
1497
- print(f"DEBUG: Error in clean-output: {str(e)}")
1498
- return jsonify({'success': False, 'error': str(e)}), 500
1499
-
1500
- @app.route('/api/download-output-zip', methods=['GET'])
1501
- def download_output_zip():
1502
- """Create a ZIP of the output folder and return it for download"""
1503
- try:
1504
- if not OUTPUT_DIR.exists():
1505
- return jsonify({'error': 'Output directory not found'}), 404
1506
-
1507
- import tempfile
1508
- import shutil
1509
-
1510
- # Create a temporary zip file
1511
- tmp_dir = tempfile.mkdtemp()
1512
- zip_base = os.path.join(tmp_dir, 'output')
1513
- zip_path = shutil.make_archive(zip_base, 'zip', root_dir=str(OUTPUT_DIR))
1514
-
1515
- # Send file for download
1516
- return send_file(zip_path, as_attachment=True, download_name='output.zip')
1517
- except Exception as e:
1518
- logger.error(f"Error creating output ZIP: {str(e)}")
1519
- return jsonify({'error': f'Failed to create ZIP: {str(e)}'}), 500
1520
-
1521
- @app.route('/api/get-generated-files', methods=['GET'])
1522
- def get_generated_files():
1523
- """Return contents of known generated input files for preview"""
1524
- try:
1525
- files_to_read = [
1526
- 'min_restrained.in',
1527
- 'min.in',
1528
- 'HeatNPT.in',
1529
- 'mdin_equi.in',
1530
- 'mdin_prod.in'
1531
- ]
1532
- # Note: Force field parameter files (protein.prmtop, protein.inpcrd, protein_solvated.pdb)
1533
- # are excluded from preview as they are binary/large files
1534
- result = {}
1535
- for name in files_to_read:
1536
- path = OUTPUT_DIR / name
1537
- if path.exists():
1538
- try:
1539
- with open(path, 'r') as f:
1540
- result[name] = f.read()
1541
- except Exception as fe:
1542
- result[name] = f"<error reading file: {fe}>"
1543
- else:
1544
- result[name] = "<file not found>"
1545
- return jsonify({'success': True, 'files': result})
1546
- except Exception as e:
1547
- logger.error(f"Error reading generated files: {str(e)}")
1548
- return jsonify({'error': f'Failed to read files: {str(e)}'}), 500
1549
-
1550
- def get_ligand_residue_name():
1551
- """Extract ligand residue name from tleap_ready.pdb"""
1552
- try:
1553
- with open(OUTPUT_DIR / "tleap_ready.pdb", 'r') as f:
1554
- for line in f:
1555
- if line.startswith('HETATM'):
1556
- # Extract residue name (columns 18-20)
1557
- residue_name = line[17:20].strip()
1558
- if residue_name and residue_name not in ['HOH', 'WAT', 'TIP', 'SPC']: # Exclude water
1559
- return residue_name
1560
- return "LIG" # Default fallback
1561
- except:
1562
- return "LIG" # Default fallback
1563
-
1564
- def generate_ff_parameters_file(force_field, water_model, add_ions, distance):
1565
- """Generate the final force field parameters file with dynamic values"""
1566
- # Debug logging
1567
- print(f"DEBUG: force_field={force_field}, water_model={water_model}, add_ions={add_ions}, distance={distance}")
1568
-
1569
- # Determine if ligand is present
1570
- ligand_present = (OUTPUT_DIR / "4_ligands_corrected.mol2").exists()
1571
-
1572
- # Get dynamic ligand residue name
1573
- ligand_name = get_ligand_residue_name()
1574
-
1575
- # Build the content dynamically
1576
- content = f"source leaprc.protein.{force_field}\n"
1577
-
1578
- # Add water model source
1579
- print(f"DEBUG: water_model={water_model}")
1580
- if water_model.lower() == "tip3p":
1581
- content += "source leaprc.water.tip3p\n"
1582
- elif water_model == "spce":
1583
- content += "source leaprc.water.spce\n"
1584
-
1585
- # Add ligand-related commands only if ligand is present
1586
- if ligand_present:
1587
- content += "source leaprc.gaff2\n\n"
1588
- content += "loadamberparams 4_ligands_corrected.frcmod\n\n"
1589
- content += f"{ligand_name} = loadmol2 4_ligands_corrected.mol2\n\n"
1590
- else:
1591
- content += "\n"
1592
-
1593
- content += "x = loadpdb tleap_ready.pdb\n\n"
1594
- content += "charge x\n\n"
1595
-
1596
- # Add ions based on selection
1597
- if add_ions == "Na+":
1598
- content += "addions x Na+ 0.0\n\n"
1599
- elif add_ions == "Cl-":
1600
- content += "addions x Cl- 0.0\n\n"
1601
- # If "None", skip adding ions
1602
-
1603
- # Add solvation with selected water model and distance
1604
- if water_model.lower() == "tip3p":
1605
- content += f"solvateBox x TIP3PBOX {distance}\n\n"
1606
- elif water_model.lower() == "spce":
1607
- content += f"solvateBox x SPCBOX {distance}\n\n"
1608
-
1609
- content += "saveamberparm x protein.prmtop protein.inpcrd\n\n"
1610
- content += "savepdb x protein_solvated.pdb\n\n"
1611
- content += "quit\n"
1612
-
1613
- # Debug: print the generated content
1614
- print("DEBUG: Generated content:")
1615
- print(content)
1616
-
1617
- # Write the file
1618
- with open(OUTPUT_DIR / "generate_ff_parameters.in", 'w') as f:
1619
- f.write(content)
1620
-
1621
- @app.route('/api/generate-ff-parameters', methods=['POST'])
1622
- def generate_ff_parameters():
1623
- """Generate final force field parameters using tleap"""
1624
- try:
1625
- data = request.get_json()
1626
- force_field = data.get('force_field', 'ff14SB')
1627
- water_model = data.get('water_model', 'TIP3P')
1628
- add_ions = data.get('add_ions', 'None')
1629
- distance = data.get('distance', 10.0)
1630
-
1631
- # Generate the dynamic input file
1632
- generate_ff_parameters_file(force_field, water_model, add_ions, distance)
1633
-
1634
- # Find tleap executable
1635
- tleap_path = None
1636
- try:
1637
- result = subprocess.run(['which', 'tleap'], capture_output=True, text=True)
1638
- if result.returncode == 0:
1639
- tleap_path = result.stdout.strip()
1640
- except:
1641
- pass
1642
-
1643
- if not tleap_path:
1644
- conda_prefix = os.environ.get('CONDA_PREFIX')
1645
- if conda_prefix:
1646
- tleap_path = os.path.join(conda_prefix, 'bin', 'tleap')
1647
- else:
1648
- tleap_path = '/home/hn533621/.conda/envs/MD_pipeline/bin/tleap'
1649
-
1650
- # Run tleap
1651
- cmd = f"{tleap_path} -f generate_ff_parameters.in"
1652
- result = subprocess.run(cmd, shell=True, cwd=str(OUTPUT_DIR),
1653
- capture_output=True, text=True, timeout=300)
1654
-
1655
- if result.returncode != 0:
1656
- logger.error(f"tleap failed: {result.stderr}")
1657
- return jsonify({
1658
- 'success': False,
1659
- 'error': f'tleap failed: {result.stderr}'
1660
- }), 500
1661
-
1662
- # Check if key output files were created
1663
- output_files = ['protein.prmtop', 'protein.inpcrd', 'protein_solvated.pdb']
1664
- missing_files = [f for f in output_files if not (OUTPUT_DIR / f).exists()]
1665
-
1666
- if missing_files:
1667
- return jsonify({
1668
- 'success': False,
1669
- 'error': f'Missing output files: {", ".join(missing_files)}'
1670
- }), 500
1671
-
1672
- return jsonify({
1673
- 'success': True,
1674
- 'message': 'Force field parameters generated successfully',
1675
- 'files_generated': output_files
1676
- })
1677
-
1678
- except subprocess.TimeoutExpired:
1679
- return jsonify({
1680
- 'success': False,
1681
- 'error': 'tleap command timed out after 5 minutes'
1682
- }), 500
1683
- except Exception as e:
1684
- logger.error(f"Error generating FF parameters: {str(e)}")
1685
- return jsonify({
1686
- 'success': False,
1687
- 'error': f'Failed to generate force field parameters: {str(e)}'
1688
- }), 500
1689
-
1690
- if __name__ == '__main__':
1691
- print("🧬 MD Simulation Pipeline")
1692
- print("=========================")
1693
- print("🌐 Starting Flask server...")
1694
- print("📡 Backend API: http://localhost:5000")
1695
- print("🔗 Web Interface: http://localhost:5000")
1696
- print("")
1697
- print("Press Ctrl+C to stop the server")
1698
- print("")
1699
-
1700
- # Clean and create fresh output folder on startup
1701
- print("🧹 Cleaning output folder...")
1702
- clean_and_create_output_folder()
1703
- print("✅ Output folder ready!")
1704
- print("")
1705
-
1706
- app.run(debug=False, host='0.0.0.0', port=5000)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
start_web_server.py CHANGED
@@ -1,20 +1,10 @@
1
  #!/usr/bin/env python3
2
  """
3
- AmberFlow - Hugging Face Spaces Entry Point
4
- This file serves as the entry point for Hugging Face Spaces deployment.
5
- It imports and runs the existing Flask application from python/app.py
6
  """
7
 
8
- import os
9
- import sys
10
- import subprocess
11
-
12
- # Add the python directory to the path so we can import the Flask app
13
- sys.path.append(os.path.join(os.path.dirname(__file__), 'python'))
14
-
15
- # Import the Flask app from the existing python/app.py
16
- from python.app import app
17
 
18
  if __name__ == "__main__":
19
- # Run the Flask app
20
- app.run(debug=False, host='0.0.0.0', port=7860)
 
1
  #!/usr/bin/env python3
2
  """
3
+ AmberFlow - Entry point when running from project root.
4
+ Uses the amberflow package. For installed package: use `amberflow` or `python -m amberflow`.
 
5
  """
6
 
7
+ from amberflow.app import app
 
 
 
 
 
 
 
 
8
 
9
  if __name__ == "__main__":
10
+ app.run(debug=False, host="0.0.0.0", port=7860)