Sync with development: Dockerfile (meeko, vina, openbabel, PYTHONPATH), amberflow package, .gitignore. Exclude Test/ (binary .ncrst not allowed on HF Hub).
Browse files- .dockerignore +54 -0
- .gitignore +61 -0
- Dockerfile +7 -7
- README.md +263 -92
- SETUP_STEPS.md +128 -0
- amberflow/Fill_missing_residues.py +446 -0
- amberflow/__init__.py +3 -0
- amberflow/__main__.py +11 -0
- add_caps.py → amberflow/add_caps.py +0 -0
- amberflow/app.py +0 -0
- amberflow/css/plumed.css +1064 -0
- {css → amberflow/css}/styles.css +908 -0
- amberflow/docking.py +54 -0
- amberflow/docking_utils.py +639 -0
- {html → amberflow/html}/index.html +571 -13
- amberflow/html/plumed.html +99 -0
- amberflow/js/plumed.js +0 -0
- amberflow/js/plumed_cv_docs.js +0 -0
- amberflow/js/script.js +0 -0
- {python → amberflow}/structure_preparation.py +549 -58
- js/script.js +0 -2004
- python/__pycache__/app.cpython-310.pyc +0 -0
- python/__pycache__/app.cpython-312.pyc +0 -0
- python/__pycache__/structure_preparation.cpython-310.pyc +0 -0
- python/app.py +0 -1706
- start_web_server.py +4 -14
.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
|
| 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 |
-
|
| 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
|
| 69 |
-
ENV PYTHONPATH="${PYTHONPATH}:/AmberFlow
|
| 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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
#
|
| 13 |
|
| 14 |
-
|
| 15 |
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
###
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
-
|
| 29 |
-
- **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.
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
- **
|
| 75 |
-
- **
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
```
|
| 82 |
AmberFlow/
|
| 83 |
-
├──
|
| 84 |
-
├── requirements.txt # Python dependencies
|
| 85 |
-
├── python/
|
| 86 |
-
│ ├── app.py # Main Flask application
|
| 87 |
-
│ ├── structure_preparation.py
|
| 88 |
-
│ └── requirements.txt
|
| 89 |
├── html/
|
| 90 |
-
│
|
|
|
|
| 91 |
├── css/
|
| 92 |
-
│
|
|
|
|
| 93 |
├── js/
|
| 94 |
-
│
|
| 95 |
-
├──
|
| 96 |
-
└──
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
```
|
| 98 |
|
|
|
|
|
|
|
| 99 |
## Citation
|
| 100 |
|
| 101 |
-
If you use AmberFlow in your
|
| 102 |
|
| 103 |
```bibtex
|
| 104 |
-
@software{
|
| 105 |
-
title={AmberFlow: Molecular Dynamics
|
| 106 |
-
author=
|
| 107 |
-
year={2025},
|
| 108 |
-
url={https://
|
| 109 |
}
|
| 110 |
```
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
## Acknowledgments
|
| 113 |
|
| 114 |
-
- **Mohd Ibrahim** (Technical University of Munich) for the protein capping
|
|
|
|
|
|
|
| 115 |
|
| 116 |
## License
|
| 117 |
|
| 118 |
-
|
|
|
|
|
|
|
| 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
|
| 251 |
-
<p><strong>Prepared atoms
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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">
|
| 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... Example: # PLUMED input file d1: DISTANCE ATOMS=1,2 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 |
-
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 259 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 267 |
-
|
| 268 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 289 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
try:
|
| 291 |
# Read protein file
|
| 292 |
with open(protein_file, 'r') as f:
|
| 293 |
protein_lines = f.readlines()
|
| 294 |
|
| 295 |
-
#
|
| 296 |
-
|
| 297 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 321 |
-
|
| 322 |
-
for
|
| 323 |
-
if
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 325 |
|
| 326 |
-
# Combine: protein + TER + ligand
|
| 327 |
-
merged_content = ''.join(protein_processed) + ''.join(
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 354 |
-
|
| 355 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
#
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 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 |
-
|
| 432 |
-
|
| 433 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 503 |
-
|
| 504 |
-
if
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 -
|
| 4 |
-
|
| 5 |
-
It imports and runs the existing Flask application from python/app.py
|
| 6 |
"""
|
| 7 |
|
| 8 |
-
import
|
| 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 |
-
|
| 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)
|
|
|