Spaces:
Running
Running
naagrh Cursor commited on
Commit ·
b8a800e
0
Parent(s):
AmberMDFlow for HF Space (no binaries)
Browse filesCo-authored-by: Cursor <cursoragent@cursor.com>
- .dockerignore +54 -0
- .gitattributes +35 -0
- .gitignore +61 -0
- Dockerfile +74 -0
- LICENSE +21 -0
- MANIFEST.in +18 -0
- README.md +331 -0
- ambermdflow/Fill_missing_residues.py +446 -0
- ambermdflow/__init__.py +37 -0
- ambermdflow/__main__.py +11 -0
- ambermdflow/add_caps.py +176 -0
- ambermdflow/app.py +0 -0
- ambermdflow/css/plumed.css +1064 -0
- ambermdflow/css/styles.css +2029 -0
- ambermdflow/docking.py +54 -0
- ambermdflow/docking_utils.py +639 -0
- ambermdflow/html/index.html +1145 -0
- ambermdflow/html/plumed.html +99 -0
- ambermdflow/js/plumed.js +0 -0
- ambermdflow/js/plumed_cv_docs.js +0 -0
- ambermdflow/js/script.js +0 -0
- ambermdflow/structure_preparation.py +1194 -0
- pyproject.toml +97 -0
- start_web_server.py +10 -0
.dockerignore
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Git
|
| 2 |
+
.git
|
| 3 |
+
.gitignore
|
| 4 |
+
|
| 5 |
+
# Python
|
| 6 |
+
__pycache__
|
| 7 |
+
*.pyc
|
| 8 |
+
*.pyo
|
| 9 |
+
*.pyd
|
| 10 |
+
.Python
|
| 11 |
+
*.so
|
| 12 |
+
*.egg
|
| 13 |
+
*.egg-info
|
| 14 |
+
dist
|
| 15 |
+
build
|
| 16 |
+
.pytest_cache
|
| 17 |
+
.coverage
|
| 18 |
+
htmlcov
|
| 19 |
+
|
| 20 |
+
# Virtual environments
|
| 21 |
+
venv/
|
| 22 |
+
env/
|
| 23 |
+
ENV/
|
| 24 |
+
|
| 25 |
+
# IDE
|
| 26 |
+
.vscode/
|
| 27 |
+
.idea/
|
| 28 |
+
*.swp
|
| 29 |
+
*.swo
|
| 30 |
+
*~
|
| 31 |
+
|
| 32 |
+
# OS
|
| 33 |
+
.DS_Store
|
| 34 |
+
Thumbs.db
|
| 35 |
+
|
| 36 |
+
# Output and temporary files
|
| 37 |
+
output/
|
| 38 |
+
temp/
|
| 39 |
+
*.log
|
| 40 |
+
*.tmp
|
| 41 |
+
|
| 42 |
+
# Documentation
|
| 43 |
+
*.md
|
| 44 |
+
!README.md
|
| 45 |
+
|
| 46 |
+
# Docker
|
| 47 |
+
Dockerfile
|
| 48 |
+
docker-compose.yml
|
| 49 |
+
.dockerignore
|
| 50 |
+
|
| 51 |
+
# Other
|
| 52 |
+
*.bak
|
| 53 |
+
*.backup
|
| 54 |
+
|
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
|
| 23 |
+
# Virtual environments
|
| 24 |
+
venv/
|
| 25 |
+
env/
|
| 26 |
+
ENV/
|
| 27 |
+
|
| 28 |
+
# IDE
|
| 29 |
+
.vscode/
|
| 30 |
+
.idea/
|
| 31 |
+
*.swp
|
| 32 |
+
*.swo
|
| 33 |
+
|
| 34 |
+
# OS
|
| 35 |
+
.DS_Store
|
| 36 |
+
Thumbs.db
|
| 37 |
+
|
| 38 |
+
# Project specific
|
| 39 |
+
output/
|
| 40 |
+
*.log
|
| 41 |
+
*.tmp
|
| 42 |
+
temp/
|
| 43 |
+
|
| 44 |
+
# PDB files (optional - remove if you want to include example PDBs)
|
| 45 |
+
*.pdb
|
| 46 |
+
*.ent
|
| 47 |
+
|
| 48 |
+
# AMBER files
|
| 49 |
+
*.prmtop
|
| 50 |
+
*.inpcrd
|
| 51 |
+
*.rst
|
| 52 |
+
*.rst7
|
| 53 |
+
*.ncrst
|
| 54 |
+
*.mdcrd
|
| 55 |
+
*.nc
|
| 56 |
+
*.dcd
|
| 57 |
+
*.xtc
|
| 58 |
+
*.trr
|
| 59 |
+
|
| 60 |
+
# Binaries (HF Spaces rejects; logo can be hosted elsewhere)
|
| 61 |
+
AmberMDFlow.png
|
Dockerfile
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.9-slim
|
| 2 |
+
|
| 3 |
+
# Install system dependencies
|
| 4 |
+
RUN apt-get update && apt-get install -y \
|
| 5 |
+
wget \
|
| 6 |
+
curl \
|
| 7 |
+
build-essential \
|
| 8 |
+
gcc \
|
| 9 |
+
g++ \
|
| 10 |
+
make \
|
| 11 |
+
libffi-dev \
|
| 12 |
+
libssl-dev \
|
| 13 |
+
libglib2.0-0 \
|
| 14 |
+
libxext6 \
|
| 15 |
+
libsm6 \
|
| 16 |
+
libxrender1 \
|
| 17 |
+
libgomp1 \
|
| 18 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 19 |
+
|
| 20 |
+
# Install Miniforge (conda-forge–based; no Anaconda ToS) and configure channels
|
| 21 |
+
RUN wget -q https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh && \
|
| 22 |
+
bash Miniforge3-Linux-x86_64.sh -b -p /opt/conda && \
|
| 23 |
+
rm Miniforge3-Linux-x86_64.sh
|
| 24 |
+
|
| 25 |
+
ENV PATH="/opt/conda/bin:${PATH}"
|
| 26 |
+
|
| 27 |
+
# Add bioconda; conda-forge is default for Miniforge. No Anaconda ToS needed.
|
| 28 |
+
RUN conda config --add channels bioconda && \
|
| 29 |
+
conda config --set channel_priority flexible
|
| 30 |
+
|
| 31 |
+
# Install mamba for faster package installation
|
| 32 |
+
RUN conda install -n base -c conda-forge mamba -y
|
| 33 |
+
|
| 34 |
+
# Install AMBER tools, PyMOL, AutoDock Vina 1.1.2, Open Babel, RDKit, and gemmi (for Meeko)
|
| 35 |
+
RUN mamba install -y python=3.11 \
|
| 36 |
+
conda-forge::ambertools conda-forge::pymol-open-source \
|
| 37 |
+
bioconda::autodock-vina conda-forge::openbabel conda-forge::rdkit conda-forge::gemmi
|
| 38 |
+
|
| 39 |
+
# Clean up conda/mamba cache to reduce image size
|
| 40 |
+
RUN conda clean -afy
|
| 41 |
+
|
| 42 |
+
# Install Python packages via pip (only packages not provided by conda or that need pip)
|
| 43 |
+
# Note: numpy, pandas, matplotlib are already installed by conda; don't override to avoid conflicts
|
| 44 |
+
RUN pip install --no-cache-dir \
|
| 45 |
+
flask==2.3.3 \
|
| 46 |
+
flask-cors==4.0.0 \
|
| 47 |
+
biopython \
|
| 48 |
+
seaborn \
|
| 49 |
+
mdanalysis \
|
| 50 |
+
gunicorn==21.2.0 \
|
| 51 |
+
requests \
|
| 52 |
+
meeko>=0.7.0 \
|
| 53 |
+
prody \
|
| 54 |
+
"numpy<2.0"
|
| 55 |
+
|
| 56 |
+
# Set working directory
|
| 57 |
+
WORKDIR /AmberMDFlow
|
| 58 |
+
|
| 59 |
+
# Copy the entire project
|
| 60 |
+
COPY . .
|
| 61 |
+
|
| 62 |
+
# Create necessary directories with proper permissions
|
| 63 |
+
RUN mkdir -p /AmberMDFlow/obsolete /AmberMDFlow/pdb /AmberMDFlow/temp /AmberMDFlow/output && \
|
| 64 |
+
chmod -R 777 /AmberMDFlow
|
| 65 |
+
|
| 66 |
+
# Make sure the ambermdflow package is on the Python path
|
| 67 |
+
ENV PYTHONPATH="${PYTHONPATH}:/AmberMDFlow"
|
| 68 |
+
|
| 69 |
+
# Expose the port
|
| 70 |
+
EXPOSE 7860
|
| 71 |
+
|
| 72 |
+
# Run the application
|
| 73 |
+
CMD ["python", "start_web_server.py"]
|
| 74 |
+
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Hemant Nagar
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
MANIFEST.in
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Include package data
|
| 2 |
+
recursive-include ambermdflow/html *
|
| 3 |
+
recursive-include ambermdflow/css *
|
| 4 |
+
recursive-include ambermdflow/js *
|
| 5 |
+
|
| 6 |
+
# Include documentation
|
| 7 |
+
include README.md
|
| 8 |
+
include LICENSE
|
| 9 |
+
|
| 10 |
+
# Exclude development/test files
|
| 11 |
+
exclude .gitignore
|
| 12 |
+
exclude .dockerignore
|
| 13 |
+
exclude Dockerfile
|
| 14 |
+
prune Test
|
| 15 |
+
prune __pycache__
|
| 16 |
+
global-exclude *.pyc
|
| 17 |
+
global-exclude *.pyo
|
| 18 |
+
global-exclude .DS_Store
|
README.md
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: AmberMDFlow
|
| 3 |
+
sdk: docker
|
| 4 |
+
app_port: 7860
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# AmberMDFlow
|
| 8 |
+
|
| 9 |
+
**AmberMDFlow** is a web-based pipeline for preparing structures, setting up molecular dynamics (MD) simulations with the AMBER force field. It integrates structure completion (ESMFold), preparation, force field parameterization, simulation file generation, and PLUMED-based biased MD in a single interface. This is the beta version.
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## Note
|
| 14 |
+
- If you plan to dock ligands and have filled missing residues in the protein chain using ESMFold, you should also energy-minimize the structure.
|
| 15 |
+
- The **Fill Missing Residues** option works only for PDB files retrieved from the RCSB database, as it relies on the REMARK 465 records to identify missing residues.
|
| 16 |
+
- The public ESMFold API is used to predict protein structures from input sequences, and it supports sequences of up to 400 amino acids.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## Features
|
| 21 |
+
|
| 22 |
+
| Section | Description |
|
| 23 |
+
|--------|-------------|
|
| 24 |
+
| **Protein Loading** | Upload PDB files or fetch from RCSB PDB; 3D visualization with NGL |
|
| 25 |
+
| **Fill Missing Residues** | Detect missing residues (RCSB annotations), complete with ESMFold, optional trimming and energy minimization of predicted structure|
|
| 26 |
+
| **Structure Preparation** | Remove water/ions/H; add ACE/NME capping; chain and ligand selection; GAFF/GAFF2 parameterization |
|
| 27 |
+
| **Ligand Docking** | AutoDock Vina + Meeko; configurable search box; pose selection and use selected ligand pose to setup MD simulations |
|
| 28 |
+
| **Simulation Parameters** | Force fields (ff14SB, ff19SB), water models (TIP3P, SPCE), box size, temperature, pressure |
|
| 29 |
+
| **Simulation Steps** | Restrained minimization, minimization, NVT, NPT, production — each with configurable parameters |
|
| 30 |
+
| **Generate Files** | AMBER `.in` files, `prmtop`/`inpcrd`, PBS submission scripts |
|
| 31 |
+
| **PLUMED** | Collective variables (PLUMED v2.9), `plumed.dat` editor, and simulation file generation with PLUMED |
|
| 32 |
+
|
| 33 |
+
---
|
| 34 |
+
|
| 35 |
+
## Requirements for Custom PDB Files
|
| 36 |
+
|
| 37 |
+
For **custom PDB files** (uploaded or fetched), ensure:
|
| 38 |
+
|
| 39 |
+
| Requirement | Description |
|
| 40 |
+
|-------------|-------------|
|
| 41 |
+
| **Chain IDs** | Chain IDs (A,B,C..) must be clearly marked in the PDB file. |
|
| 42 |
+
| **Ligands as HETATM** | All ligands must be in **HETATM** records with name and IDs marked (LIG and A). |
|
| 43 |
+
| **Standard amino acids** | AmberMDFlow supports **standard amino acids** only. Non-standard residues and residues with PTMs are currently not supported in the pipeline. |
|
| 44 |
+
|
| 45 |
+
For RCSB structures, the pipeline parses the header and HETATM as provided; for your own PDBs, apply the above conventions.
|
| 46 |
+
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
## Quick Start
|
| 50 |
+
|
| 51 |
+
Try AmberMDFlow instantly on Hugging Face Spaces (no installation required):
|
| 52 |
+
|
| 53 |
+
**[https://huggingface.co/spaces/hemantn/AmberMDFlow](https://huggingface.co/spaces/hemantn/AmberMDFlow)**
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
## Installation
|
| 58 |
+
|
| 59 |
+
### Prerequisites
|
| 60 |
+
|
| 61 |
+
AmberMDFlow requires scientific packages that are only available via **conda** (not PyPI). You must install these first:
|
| 62 |
+
|
| 63 |
+
| Package | Purpose |
|
| 64 |
+
|---------|---------|
|
| 65 |
+
| `ambertools` | AMBER MD tools (tleap, antechamber, sander) |
|
| 66 |
+
| `pymol-open-source` | Structure visualization and editing |
|
| 67 |
+
| `autodock-vina` | AutoDock Vina 1.1.2 molecular docking (from bioconda) |
|
| 68 |
+
| `openbabel` | Molecule format conversion |
|
| 69 |
+
| `rdkit` | Cheminformatics toolkit |
|
| 70 |
+
| `gemmi` | Structure file parsing (required by Meeko) |
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
### Option 1: pip install (recommended)
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
# Step 1: Create conda environment with required tools
|
| 78 |
+
conda create -n ambermdflow python=3.11 -y
|
| 79 |
+
conda activate ambermdflow
|
| 80 |
+
|
| 81 |
+
# Step 2: Install conda-only dependencies
|
| 82 |
+
conda install -c conda-forge -c bioconda ambertools pymol-open-source autodock-vina openbabel rdkit gemmi -y
|
| 83 |
+
|
| 84 |
+
# Step 3: Install AmberMDFlow from Test PyPI
|
| 85 |
+
pip install --extra-index-url https://test.pypi.org/simple/ ambermdflow
|
| 86 |
+
|
| 87 |
+
# Step 4: Run the web app
|
| 88 |
+
ambermdflow
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
Open your browser at **http://localhost:7860**
|
| 92 |
+
|
| 93 |
+
---
|
| 94 |
+
|
| 95 |
+
### Option 2: Docker (no conda/pip needed)
|
| 96 |
+
**build from source:**
|
| 97 |
+
```bash
|
| 98 |
+
git clone https://github.com/nagarh/AmberMDFlow.git
|
| 99 |
+
cd AmberMDFlow
|
| 100 |
+
docker build -t ambermdflow .
|
| 101 |
+
docker run -p 7860:7860 ambermdflow
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
Open your browser at **http://localhost:7860**
|
| 105 |
+
|
| 106 |
+
---
|
| 107 |
+
|
| 108 |
+
### Troubleshooting
|
| 109 |
+
|
| 110 |
+
| Issue | Solution |
|
| 111 |
+
|-------|----------|
|
| 112 |
+
| `ModuleNotFoundError: No module named 'gemmi'` | Run: `conda install -c conda-forge gemmi` |
|
| 113 |
+
| `vina: command not found` | Run: `conda install -c conda-forge vina` |
|
| 114 |
+
| Port 7860 already in use | Kill the process or edit `start_web_server.py` to use a different port |
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
---
|
| 118 |
+
|
| 119 |
+
## Usage
|
| 120 |
+
|
| 121 |
+
### 1. Protein Loading
|
| 122 |
+
|
| 123 |
+
- **Upload**: Drag-and-drop or choose a `.pdb` file.
|
| 124 |
+
- **Fetch**: Enter a 4-character PDB ID (e.g. `1HPV`) to download from RCSB.
|
| 125 |
+
|
| 126 |
+
After loading, the **Protein Preview** shows: structure ID, atom count, chains, residues, water, ions, ligands, and HETATM count. Use the 3D viewer to inspect the structure.
|
| 127 |
+
|
| 128 |
+
---
|
| 129 |
+
|
| 130 |
+
### 2. Fill Missing Residues
|
| 131 |
+
|
| 132 |
+
- Click **Analyze Missing Residues** to detect gaps from RCSB metadata.
|
| 133 |
+
- **Select chains** to complete with ESMFold.
|
| 134 |
+
- **Trim residues** (optional): remove residues from N- or C-terminal edges; internal loops are always filled by ESMFold.
|
| 135 |
+
- **Energy minimization** (optional): if you enable ESMFold completion, you can minimize selected chains to resolve clashes before docking. Recommended if receptor preparation (Meeko) fails later.
|
| 136 |
+
- **Build Completed Structure** to run ESMFold and (if requested) minimization. Use **Preview Completed Structure** and **View Superimposed Structures** to compare original and completed chains.
|
| 137 |
+
|
| 138 |
+
> If you use ESMFold in this workflow, please cite [ESM Atlas](https://esmatlas.com/about).
|
| 139 |
+
|
| 140 |
+
---
|
| 141 |
+
|
| 142 |
+
### 3. Structure Preparation
|
| 143 |
+
|
| 144 |
+
- **Remove**: Water, ions, and hydrogens (options are pre-configured).
|
| 145 |
+
- **Add capping**: ACE (N-terminal) and NME (C-terminal).
|
| 146 |
+
- **Chains**: Select which protein chains to keep for force field generation.
|
| 147 |
+
- **Ligands**:
|
| 148 |
+
- **Preserve ligands** to keep them in the structure.
|
| 149 |
+
- **Select ligands to preserve** (e.g. `GOL-A-1`, `LIZ-A`). Unselected ligands are dropped.
|
| 150 |
+
- **Create separate ligand file** to export selected ligand(s) to a PDB.
|
| 151 |
+
- **Protonate** ligand using Open Babel.
|
| 152 |
+
|
| 153 |
+
Click **Prepare Structure**. The status panel reports original vs prepared atom counts, removed components, added capping, and preserved ligands. Use **View Prepared Structure** and **Download Prepared PDB** as needed.
|
| 154 |
+
|
| 155 |
+
**Ligand Docking** (nested in this tab):
|
| 156 |
+
|
| 157 |
+
- Select ligands to dock.
|
| 158 |
+
- Set the **search space** (center and size in X, Y, Z) with live 3D visualization.
|
| 159 |
+
- **Run Docking** (AutoDock Vina + Meeko). Progress and logs are shown in the docking panel.
|
| 160 |
+
- **Select poses** per ligand and **Use selected pose** to write the chosen pose into the structure for AMBER. You can switch modes (e.g. 1–9) and jump by clicking the mode labels.
|
| 161 |
+
|
| 162 |
+
---
|
| 163 |
+
|
| 164 |
+
### 4. Simulation Parameters
|
| 165 |
+
|
| 166 |
+
- **Force field**: ff14SB or ff19SB.
|
| 167 |
+
- **Water model**: TIP3P or SPCE.
|
| 168 |
+
- **Box size** (Å): padding for solvation.
|
| 169 |
+
- **Add ions**: to neutralize (and optionally reach a salt concentration).
|
| 170 |
+
- **Temperature** and **Pressure** (e.g. 300 K, 1 bar).
|
| 171 |
+
- **Time step** and **Cutoff** for non-bonded interactions.
|
| 172 |
+
|
| 173 |
+
If ligands were preserved, **Ligand force field** (GAFF/GAFF2) is configured here; net charge is computed before `antechamber` runs.
|
| 174 |
+
|
| 175 |
+
---
|
| 176 |
+
|
| 177 |
+
### 5. Simulation Steps
|
| 178 |
+
|
| 179 |
+
Enable/disable and set parameters for:
|
| 180 |
+
|
| 181 |
+
- **Restrained minimization** (steps, force constant)
|
| 182 |
+
- **Minimization** (steps, cutoff)
|
| 183 |
+
- **NVT heating** (steps, temperature)
|
| 184 |
+
- **NPT equilibration** (steps, temperature, pressure)
|
| 185 |
+
- **Production** (steps, temperature, pressure)
|
| 186 |
+
|
| 187 |
+
---
|
| 188 |
+
|
| 189 |
+
### 6. Generate Files
|
| 190 |
+
|
| 191 |
+
- **Generate All Files** to create AMBER inputs (`min_restrained.in`, `min.in`, `HeatNPT.in`, `mdin_equi.in`, `mdin_prod.in`), `tleap` scripts, `submit_job.pbs`, and (after `tleap`) `prmtop`/`inpcrd`.
|
| 192 |
+
- **Preview Files** to open and **edit** each file (e.g. `min.in`, `submit_job.pbs`) and **Save**; changes are written to the output directory.
|
| 193 |
+
- **Preview Solvated Protein** / **Download Solvated Protein** to inspect and download the solvated system.
|
| 194 |
+
|
| 195 |
+
For **PLUMED-based runs**, go to the **PLUMED** tab to configure CVs and `plumed.dat`, then use **Generate simulation files** there to produce inputs that include PLUMED.
|
| 196 |
+
|
| 197 |
+
---
|
| 198 |
+
|
| 199 |
+
### 7. PLUMED
|
| 200 |
+
|
| 201 |
+
- **Collective Variables**: search and select CVs from the PLUMED v2.9 set; view docs and add/edit lines in `plumed.dat`.
|
| 202 |
+
- **Custom PLUMED**: edit `plumed.dat` directly.
|
| 203 |
+
- **Generate simulation files**: create AMBER + PLUMED input files. Generated files can be **previewed, edited, and saved** as in the main **Generate Files** tab.
|
| 204 |
+
|
| 205 |
+
> PLUMED citation: [plumed.org/cite](https://www.plumed.org/cite).
|
| 206 |
+
|
| 207 |
+
---
|
| 208 |
+
|
| 209 |
+
## Pipeline Overview
|
| 210 |
+
|
| 211 |
+
```
|
| 212 |
+
Protein Loading (upload/fetch)
|
| 213 |
+
↓
|
| 214 |
+
Fill Missing Residues (detect → ESMFold → optional trim & minimize)
|
| 215 |
+
↓
|
| 216 |
+
Structure Preparation (clean, cap, chains, ligands) → optional Docking (Vina, select pose)
|
| 217 |
+
↓
|
| 218 |
+
Simulation Parameters (FF, water, box, T, P, etc.)
|
| 219 |
+
↓
|
| 220 |
+
Simulation Steps (min, NVT, NPT, prod)
|
| 221 |
+
↓
|
| 222 |
+
Generate Files (AMBER .in, tleap, prmtop/inpcrd, PBS)
|
| 223 |
+
↓
|
| 224 |
+
[Optional] PLUMED (CVs, plumed.dat, generate PLUMED-enabled files)
|
| 225 |
+
```
|
| 226 |
+
|
| 227 |
+
---
|
| 228 |
+
|
| 229 |
+
## Output Layout
|
| 230 |
+
|
| 231 |
+
Generated files are written under `output/` (or the path set in the app), for example:
|
| 232 |
+
|
| 233 |
+
- `0_original_input.pdb` — raw input
|
| 234 |
+
- `1_protein_no_hydrogens.pdb` — cleaned, capped, chain/ligand selection applied
|
| 235 |
+
- `2_protein_with_caps.pdb`, `tleap_ready.pdb` — intermediates
|
| 236 |
+
- `4_ligands_corrected_*.pdb` — prepared ligands
|
| 237 |
+
- `protein.prmtop`, `protein.inpcrd` — after `tleap`
|
| 238 |
+
- `min_restrained.in`, `min.in`, `HeatNPT.in`, `mdin_equi.in`, `mdin_prod.in`, `submit_job.pbs`
|
| 239 |
+
- `output/docking/` — receptor, ligands, Vina configs, poses, logs
|
| 240 |
+
- `plumed.dat` — when using PLUMED
|
| 241 |
+
|
| 242 |
+
---
|
| 243 |
+
|
| 244 |
+
## Multi-user deployment (e.g. Hugging Face Spaces)
|
| 245 |
+
|
| 246 |
+
When multiple users use the app at the same time (e.g. on Hugging Face Spaces), each user gets an **isolated output folder** so one user’s files are not overwritten by another’s. The app assigns a session ID when the page loads; all API requests send this ID and generated files are stored under `output/<session_id>/`. No configuration is required—this works automatically in multi-user and single-user setups.
|
| 247 |
+
|
| 248 |
+
---
|
| 249 |
+
|
| 250 |
+
## Dependencies
|
| 251 |
+
|
| 252 |
+
| Category | Tools / libraries |
|
| 253 |
+
|----------|-------------------|
|
| 254 |
+
| **Python** | Flask, Flask-CORS, BioPython, NumPy, Pandas, Matplotlib, Seaborn, MDAnalysis, Requests, RDKit, SciPy |
|
| 255 |
+
| **AMBER** | AMBER Tools (tleap, antechamber, sander, ambpdb, etc.) |
|
| 256 |
+
| **Docking** | Meeko (`mk_prepare_ligand`, `mk_prepare_receptor`), AutoDock Vina, Open Babel |
|
| 257 |
+
| **Visualization** | PyMOL (scripted for H removal, structure editing), NGL (in-browser 3D) |
|
| 258 |
+
| **Structure completion** | ESMFold (via API or local, depending on deployment) |
|
| 259 |
+
|
| 260 |
+
---
|
| 261 |
+
|
| 262 |
+
## Project Structure
|
| 263 |
+
|
| 264 |
+
```
|
| 265 |
+
AmberMDFlow/
|
| 266 |
+
├── start_web_server.py # Entry point
|
| 267 |
+
├── html/
|
| 268 |
+
│ ├── index.html # Main UI
|
| 269 |
+
│ └── plumed.html # PLUMED-focused view (if used)
|
| 270 |
+
├── css/
|
| 271 |
+
│ ├── styles.css
|
| 272 |
+
│ └── plumed.css
|
| 273 |
+
├── js/
|
| 274 |
+
│ ├── script.js # Main frontend logic
|
| 275 |
+
│ ├── plumed.js # PLUMED + docking UI
|
| 276 |
+
│ └── plumed_cv_docs.js # CV documentation
|
| 277 |
+
├── python/
|
| 278 |
+
│ ├── app.py # Flask backend, API, file generation
|
| 279 |
+
│ ├── structure_preparation.py
|
| 280 |
+
│ ├── add_caps.py # ACE/NME capping
|
| 281 |
+
│ ├── Fill_missing_residues.py # ESMFold, trimming, minimization
|
| 282 |
+
│ ├── docking.py # Docking helpers
|
| 283 |
+
│ └── docking_utils.py
|
| 284 |
+
├── output/ # Generated files (gitignored in dev)
|
| 285 |
+
├── Dockerfile
|
| 286 |
+
└── README.md
|
| 287 |
+
```
|
| 288 |
+
|
| 289 |
+
---
|
| 290 |
+
|
| 291 |
+
## Citation
|
| 292 |
+
|
| 293 |
+
If you use AmberMDFlow in your work, please cite:
|
| 294 |
+
|
| 295 |
+
```bibtex
|
| 296 |
+
@software{AmberMDFlow,
|
| 297 |
+
title = {AmberMDFlow: Molecular Dynamics and Docking Pipeline},
|
| 298 |
+
author = {Nagar, Hemant},
|
| 299 |
+
year = {2025},
|
| 300 |
+
url = {https://github.com/nagarh/AmberMDFlow}
|
| 301 |
+
```
|
| 302 |
+
|
| 303 |
+
**Related software to cite when used:**
|
| 304 |
+
|
| 305 |
+
- **AMBER**: [ambermd.org](https://ambermd.org)
|
| 306 |
+
- **PLUMED**: [plumed.org/cite](https://www.plumed.org/cite)
|
| 307 |
+
- **ESMFold / ESM Atlas**: [esmatlas.com/about](https://esmatlas.com/about)
|
| 308 |
+
- **AutoDock Vina**: [autodock-vina/cite](https://autodock-vina.readthedocs.io/en/latest/citations.html)
|
| 309 |
+
- **Meeko**: [github.com/forlilab/Meeko](https://github.com/forlilab/Meeko)
|
| 310 |
+
- **MDAnalysis**: [mdanalysis/cite](https://www.mdanalysis.org/pages/citations/)
|
| 311 |
+
- **NGL Viewer**: [nglviewer/cite](https://doi.org/10.1093/bioinformatics/bty419)
|
| 312 |
+
- **PyMOL**: [pymol/cite](https://www.pymol.org/support.html)
|
| 313 |
+
|
| 314 |
+
---
|
| 315 |
+
|
| 316 |
+
## Acknowledgments
|
| 317 |
+
|
| 318 |
+
- **Mohd Ibrahim** (Technical University of Munich) for the protein capping logic (`add_caps.py`).
|
| 319 |
+
|
| 320 |
+
---
|
| 321 |
+
|
| 322 |
+
## License
|
| 323 |
+
|
| 324 |
+
MIT License. See `LICENSE` for details.
|
| 325 |
+
|
| 326 |
+
---
|
| 327 |
+
|
| 328 |
+
## Contact
|
| 329 |
+
|
| 330 |
+
- **Author**: Hemant Nagar
|
| 331 |
+
- **Email**: hn533621@ohio.edu
|
ambermdflow/Fill_missing_residues.py
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
from collections import defaultdict
|
| 3 |
+
from textwrap import wrap
|
| 4 |
+
import re
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def get_pdb_id_from_pdb_file(pdb_path):
|
| 8 |
+
"""
|
| 9 |
+
Extract the 4-character PDB ID from a PDB file.
|
| 10 |
+
|
| 11 |
+
By convention, PDB files have a line starting with 'HEADER' where
|
| 12 |
+
columns 63–66 contain the PDB ID code.
|
| 13 |
+
|
| 14 |
+
If that cannot be found, this function will raise a ValueError so
|
| 15 |
+
that the pipeline fails loudly instead of silently doing the wrong thing.
|
| 16 |
+
"""
|
| 17 |
+
with open(pdb_path, "r") as fh:
|
| 18 |
+
for line in fh:
|
| 19 |
+
if line.startswith("HEADER") and len(line) >= 66:
|
| 20 |
+
pdb_id = line[62:66].strip()
|
| 21 |
+
if pdb_id:
|
| 22 |
+
return pdb_id.upper()
|
| 23 |
+
|
| 24 |
+
raise ValueError(
|
| 25 |
+
f"Could not determine PDB ID from file: {pdb_path}. "
|
| 26 |
+
"Expected a 'HEADER' record with ID in columns 63–66."
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
GRAPHQL_URL = "https://data.rcsb.org/graphql"
|
| 30 |
+
|
| 31 |
+
def detect_missing_residues(pdb_id):
|
| 32 |
+
url = f"https://files.rcsb.org/download/{pdb_id}.pdb"
|
| 33 |
+
response = requests.get(url)
|
| 34 |
+
response.raise_for_status()
|
| 35 |
+
|
| 36 |
+
missing_by_chain = defaultdict(list)
|
| 37 |
+
|
| 38 |
+
for line in response.text.splitlines():
|
| 39 |
+
if line.startswith("REMARK 465"):
|
| 40 |
+
parts = line.split()
|
| 41 |
+
if len(parts) >= 5 and parts[2].isalpha():
|
| 42 |
+
resname = parts[2]
|
| 43 |
+
chain = parts[3]
|
| 44 |
+
|
| 45 |
+
# Extract residue number (strip insertion code, handle negative numbers)
|
| 46 |
+
match = re.match(r"(-?\d+)", parts[4])
|
| 47 |
+
if match:
|
| 48 |
+
resnum = int(match.group(1))
|
| 49 |
+
missing_by_chain[chain].append((resname, resnum))
|
| 50 |
+
|
| 51 |
+
return dict(missing_by_chain)
|
| 52 |
+
|
| 53 |
+
def get_chain_sequences(pdb_id):
|
| 54 |
+
query = """
|
| 55 |
+
query ChainSequences($pdb_id: String!) {
|
| 56 |
+
entry(entry_id: $pdb_id) {
|
| 57 |
+
polymer_entities {
|
| 58 |
+
entity_poly {
|
| 59 |
+
pdbx_seq_one_letter_code_can
|
| 60 |
+
}
|
| 61 |
+
polymer_entity_instances {
|
| 62 |
+
rcsb_polymer_entity_instance_container_identifiers {
|
| 63 |
+
auth_asym_id
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
r = requests.post(
|
| 72 |
+
GRAPHQL_URL,
|
| 73 |
+
json={"query": query, "variables": {"pdb_id": pdb_id}}
|
| 74 |
+
)
|
| 75 |
+
r.raise_for_status()
|
| 76 |
+
|
| 77 |
+
chain_seqs = {}
|
| 78 |
+
|
| 79 |
+
for entity in r.json()["data"]["entry"]["polymer_entities"]:
|
| 80 |
+
seq = entity["entity_poly"]["pdbx_seq_one_letter_code_can"]
|
| 81 |
+
for inst in entity["polymer_entity_instances"]:
|
| 82 |
+
chain = inst[
|
| 83 |
+
"rcsb_polymer_entity_instance_container_identifiers"
|
| 84 |
+
]["auth_asym_id"]
|
| 85 |
+
chain_seqs[chain] = seq
|
| 86 |
+
|
| 87 |
+
return chain_seqs
|
| 88 |
+
|
| 89 |
+
def trim_residues_from_edges(sequence, n_terminal_trim=0, c_terminal_trim=0):
|
| 90 |
+
"""
|
| 91 |
+
Trim residues from the edges (N-terminal and C-terminal) of a sequence.
|
| 92 |
+
Only trims from the edges, not from loops in between.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
sequence: str
|
| 96 |
+
The amino acid sequence to trim
|
| 97 |
+
n_terminal_trim: int
|
| 98 |
+
Number of residues to remove from the N-terminal (start)
|
| 99 |
+
c_terminal_trim: int
|
| 100 |
+
Number of residues to remove from the C-terminal (end)
|
| 101 |
+
|
| 102 |
+
Returns:
|
| 103 |
+
str: The trimmed sequence
|
| 104 |
+
|
| 105 |
+
Raises:
|
| 106 |
+
ValueError: If trim counts exceed sequence length or are negative
|
| 107 |
+
"""
|
| 108 |
+
if n_terminal_trim < 0 or c_terminal_trim < 0:
|
| 109 |
+
raise ValueError("Trim counts must be non-negative")
|
| 110 |
+
|
| 111 |
+
if n_terminal_trim + c_terminal_trim >= len(sequence):
|
| 112 |
+
raise ValueError(
|
| 113 |
+
f"Total trim count ({n_terminal_trim + c_terminal_trim}) exceeds sequence length ({len(sequence)})"
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
# Trim from N-terminal (start) and C-terminal (end)
|
| 117 |
+
trimmed = sequence[n_terminal_trim:len(sequence) - c_terminal_trim]
|
| 118 |
+
|
| 119 |
+
return trimmed
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def trim_chains_sequences(chains_with_sequences, trim_specs):
|
| 123 |
+
"""
|
| 124 |
+
Apply trimming to multiple chain sequences based on specifications.
|
| 125 |
+
|
| 126 |
+
Args:
|
| 127 |
+
chains_with_sequences: dict
|
| 128 |
+
Dictionary mapping chain IDs to sequences
|
| 129 |
+
Example: {'A': 'MKTAYIAKQR...', 'B': 'MKTAYIAKQR...'}
|
| 130 |
+
trim_specs: dict
|
| 131 |
+
Dictionary mapping chain IDs to trim specifications
|
| 132 |
+
Each specification is a dict with 'n_terminal' and/or 'c_terminal' keys
|
| 133 |
+
Example: {'A': {'n_terminal': 5, 'c_terminal': 3}, 'B': {'n_terminal': 2}}
|
| 134 |
+
|
| 135 |
+
Returns:
|
| 136 |
+
dict: Dictionary mapping chain IDs to trimmed sequences
|
| 137 |
+
"""
|
| 138 |
+
trimmed_chains = {}
|
| 139 |
+
|
| 140 |
+
for chain, sequence in chains_with_sequences.items():
|
| 141 |
+
if chain in trim_specs:
|
| 142 |
+
spec = trim_specs[chain]
|
| 143 |
+
n_term = spec.get('n_terminal', 0)
|
| 144 |
+
c_term = spec.get('c_terminal', 0)
|
| 145 |
+
|
| 146 |
+
try:
|
| 147 |
+
trimmed_seq = trim_residues_from_edges(sequence, n_term, c_term)
|
| 148 |
+
trimmed_chains[chain] = trimmed_seq
|
| 149 |
+
except ValueError as e:
|
| 150 |
+
raise ValueError(f"Error trimming chain {chain}: {str(e)}")
|
| 151 |
+
else:
|
| 152 |
+
# No trimming specified for this chain, keep original
|
| 153 |
+
trimmed_chains[chain] = sequence
|
| 154 |
+
|
| 155 |
+
return trimmed_chains
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def write_fasta_for_missing_chains(pdb_id, chains_with_missing, output_dir=None):
|
| 159 |
+
"""
|
| 160 |
+
Write FASTA file for chains with missing residues.
|
| 161 |
+
|
| 162 |
+
Args:
|
| 163 |
+
pdb_id: PDB identifier
|
| 164 |
+
chains_with_missing: Dictionary mapping chain IDs to sequences
|
| 165 |
+
output_dir: Optional output directory. If None, writes to current directory.
|
| 166 |
+
"""
|
| 167 |
+
filename = f"{pdb_id}_chains_with_missing.fasta"
|
| 168 |
+
|
| 169 |
+
if output_dir:
|
| 170 |
+
from pathlib import Path
|
| 171 |
+
output_path = Path(output_dir) / filename
|
| 172 |
+
else:
|
| 173 |
+
output_path = filename
|
| 174 |
+
|
| 175 |
+
with open(output_path, "w") as f:
|
| 176 |
+
for chain, seq in chains_with_missing.items():
|
| 177 |
+
f.write(f">{pdb_id.upper()}_{chain}\n")
|
| 178 |
+
for line in wrap(seq, 60):
|
| 179 |
+
f.write(line + "\n")
|
| 180 |
+
|
| 181 |
+
print(f"Wrote FASTA: {output_path}")
|
| 182 |
+
|
| 183 |
+
def run_esmfold(sequence):
|
| 184 |
+
response = requests.post(
|
| 185 |
+
"https://api.esmatlas.com/foldSequence/v1/pdb/",
|
| 186 |
+
data=sequence,
|
| 187 |
+
timeout=300
|
| 188 |
+
)
|
| 189 |
+
response.raise_for_status()
|
| 190 |
+
return response.text
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def merge_non_protein_atoms(original_pdb_path, protein_pdb_path, output_pdb_path, chains_to_replace):
|
| 194 |
+
"""
|
| 195 |
+
Add non-protein atoms (water, ions, ligands) from original file to the completed protein structure.
|
| 196 |
+
|
| 197 |
+
Parameters:
|
| 198 |
+
-----------
|
| 199 |
+
original_pdb_path : str
|
| 200 |
+
Path to the original PDB file
|
| 201 |
+
protein_pdb_path : str
|
| 202 |
+
Path to the temporary protein-only PDB file
|
| 203 |
+
output_pdb_path : str
|
| 204 |
+
Path where the final merged PDB will be written
|
| 205 |
+
chains_to_replace : list[str]
|
| 206 |
+
List of chain IDs that were replaced by ESMFold (not used, kept for compatibility)
|
| 207 |
+
"""
|
| 208 |
+
import os
|
| 209 |
+
|
| 210 |
+
# Extract non-protein atoms (HETATM records) from original PDB
|
| 211 |
+
non_protein_atoms = []
|
| 212 |
+
|
| 213 |
+
if not os.path.exists(original_pdb_path):
|
| 214 |
+
print(f"Warning: Original PDB file not found: {original_pdb_path}")
|
| 215 |
+
# Just copy the protein file if original doesn't exist
|
| 216 |
+
if os.path.exists(protein_pdb_path):
|
| 217 |
+
import shutil
|
| 218 |
+
shutil.copy2(protein_pdb_path, output_pdb_path)
|
| 219 |
+
return
|
| 220 |
+
|
| 221 |
+
# Read HETATM records from original PDB
|
| 222 |
+
with open(original_pdb_path, 'r') as f:
|
| 223 |
+
for line in f:
|
| 224 |
+
if line.startswith('HETATM'):
|
| 225 |
+
# Include all HETATM records (water, ions, ligands)
|
| 226 |
+
non_protein_atoms.append(line)
|
| 227 |
+
|
| 228 |
+
# Read the completed protein structure
|
| 229 |
+
if not os.path.exists(protein_pdb_path):
|
| 230 |
+
print(f"Error: Protein PDB file not found: {protein_pdb_path}")
|
| 231 |
+
return
|
| 232 |
+
|
| 233 |
+
# Write merged PDB file: protein structure + non-protein atoms
|
| 234 |
+
with open(output_pdb_path, 'w') as f:
|
| 235 |
+
# Write the completed protein structure (all lines except END)
|
| 236 |
+
with open(protein_pdb_path, 'r') as protein_file:
|
| 237 |
+
for line in protein_file:
|
| 238 |
+
if not line.startswith('END'):
|
| 239 |
+
f.write(line)
|
| 240 |
+
|
| 241 |
+
# Add non-protein atoms (water, ions, ligands) from original
|
| 242 |
+
for line in non_protein_atoms:
|
| 243 |
+
f.write(line)
|
| 244 |
+
|
| 245 |
+
# Write END record at the very end
|
| 246 |
+
f.write("END \n")
|
| 247 |
+
|
| 248 |
+
print(f"✅ Added {len(non_protein_atoms)} non-protein atoms to completed structure")
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def rebuild_pdb_with_esmfold(
|
| 252 |
+
pdb_id,
|
| 253 |
+
chains_to_replace,
|
| 254 |
+
output_pdb=None,
|
| 255 |
+
original_pdb_path=None,
|
| 256 |
+
chains_use_minimized=None,
|
| 257 |
+
):
|
| 258 |
+
"""
|
| 259 |
+
pdb_id: str
|
| 260 |
+
Original crystal structure object name (e.g. '3hhr')
|
| 261 |
+
|
| 262 |
+
chains_to_replace: list[str]
|
| 263 |
+
Chains that were missing residues and replaced by ESMFold
|
| 264 |
+
Example: ['A', 'B', 'C']
|
| 265 |
+
|
| 266 |
+
output_pdb: str, optional
|
| 267 |
+
Output PDB filename.
|
| 268 |
+
|
| 269 |
+
original_pdb_path: str, optional
|
| 270 |
+
Path to the original PDB file that should be loaded into PyMOL
|
| 271 |
+
as the reference object named `pdb_id`. If None, defaults to
|
| 272 |
+
'../../output/0_original_input.pdb'.
|
| 273 |
+
|
| 274 |
+
chains_use_minimized: list[str], optional
|
| 275 |
+
For these chains, load the superimposed minimized PDB
|
| 276 |
+
({pdb_id}_chain_{c}_esmfold_minimized_noH.pdb) instead of the
|
| 277 |
+
ESMFold PDB. The minimized structure is aligned to the original
|
| 278 |
+
the same way as ESMFold (CA-based superimposition).
|
| 279 |
+
"""
|
| 280 |
+
|
| 281 |
+
from pymol import cmd
|
| 282 |
+
|
| 283 |
+
# -----------------------------
|
| 284 |
+
# 0. Clean up any existing objects with the same names
|
| 285 |
+
# -----------------------------
|
| 286 |
+
try:
|
| 287 |
+
# Delete existing objects if they exist
|
| 288 |
+
existing_objects = cmd.get_object_list()
|
| 289 |
+
if pdb_id in existing_objects:
|
| 290 |
+
cmd.delete(pdb_id)
|
| 291 |
+
|
| 292 |
+
# Delete any existing ESMFold objects for the chains we're processing
|
| 293 |
+
for chain in chains_to_replace:
|
| 294 |
+
esm_obj = f"{pdb_id}_chain_{chain}_esmfold"
|
| 295 |
+
if esm_obj in existing_objects:
|
| 296 |
+
cmd.delete(esm_obj)
|
| 297 |
+
|
| 298 |
+
# Delete final_model if it exists
|
| 299 |
+
if "final_model" in existing_objects:
|
| 300 |
+
cmd.delete("final_model")
|
| 301 |
+
except Exception as e:
|
| 302 |
+
print(f"Warning: Could not clean up existing objects: {e}")
|
| 303 |
+
|
| 304 |
+
# -----------------------------
|
| 305 |
+
# 1. Load original PDB into PyMOL
|
| 306 |
+
# -----------------------------
|
| 307 |
+
if original_pdb_path is None:
|
| 308 |
+
# Default to the pipeline output location
|
| 309 |
+
original_pdb_path = "../../output/0_original_input.pdb"
|
| 310 |
+
|
| 311 |
+
print(f"Loading original PDB from {original_pdb_path} as object '{pdb_id}'")
|
| 312 |
+
cmd.load(original_pdb_path, pdb_id)
|
| 313 |
+
|
| 314 |
+
if output_pdb is None:
|
| 315 |
+
output_pdb = f"{pdb_id}_rebuilt.pdb"
|
| 316 |
+
|
| 317 |
+
# -----------------------------
|
| 318 |
+
# 2. Align each ESMFold (or minimized) chain and fix chain IDs
|
| 319 |
+
# -----------------------------
|
| 320 |
+
for chain in chains_to_replace:
|
| 321 |
+
esm_obj = f"{pdb_id}_chain_{chain}_esmfold"
|
| 322 |
+
|
| 323 |
+
# For minimized chains, use the superimposed minimized noH PDB
|
| 324 |
+
# (minimization writes in a different frame; we align it to original here).
|
| 325 |
+
if chains_use_minimized and chain in chains_use_minimized:
|
| 326 |
+
esm_pdb_filename = f"{pdb_id}_chain_{chain}_esmfold_minimized_noH.pdb"
|
| 327 |
+
print(f"Loading minimized PDB {esm_pdb_filename} as object '{esm_obj}' (will superimpose to original)")
|
| 328 |
+
else:
|
| 329 |
+
esm_pdb_filename = f"{pdb_id}_chain_{chain}_esmfold.pdb"
|
| 330 |
+
print(f"Loading ESMFold PDB {esm_pdb_filename} as object '{esm_obj}'")
|
| 331 |
+
cmd.load(esm_pdb_filename, esm_obj)
|
| 332 |
+
|
| 333 |
+
# ESMFold outputs everything as chain A by default.
|
| 334 |
+
# Rename the chain in the loaded object to match the target chain ID.
|
| 335 |
+
print(f"Renaming chain A -> {chain} in {esm_obj}")
|
| 336 |
+
cmd.alter(esm_obj, f"chain='{chain}'")
|
| 337 |
+
cmd.sort(esm_obj) # Rebuild internal indices after alter
|
| 338 |
+
|
| 339 |
+
align_cmd = (
|
| 340 |
+
f"{esm_obj} and name CA",
|
| 341 |
+
f"{pdb_id} and chain {chain} and name CA"
|
| 342 |
+
)
|
| 343 |
+
|
| 344 |
+
print(f"Aligning {esm_obj} to {pdb_id} chain {chain}")
|
| 345 |
+
cmd.align(*align_cmd)
|
| 346 |
+
|
| 347 |
+
# -----------------------------
|
| 348 |
+
# 3. Build selection strings
|
| 349 |
+
# -----------------------------
|
| 350 |
+
chains_str = "+".join(chains_to_replace)
|
| 351 |
+
|
| 352 |
+
esm_objs_str = " or ".join(
|
| 353 |
+
f"{pdb_id}_chain_{chain}_esmfold"
|
| 354 |
+
for chain in chains_to_replace
|
| 355 |
+
)
|
| 356 |
+
|
| 357 |
+
selection = (
|
| 358 |
+
f"({pdb_id} and not chain {chains_str}) or "
|
| 359 |
+
f"({esm_objs_str})"
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
# -----------------------------
|
| 363 |
+
# 4. Create final model
|
| 364 |
+
# -----------------------------
|
| 365 |
+
cmd.select("final_model", selection)
|
| 366 |
+
|
| 367 |
+
# -----------------------------
|
| 368 |
+
# 5. Save rebuilt structure (protein only)
|
| 369 |
+
# -----------------------------
|
| 370 |
+
import os
|
| 371 |
+
temp_protein_pdb = output_pdb.replace('.pdb', '_protein_temp.pdb')
|
| 372 |
+
cmd.save(temp_protein_pdb, "final_model")
|
| 373 |
+
|
| 374 |
+
# -----------------------------
|
| 375 |
+
# 6. Add non-protein atoms from original PDB
|
| 376 |
+
# -----------------------------
|
| 377 |
+
print(f"Adding non-protein atoms from original file...")
|
| 378 |
+
# Convert paths to absolute paths if they're relative
|
| 379 |
+
abs_original = os.path.abspath(original_pdb_path) if original_pdb_path else None
|
| 380 |
+
abs_temp = os.path.abspath(temp_protein_pdb)
|
| 381 |
+
abs_output = os.path.abspath(output_pdb)
|
| 382 |
+
merge_non_protein_atoms(abs_original, abs_temp, abs_output, chains_to_replace)
|
| 383 |
+
|
| 384 |
+
# Clean up temporary protein file
|
| 385 |
+
try:
|
| 386 |
+
if os.path.exists(temp_protein_pdb):
|
| 387 |
+
os.remove(temp_protein_pdb)
|
| 388 |
+
except Exception as e:
|
| 389 |
+
print(f"Warning: Could not remove temporary file {temp_protein_pdb}: {e}")
|
| 390 |
+
|
| 391 |
+
# -----------------------------
|
| 392 |
+
# 7. Clean up temporary objects (keep final_model for potential reuse)
|
| 393 |
+
# -----------------------------
|
| 394 |
+
try:
|
| 395 |
+
# Delete the original and ESMFold objects, but keep final_model
|
| 396 |
+
cmd.delete(pdb_id)
|
| 397 |
+
for chain in chains_to_replace:
|
| 398 |
+
esm_obj = f"{pdb_id}_chain_{chain}_esmfold"
|
| 399 |
+
cmd.delete(esm_obj)
|
| 400 |
+
except Exception as e:
|
| 401 |
+
print(f"Warning: Could not clean up temporary objects: {e}")
|
| 402 |
+
|
| 403 |
+
print(f"✅ Final rebuilt structure saved as: {output_pdb}")
|
| 404 |
+
|
| 405 |
+
|
| 406 |
+
if __name__ == "__main__":
|
| 407 |
+
# Path to the original input PDB used by the pipeline
|
| 408 |
+
original_pdb_path = "../../output/0_original_input.pdb"
|
| 409 |
+
|
| 410 |
+
# Automatically infer the PDB ID from the original PDB file,
|
| 411 |
+
# instead of hard-coding it (e.g., '3hhr').
|
| 412 |
+
pdb_id = get_pdb_id_from_pdb_file(original_pdb_path)
|
| 413 |
+
print(f"Detected PDB ID from original file: {pdb_id}")
|
| 414 |
+
|
| 415 |
+
# 1) Find missing residues for this structure
|
| 416 |
+
missing = detect_missing_residues(pdb_id)
|
| 417 |
+
chain_sequences = get_chain_sequences(pdb_id)
|
| 418 |
+
|
| 419 |
+
chains_with_missing = {
|
| 420 |
+
chain: chain_sequences[chain]
|
| 421 |
+
for chain in missing
|
| 422 |
+
if chain in chain_sequences
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
# 2) Write FASTA for chains with missing residues
|
| 426 |
+
write_fasta_for_missing_chains(pdb_id, chains_with_missing)
|
| 427 |
+
|
| 428 |
+
# 3) Run ESMFold for each chain and save results
|
| 429 |
+
esmfold_results = {}
|
| 430 |
+
chains_to_replace = []
|
| 431 |
+
|
| 432 |
+
for chain, seq in chains_with_missing.items():
|
| 433 |
+
print(f"Running ESMFold for chain {chain}")
|
| 434 |
+
pdb_text = run_esmfold(seq)
|
| 435 |
+
esmfold_results[chain] = pdb_text
|
| 436 |
+
chains_to_replace.append(chain)
|
| 437 |
+
# Save each chain
|
| 438 |
+
with open(f"{pdb_id}_chain_{chain}_esmfold.pdb", "w") as f:
|
| 439 |
+
f.write(pdb_text)
|
| 440 |
+
|
| 441 |
+
# 4) Rebuild PDB in PyMOL using original structure and ESMFold chains
|
| 442 |
+
rebuild_pdb_with_esmfold(
|
| 443 |
+
pdb_id,
|
| 444 |
+
chains_to_replace,
|
| 445 |
+
original_pdb_path=original_pdb_path,
|
| 446 |
+
)
|
ambermdflow/__init__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AmberMDFlow: Web-based MD simulation pipeline with AMBER, ESMFold, docking, and PLUMED.
|
| 3 |
+
|
| 4 |
+
AmberMDFlow provides a complete workflow for:
|
| 5 |
+
- Protein structure loading and visualization
|
| 6 |
+
- Missing residue completion with ESMFold
|
| 7 |
+
- Structure preparation (cleaning, capping, chain/ligand selection)
|
| 8 |
+
- Ligand docking with AutoDock Vina + Meeko
|
| 9 |
+
- AMBER force field parameterization
|
| 10 |
+
- MD simulation file generation
|
| 11 |
+
- PLUMED collective variable configuration
|
| 12 |
+
|
| 13 |
+
Usage:
|
| 14 |
+
# Run the web interface
|
| 15 |
+
$ ambermdflow
|
| 16 |
+
# or
|
| 17 |
+
$ python -m ambermdflow
|
| 18 |
+
|
| 19 |
+
# Import in Python
|
| 20 |
+
from ambermdflow.app import app
|
| 21 |
+
from ambermdflow.structure_preparation import prepare_structure
|
| 22 |
+
|
| 23 |
+
Requirements:
|
| 24 |
+
- Python >= 3.10
|
| 25 |
+
- Conda packages: ambertools, pymol-open-source, vina, openbabel, rdkit, gemmi
|
| 26 |
+
- See README.md for full installation instructions
|
| 27 |
+
|
| 28 |
+
License: MIT
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
__version__ = "0.0.1"
|
| 32 |
+
__author__ = "Hemant Nagar"
|
| 33 |
+
__email__ = "hn533621@ohio.edu"
|
| 34 |
+
# Expose key components for programmatic use
|
| 35 |
+
from ambermdflow.app import app
|
| 36 |
+
|
| 37 |
+
__all__ = ["app", "__version__"]
|
ambermdflow/__main__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Run the AmberMDFlow web server. Use: python -m ambermdflow or ambermdflow"""
|
| 2 |
+
|
| 3 |
+
from ambermdflow.app import app
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def main():
|
| 7 |
+
app.run(debug=False, host="0.0.0.0", port=7860)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
if __name__ == "__main__":
|
| 11 |
+
main()
|
ambermdflow/add_caps.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Written by Mohd Ibrahim
|
| 2 |
+
# Technical University of Munich
|
| 3 |
+
# Email: ibrahim.mohd@tum.de
|
| 4 |
+
|
| 5 |
+
import numpy as np
|
| 6 |
+
import MDAnalysis as mda
|
| 7 |
+
import argparse
|
| 8 |
+
import warnings
|
| 9 |
+
warnings.filterwarnings("ignore")
|
| 10 |
+
|
| 11 |
+
np.random.seed(42)
|
| 12 |
+
|
| 13 |
+
parser = argparse.ArgumentParser(
|
| 14 |
+
description="Add capping groups ACE and NME to protein termini. "
|
| 15 |
+
"Remove hydrogens before using this script")
|
| 16 |
+
parser.add_argument('-i', dest='in_file', type=str,
|
| 17 |
+
default='protein_noh.pdb', help='pdb file')
|
| 18 |
+
parser.add_argument('-o', dest='out_file', type=str,
|
| 19 |
+
default='protein_noh_cap.pdb', help='output file')
|
| 20 |
+
|
| 21 |
+
args = parser.parse_args()
|
| 22 |
+
in_file = args.in_file
|
| 23 |
+
out_file = args.out_file
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def create_universe(n_atoms, name, resname, positions, resids, segid):
|
| 27 |
+
u_new = mda.Universe.empty(
|
| 28 |
+
n_atoms=n_atoms,
|
| 29 |
+
n_residues=n_atoms,
|
| 30 |
+
atom_resindex=np.arange(n_atoms),
|
| 31 |
+
residue_segindex=np.arange(n_atoms),
|
| 32 |
+
n_segments=n_atoms,
|
| 33 |
+
trajectory=True
|
| 34 |
+
)
|
| 35 |
+
u_new.add_TopologyAttr('name', name)
|
| 36 |
+
u_new.add_TopologyAttr('resid', resids)
|
| 37 |
+
u_new.add_TopologyAttr('resname', resname)
|
| 38 |
+
u_new.atoms.positions = positions
|
| 39 |
+
u_new.add_TopologyAttr('segid', n_atoms * [segid])
|
| 40 |
+
u_new.add_TopologyAttr('chainID', n_atoms * [segid])
|
| 41 |
+
return u_new
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def get_nme_pos(end_residue):
|
| 45 |
+
if "OXT" in end_residue.names:
|
| 46 |
+
index = np.where(end_residue.names == "OXT")[0][0]
|
| 47 |
+
N_position = end_residue.positions[index]
|
| 48 |
+
index_c = np.where(end_residue.names == "C")[0][0]
|
| 49 |
+
carbon_position = end_residue.positions[index_c]
|
| 50 |
+
vector = N_position - carbon_position
|
| 51 |
+
vector /= np.sqrt(sum(vector**2))
|
| 52 |
+
C_position = N_position + vector * 1.36
|
| 53 |
+
return N_position, C_position
|
| 54 |
+
else:
|
| 55 |
+
index_o = np.where(end_residue.names == "O")[0][0]
|
| 56 |
+
index_ca = np.where(end_residue.names == "CA")[0][0]
|
| 57 |
+
mid_point = (end_residue.positions[index_o] +
|
| 58 |
+
end_residue.positions[index_ca]) / 2
|
| 59 |
+
index_c = np.where(end_residue.names == "C")[0][0]
|
| 60 |
+
vector = end_residue.positions[index_c] - mid_point
|
| 61 |
+
vector /= np.sqrt(sum(vector**2))
|
| 62 |
+
N_position = end_residue.positions[index_c] + 1.36 * vector
|
| 63 |
+
C_position = N_position + 1.36 * vector
|
| 64 |
+
return N_position, C_position
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def get_ace_pos(end_residue):
|
| 68 |
+
index_ca = np.where(end_residue.names == "CA")[0][0]
|
| 69 |
+
index_n = np.where(end_residue.names == "N")[0][0]
|
| 70 |
+
vector = end_residue.positions[index_n] - end_residue.positions[index_ca]
|
| 71 |
+
vector /= np.sqrt(sum(vector**2))
|
| 72 |
+
C1_position = end_residue.positions[index_n] + 1.36 * vector
|
| 73 |
+
|
| 74 |
+
xa, ya, za = end_residue.positions[index_ca]
|
| 75 |
+
xg, yg, zg = C1_position
|
| 76 |
+
|
| 77 |
+
orientation = np.array([2 * np.random.rand() - 1,
|
| 78 |
+
2 * np.random.rand() - 1,
|
| 79 |
+
2 * np.random.rand() - 1])
|
| 80 |
+
nx, ny, nz = orientation / np.sqrt(sum(orientation**2))
|
| 81 |
+
|
| 82 |
+
x1 = xg - (xa - xg) / 2 + np.sqrt(3) * (ny * (za - zg) - nz * (ya - yg)) / 2
|
| 83 |
+
y1 = yg - (ya - yg) / 2 + np.sqrt(3) * (nz * (xa - xg) - nx * (za - zg)) / 2
|
| 84 |
+
z1 = zg - (za - zg) / 2 + np.sqrt(3) * (nx * (ya - yg) - ny * (xa - xg)) / 2
|
| 85 |
+
|
| 86 |
+
x2 = xg - (xa - xg) / 2 - np.sqrt(3) * (ny * (za - zg) - nz * (ya - yg)) / 2
|
| 87 |
+
y2 = yg - (ya - yg) / 2 - np.sqrt(3) * (nz * (xa - xg) - nx * (za - zg)) / 2
|
| 88 |
+
z2 = zg - (za - zg) / 2 - np.sqrt(3) * (nx * (ya - yg) - ny * (xa - xg)) / 2
|
| 89 |
+
|
| 90 |
+
C2_position = np.array([x1, y1, z1])
|
| 91 |
+
O_position = np.array([x2, y2, z2])
|
| 92 |
+
|
| 93 |
+
vector = C2_position - C1_position
|
| 94 |
+
vector /= np.sqrt(sum(vector**2))
|
| 95 |
+
C2_position = C1_position + 1.36 * vector
|
| 96 |
+
|
| 97 |
+
vector = O_position - C1_position
|
| 98 |
+
vector /= np.sqrt(sum(vector**2))
|
| 99 |
+
O_position = C1_position + 1.36 * vector
|
| 100 |
+
|
| 101 |
+
return C1_position, C2_position, O_position
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
# ----------- Main processing -----------
|
| 105 |
+
u = mda.Universe(in_file)
|
| 106 |
+
res_start = 0
|
| 107 |
+
segment_universes = []
|
| 108 |
+
|
| 109 |
+
for seg in u.segments:
|
| 110 |
+
chain = u.select_atoms(f"segid {seg.segid}")
|
| 111 |
+
|
| 112 |
+
# ACE
|
| 113 |
+
resid_c = chain.residues.resids[0]
|
| 114 |
+
end_residue = u.select_atoms(f"segid {seg.segid} and resid {resid_c}")
|
| 115 |
+
c1_pos, c2_pos, o_pos = get_ace_pos(end_residue)
|
| 116 |
+
|
| 117 |
+
# keep original mapping (C, CH3, O)
|
| 118 |
+
ace_names = ["C", "CH3", "O"]
|
| 119 |
+
ace_positions = [c1_pos, c2_pos, o_pos]
|
| 120 |
+
resid = chain.residues.resids[0]
|
| 121 |
+
ace_universe = create_universe(
|
| 122 |
+
n_atoms=len(ace_positions),
|
| 123 |
+
name=ace_names,
|
| 124 |
+
resname=len(ace_names) * ["ACE"],
|
| 125 |
+
positions=ace_positions,
|
| 126 |
+
resids=resid * np.ones(len(ace_names)),
|
| 127 |
+
segid=chain.segids[0]
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
# >>> Reorder rows only: CH3, C, O <<<
|
| 131 |
+
ace_universe = mda.Merge(
|
| 132 |
+
ace_universe.atoms.select_atoms("name CH3"),
|
| 133 |
+
ace_universe.atoms.select_atoms("name C"),
|
| 134 |
+
ace_universe.atoms.select_atoms("name O")
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
# NME
|
| 138 |
+
resid_c = chain.residues.resids[-1]
|
| 139 |
+
end_residue = u.select_atoms(f"segid {seg.segid} and resid {resid_c}")
|
| 140 |
+
nme_positions = get_nme_pos(end_residue)
|
| 141 |
+
nme_names = ["N", "C"]
|
| 142 |
+
resid = chain.residues.resids[-1] + 2
|
| 143 |
+
nme_universe = create_universe(
|
| 144 |
+
n_atoms=len(nme_names),
|
| 145 |
+
name=nme_names,
|
| 146 |
+
resname=len(nme_names) * ["NME"],
|
| 147 |
+
positions=nme_positions,
|
| 148 |
+
resids=resid * np.ones(len(nme_names)),
|
| 149 |
+
segid=chain.segids[0]
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
# Remove OXT if present
|
| 153 |
+
if "OXT" in end_residue.names:
|
| 154 |
+
index = np.where(end_residue.names == "OXT")[0][0]
|
| 155 |
+
OXT = end_residue[index]
|
| 156 |
+
Chain = u.select_atoms(f"segid {seg.segid} and not index {OXT.index}")
|
| 157 |
+
else:
|
| 158 |
+
Chain = u.select_atoms(f"segid {seg.segid}")
|
| 159 |
+
|
| 160 |
+
# Merge ACE, protein, NME
|
| 161 |
+
u_all = mda.Merge(ace_universe.atoms, Chain, nme_universe.atoms)
|
| 162 |
+
|
| 163 |
+
# Renumber residues
|
| 164 |
+
resids_ace = [res_start + 1] * 3
|
| 165 |
+
resids_pro = np.arange(resids_ace[0] + 1,
|
| 166 |
+
Chain.residues.n_residues + resids_ace[0] + 1)
|
| 167 |
+
resids_nme = [resids_pro[-1] + 1] * 2
|
| 168 |
+
u_all.atoms.residues.resids = np.concatenate(
|
| 169 |
+
[resids_ace, resids_pro, resids_nme]
|
| 170 |
+
)
|
| 171 |
+
res_start = u_all.atoms.residues.resids[-1]
|
| 172 |
+
segment_universes.append(u_all)
|
| 173 |
+
|
| 174 |
+
# Join and write output
|
| 175 |
+
all_uni = mda.Merge(*(seg.atoms for seg in segment_universes))
|
| 176 |
+
all_uni.atoms.write(out_file)
|
ambermdflow/app.py
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
ambermdflow/css/plumed.css
ADDED
|
@@ -0,0 +1,1064 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* PLUMED Section Styles */
|
| 2 |
+
|
| 3 |
+
/* PLUMED Citation Note */
|
| 4 |
+
.plumed-citation-note {
|
| 5 |
+
background: #e3f2fd;
|
| 6 |
+
border: 2px solid #2196f3;
|
| 7 |
+
border-left: 5px solid #2196f3;
|
| 8 |
+
border-radius: 8px;
|
| 9 |
+
padding: 1rem 1.5rem;
|
| 10 |
+
margin-bottom: 1.5rem;
|
| 11 |
+
display: flex;
|
| 12 |
+
align-items: flex-start;
|
| 13 |
+
gap: 1rem;
|
| 14 |
+
box-shadow: 0 2px 4px rgba(33, 150, 243, 0.1);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.plumed-citation-note i {
|
| 18 |
+
color: #2196f3;
|
| 19 |
+
font-size: 1.5rem;
|
| 20 |
+
margin-top: 0.2rem;
|
| 21 |
+
flex-shrink: 0;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.citation-content {
|
| 25 |
+
flex: 1;
|
| 26 |
+
color: #1565c0;
|
| 27 |
+
line-height: 1.6;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.citation-content p {
|
| 31 |
+
margin: 0.5rem 0;
|
| 32 |
+
font-size: 0.95rem;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.citation-content p:first-child {
|
| 36 |
+
margin-top: 0;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.citation-content p:last-child {
|
| 40 |
+
margin-bottom: 0;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.citation-content strong {
|
| 44 |
+
color: #0d47a1;
|
| 45 |
+
font-weight: 600;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.citation-content a {
|
| 49 |
+
color: #1976d2;
|
| 50 |
+
text-decoration: none;
|
| 51 |
+
font-weight: 500;
|
| 52 |
+
transition: color 0.3s ease;
|
| 53 |
+
display: inline-flex;
|
| 54 |
+
align-items: center;
|
| 55 |
+
gap: 0.25rem;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.citation-content a:hover {
|
| 59 |
+
color: #0d47a1;
|
| 60 |
+
text-decoration: underline;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.citation-content a i {
|
| 64 |
+
font-size: 0.85rem;
|
| 65 |
+
color: inherit;
|
| 66 |
+
margin: 0;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.plumed-container {
|
| 70 |
+
display: flex;
|
| 71 |
+
gap: 2rem;
|
| 72 |
+
margin-top: 1.5rem;
|
| 73 |
+
min-height: 600px;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* Left Sidebar */
|
| 77 |
+
.plumed-sidebar {
|
| 78 |
+
width: 300px;
|
| 79 |
+
background: #f8f9fa;
|
| 80 |
+
border-radius: 8px;
|
| 81 |
+
border: 1px solid #dee2e6;
|
| 82 |
+
display: flex;
|
| 83 |
+
flex-direction: column;
|
| 84 |
+
max-height: 800px;
|
| 85 |
+
overflow: hidden;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.sidebar-header {
|
| 89 |
+
padding: 1rem;
|
| 90 |
+
background: #2c3e50;
|
| 91 |
+
color: white;
|
| 92 |
+
border-bottom: 2px solid #3498db;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.sidebar-header h3 {
|
| 96 |
+
margin: 0 0 1rem 0;
|
| 97 |
+
font-size: 1.2rem;
|
| 98 |
+
font-weight: 600;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.sidebar-header h3 i {
|
| 102 |
+
margin-right: 0.5rem;
|
| 103 |
+
color: #3498db;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.search-box {
|
| 107 |
+
position: relative;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.search-input {
|
| 111 |
+
width: 100%;
|
| 112 |
+
padding: 0.5rem 2.5rem 0.5rem 0.75rem;
|
| 113 |
+
border: 1px solid #dee2e6;
|
| 114 |
+
border-radius: 4px;
|
| 115 |
+
font-size: 0.9rem;
|
| 116 |
+
transition: all 0.3s ease;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.search-input:focus {
|
| 120 |
+
outline: none;
|
| 121 |
+
border-color: #3498db;
|
| 122 |
+
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.search-icon {
|
| 126 |
+
position: absolute;
|
| 127 |
+
right: 0.75rem;
|
| 128 |
+
top: 50%;
|
| 129 |
+
transform: translateY(-50%);
|
| 130 |
+
color: #7f8c8d;
|
| 131 |
+
pointer-events: none;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.cv-list {
|
| 135 |
+
flex: 1;
|
| 136 |
+
overflow-y: auto;
|
| 137 |
+
padding: 0.5rem;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.cv-item {
|
| 141 |
+
padding: 0.75rem 1rem;
|
| 142 |
+
margin-bottom: 0.5rem;
|
| 143 |
+
background: white;
|
| 144 |
+
border: 1px solid #dee2e6;
|
| 145 |
+
border-radius: 6px;
|
| 146 |
+
cursor: pointer;
|
| 147 |
+
transition: all 0.3s ease;
|
| 148 |
+
display: flex;
|
| 149 |
+
align-items: center;
|
| 150 |
+
justify-content: space-between;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.cv-item:hover {
|
| 154 |
+
background: #e3f2fd;
|
| 155 |
+
border-color: #3498db;
|
| 156 |
+
transform: translateX(5px);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.cv-item.active {
|
| 160 |
+
background: #3498db;
|
| 161 |
+
color: white;
|
| 162 |
+
border-color: #2980b9;
|
| 163 |
+
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.3);
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.cv-item-name {
|
| 167 |
+
font-weight: 600;
|
| 168 |
+
font-size: 0.95rem;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.cv-item-category {
|
| 172 |
+
font-size: 0.75rem;
|
| 173 |
+
opacity: 0.7;
|
| 174 |
+
margin-top: 0.25rem;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.cv-item.active .cv-item-category {
|
| 178 |
+
opacity: 0.9;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.cv-item-icon {
|
| 182 |
+
color: #3498db;
|
| 183 |
+
margin-left: 0.5rem;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.cv-item.active .cv-item-icon {
|
| 187 |
+
color: white;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
/* Right Content Panel */
|
| 191 |
+
.plumed-content {
|
| 192 |
+
flex: 1;
|
| 193 |
+
background: white;
|
| 194 |
+
border-radius: 8px;
|
| 195 |
+
border: 1px solid #dee2e6;
|
| 196 |
+
padding: 1.5rem;
|
| 197 |
+
overflow-y: auto;
|
| 198 |
+
max-height: 800px;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.content-header {
|
| 202 |
+
display: flex;
|
| 203 |
+
justify-content: space-between;
|
| 204 |
+
align-items: center;
|
| 205 |
+
margin-bottom: 1.5rem;
|
| 206 |
+
padding-bottom: 1rem;
|
| 207 |
+
border-bottom: 2px solid #e1e8ed;
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.content-header h3 {
|
| 211 |
+
margin: 0;
|
| 212 |
+
color: #2c3e50;
|
| 213 |
+
font-size: 1.5rem;
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
gap: 0.5rem;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.plumed-doc-link {
|
| 220 |
+
color: #3498db;
|
| 221 |
+
text-decoration: none;
|
| 222 |
+
font-size: 0.85em;
|
| 223 |
+
margin-left: 0.5rem;
|
| 224 |
+
transition: color 0.3s ease;
|
| 225 |
+
display: inline-flex;
|
| 226 |
+
align-items: center;
|
| 227 |
+
gap: 0.25rem;
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.plumed-doc-link:hover {
|
| 231 |
+
color: #2980b9;
|
| 232 |
+
text-decoration: underline;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.plumed-doc-link i {
|
| 236 |
+
font-size: 0.75em;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.welcome-message {
|
| 240 |
+
text-align: center;
|
| 241 |
+
padding: 3rem 1rem;
|
| 242 |
+
color: #7f8c8d;
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
.welcome-message i {
|
| 246 |
+
color: #bdc3c7;
|
| 247 |
+
margin-bottom: 1rem;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
.welcome-message h3 {
|
| 251 |
+
color: #2c3e50;
|
| 252 |
+
margin: 1rem 0;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
/* Documentation Sections */
|
| 256 |
+
.cv-documentation {
|
| 257 |
+
margin-bottom: 2rem;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.doc-section {
|
| 261 |
+
margin-bottom: 2rem;
|
| 262 |
+
padding: 1.5rem;
|
| 263 |
+
background: #f8f9fa;
|
| 264 |
+
border-radius: 8px;
|
| 265 |
+
border-left: 4px solid #3498db;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
.doc-section h4 {
|
| 269 |
+
color: #2c3e50;
|
| 270 |
+
margin-bottom: 1rem;
|
| 271 |
+
font-size: 1.2rem;
|
| 272 |
+
display: flex;
|
| 273 |
+
align-items: center;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.doc-section h4 {
|
| 277 |
+
display: flex;
|
| 278 |
+
align-items: center;
|
| 279 |
+
margin-bottom: 1rem;
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
.doc-section h4 i {
|
| 283 |
+
margin-right: 0.5rem;
|
| 284 |
+
color: #3498db;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
/* Options heading with legend on the right - only apply space-between to headings with color-legend */
|
| 288 |
+
.doc-section h4 .color-legend {
|
| 289 |
+
margin-left: auto;
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
/* For browsers that support :has() */
|
| 293 |
+
@supports selector(:has(*)) {
|
| 294 |
+
.doc-section h4:has(.color-legend) {
|
| 295 |
+
justify-content: space-between;
|
| 296 |
+
flex-wrap: wrap;
|
| 297 |
+
gap: 1rem;
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
/* Fallback for browsers without :has() support - use a class */
|
| 302 |
+
.doc-section h4.options-heading-with-legend {
|
| 303 |
+
justify-content: space-between;
|
| 304 |
+
flex-wrap: wrap;
|
| 305 |
+
gap: 1rem;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.color-legend {
|
| 309 |
+
display: flex;
|
| 310 |
+
align-items: center;
|
| 311 |
+
gap: 1rem;
|
| 312 |
+
font-size: 0.85rem;
|
| 313 |
+
font-weight: normal;
|
| 314 |
+
color: #7f8c8d;
|
| 315 |
+
margin-left: auto;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.legend-item {
|
| 319 |
+
display: flex;
|
| 320 |
+
align-items: center;
|
| 321 |
+
gap: 0.5rem;
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
.legend-item code {
|
| 325 |
+
font-size: 0.8rem;
|
| 326 |
+
padding: 0.25rem 0.5rem;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.doc-content {
|
| 330 |
+
color: #555;
|
| 331 |
+
line-height: 1.8;
|
| 332 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.description-paragraph {
|
| 336 |
+
margin-bottom: 1rem;
|
| 337 |
+
line-height: 1.8;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.description-list {
|
| 341 |
+
list-style: none;
|
| 342 |
+
padding-left: 0;
|
| 343 |
+
margin: 1rem 0;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.description-list li {
|
| 347 |
+
padding: 0.5rem 0 0.5rem 1.5rem;
|
| 348 |
+
position: relative;
|
| 349 |
+
line-height: 1.8;
|
| 350 |
+
margin-bottom: 0.5rem;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.description-list li::before {
|
| 354 |
+
content: '•';
|
| 355 |
+
position: absolute;
|
| 356 |
+
left: 0;
|
| 357 |
+
color: #3498db;
|
| 358 |
+
font-size: 1.2em;
|
| 359 |
+
font-weight: bold;
|
| 360 |
+
line-height: 1.4;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
.description-list li:last-child {
|
| 364 |
+
margin-bottom: 0;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
/* Ensure Unicode mathematical symbols render correctly */
|
| 368 |
+
.doc-content,
|
| 369 |
+
.math-formula {
|
| 370 |
+
font-variant-numeric: normal;
|
| 371 |
+
text-rendering: optimizeLegibility;
|
| 372 |
+
-webkit-font-feature-settings: "kern" 1;
|
| 373 |
+
font-feature-settings: "kern" 1;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.syntax-box,
|
| 377 |
+
.example-box {
|
| 378 |
+
background: #2c3e50;
|
| 379 |
+
color: #ecf0f1;
|
| 380 |
+
padding: 1rem;
|
| 381 |
+
border-radius: 6px;
|
| 382 |
+
overflow-x: auto;
|
| 383 |
+
font-family: 'Courier New', monospace;
|
| 384 |
+
font-size: 0.9rem;
|
| 385 |
+
line-height: 1.6;
|
| 386 |
+
margin: 0;
|
| 387 |
+
white-space: pre-wrap;
|
| 388 |
+
word-wrap: break-word;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.example-box {
|
| 392 |
+
background: #34495e;
|
| 393 |
+
border-left: 4px solid #3498db;
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
/* Editor Section */
|
| 397 |
+
.cv-editor-section {
|
| 398 |
+
margin-top: 2rem;
|
| 399 |
+
border-top: 2px solid #e1e8ed;
|
| 400 |
+
padding-top: 1.5rem;
|
| 401 |
+
}
|
| 402 |
+
|
| 403 |
+
.editor-header {
|
| 404 |
+
display: flex;
|
| 405 |
+
justify-content: space-between;
|
| 406 |
+
align-items: center;
|
| 407 |
+
margin-bottom: 1rem;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
.editor-header h4 {
|
| 411 |
+
color: #2c3e50;
|
| 412 |
+
margin: 0;
|
| 413 |
+
font-size: 1.1rem;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.editor-header h4 i {
|
| 417 |
+
margin-right: 0.5rem;
|
| 418 |
+
color: #3498db;
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
.editor-actions {
|
| 422 |
+
display: flex;
|
| 423 |
+
gap: 0.5rem;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.cv-editor {
|
| 427 |
+
width: 100%;
|
| 428 |
+
min-height: 300px;
|
| 429 |
+
padding: 1rem;
|
| 430 |
+
border: 2px solid #dee2e6;
|
| 431 |
+
border-radius: 6px;
|
| 432 |
+
font-family: 'Courier New', monospace;
|
| 433 |
+
font-size: 0.9rem;
|
| 434 |
+
line-height: 1.6;
|
| 435 |
+
resize: vertical;
|
| 436 |
+
transition: border-color 0.3s ease;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.cv-editor:focus {
|
| 440 |
+
outline: none;
|
| 441 |
+
border-color: #3498db;
|
| 442 |
+
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
| 443 |
+
}
|
| 444 |
+
|
| 445 |
+
.editor-footer {
|
| 446 |
+
display: flex;
|
| 447 |
+
justify-content: space-between;
|
| 448 |
+
margin-top: 0.5rem;
|
| 449 |
+
font-size: 0.85rem;
|
| 450 |
+
color: #7f8c8d;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
/* Saved Configurations */
|
| 454 |
+
.saved-configs {
|
| 455 |
+
margin-top: 2rem;
|
| 456 |
+
padding-top: 1.5rem;
|
| 457 |
+
border-top: 2px solid #e1e8ed;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.saved-configs h4 {
|
| 461 |
+
color: #2c3e50;
|
| 462 |
+
margin-bottom: 1rem;
|
| 463 |
+
font-size: 1.1rem;
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
.saved-configs h4 i {
|
| 467 |
+
margin-right: 0.5rem;
|
| 468 |
+
color: #3498db;
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
.config-item {
|
| 472 |
+
background: #f8f9fa;
|
| 473 |
+
border: 1px solid #dee2e6;
|
| 474 |
+
border-radius: 6px;
|
| 475 |
+
padding: 1rem;
|
| 476 |
+
margin-bottom: 0.75rem;
|
| 477 |
+
display: flex;
|
| 478 |
+
justify-content: space-between;
|
| 479 |
+
align-items: center;
|
| 480 |
+
transition: all 0.3s ease;
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
.config-item:hover {
|
| 484 |
+
background: #e3f2fd;
|
| 485 |
+
border-color: #3498db;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
.config-item-name {
|
| 489 |
+
font-weight: 600;
|
| 490 |
+
color: #2c3e50;
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
.config-item-actions {
|
| 494 |
+
display: flex;
|
| 495 |
+
gap: 0.5rem;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.config-item-actions button {
|
| 499 |
+
padding: 0.25rem 0.75rem;
|
| 500 |
+
font-size: 0.85rem;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
/* Section Description */
|
| 504 |
+
.section-description {
|
| 505 |
+
color: #7f8c8d;
|
| 506 |
+
margin-bottom: 1.5rem;
|
| 507 |
+
font-style: italic;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
/* Glossary and Options Lists */
|
| 511 |
+
.glossary-content,
|
| 512 |
+
.options-list,
|
| 513 |
+
.components-list {
|
| 514 |
+
display: flex;
|
| 515 |
+
flex-direction: column;
|
| 516 |
+
gap: 1rem;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.glossary-item {
|
| 520 |
+
background: white;
|
| 521 |
+
padding: 1rem;
|
| 522 |
+
border-radius: 6px;
|
| 523 |
+
border-left: 3px solid #9b59b6;
|
| 524 |
+
margin-bottom: 0.5rem;
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
.glossary-item strong {
|
| 528 |
+
color: #2c3e50;
|
| 529 |
+
font-size: 1rem;
|
| 530 |
+
display: inline-block;
|
| 531 |
+
margin-right: 0.5rem;
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
.glossary-item p {
|
| 535 |
+
margin: 0.5rem 0 0 0;
|
| 536 |
+
color: #555;
|
| 537 |
+
line-height: 1.6;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
.glossary-content > p {
|
| 541 |
+
font-weight: 600;
|
| 542 |
+
color: #2c3e50;
|
| 543 |
+
margin-bottom: 1rem;
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
/* Options Keywords - Horizontal Layout */
|
| 547 |
+
.options-keywords {
|
| 548 |
+
display: flex;
|
| 549 |
+
flex-wrap: wrap;
|
| 550 |
+
gap: 0.75rem;
|
| 551 |
+
align-items: center;
|
| 552 |
+
padding: 1rem;
|
| 553 |
+
background: #f8f9fa;
|
| 554 |
+
border-radius: 6px;
|
| 555 |
+
border: 1px solid #dee2e6;
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
.options-keywords code {
|
| 559 |
+
padding: 0.5rem 0.75rem;
|
| 560 |
+
border-radius: 4px;
|
| 561 |
+
font-family: 'Courier New', monospace;
|
| 562 |
+
font-size: 0.9rem;
|
| 563 |
+
font-weight: 600;
|
| 564 |
+
white-space: nowrap;
|
| 565 |
+
display: inline-block;
|
| 566 |
+
transition: all 0.2s ease;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
.keyword-required {
|
| 570 |
+
background: #fee;
|
| 571 |
+
color: #c33;
|
| 572 |
+
border: 1px solid #fcc;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
.keyword-required:hover {
|
| 576 |
+
background: #fdd;
|
| 577 |
+
border-color: #c33;
|
| 578 |
+
transform: translateY(-1px);
|
| 579 |
+
box-shadow: 0 2px 4px rgba(204, 51, 51, 0.2);
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
.keyword-optional {
|
| 583 |
+
background: #f5f5f5;
|
| 584 |
+
color: #666;
|
| 585 |
+
border: 1px solid #ddd;
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
.keyword-optional:hover {
|
| 589 |
+
background: #eee;
|
| 590 |
+
border-color: #999;
|
| 591 |
+
transform: translateY(-1px);
|
| 592 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
/* Components Lists */
|
| 596 |
+
.components-list {
|
| 597 |
+
display: flex;
|
| 598 |
+
flex-direction: column;
|
| 599 |
+
gap: 1rem;
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
.component-item {
|
| 603 |
+
background: white;
|
| 604 |
+
padding: 1rem;
|
| 605 |
+
border-radius: 6px;
|
| 606 |
+
border-left: 3px solid #3498db;
|
| 607 |
+
margin-bottom: 0.5rem;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
.component-item strong {
|
| 611 |
+
color: #2c3e50;
|
| 612 |
+
font-size: 1rem;
|
| 613 |
+
display: inline-block;
|
| 614 |
+
margin-right: 0.5rem;
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
.component-item p {
|
| 618 |
+
margin: 0.5rem 0 0 0;
|
| 619 |
+
color: #555;
|
| 620 |
+
line-height: 1.6;
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
/* Notes List */
|
| 624 |
+
.notes-list {
|
| 625 |
+
list-style: none;
|
| 626 |
+
padding: 0;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
.notes-list li {
|
| 630 |
+
padding: 0.75rem;
|
| 631 |
+
margin-bottom: 0.5rem;
|
| 632 |
+
background: #fff3cd;
|
| 633 |
+
border-left: 3px solid #ffc107;
|
| 634 |
+
border-radius: 4px;
|
| 635 |
+
color: #856404;
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
.notes-list li::before {
|
| 639 |
+
content: "ℹ️";
|
| 640 |
+
margin-right: 0.5rem;
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
/* Math Formulas */
|
| 644 |
+
.math-formula {
|
| 645 |
+
background: #f8f9fa;
|
| 646 |
+
padding: 0.75rem;
|
| 647 |
+
border-radius: 4px;
|
| 648 |
+
margin: 0.75rem 0;
|
| 649 |
+
font-family: 'Times New Roman', 'DejaVu Serif', serif;
|
| 650 |
+
font-size: 1rem;
|
| 651 |
+
border-left: 3px solid #3498db;
|
| 652 |
+
white-space: pre-wrap;
|
| 653 |
+
line-height: 1.8;
|
| 654 |
+
color: #2c3e50;
|
| 655 |
+
font-style: italic;
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
/* Mathematical formatting */
|
| 659 |
+
.math-fraction {
|
| 660 |
+
display: inline-flex;
|
| 661 |
+
flex-direction: column;
|
| 662 |
+
vertical-align: middle;
|
| 663 |
+
text-align: center;
|
| 664 |
+
margin: 0 0.3em;
|
| 665 |
+
line-height: 1;
|
| 666 |
+
font-size: 1.1em;
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
.math-numerator {
|
| 670 |
+
border-bottom: 1.5px solid currentColor;
|
| 671 |
+
padding: 0 0.3em 0.1em 0.3em;
|
| 672 |
+
line-height: 1.1;
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
.math-denominator {
|
| 676 |
+
padding: 0.1em 0.3em 0 0.3em;
|
| 677 |
+
line-height: 1.1;
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
.math-sqrt {
|
| 681 |
+
display: inline-block;
|
| 682 |
+
position: relative;
|
| 683 |
+
vertical-align: middle;
|
| 684 |
+
margin: 0 0.2em;
|
| 685 |
+
font-size: 1.1em;
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.math-sqrt-symbol {
|
| 689 |
+
font-size: 1.3em;
|
| 690 |
+
vertical-align: middle;
|
| 691 |
+
margin-right: 0.1em;
|
| 692 |
+
line-height: 1;
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
.math-radicand {
|
| 696 |
+
display: inline-block;
|
| 697 |
+
padding: 0.1em 0.3em;
|
| 698 |
+
margin-left: 0.1em;
|
| 699 |
+
border-top: 1.5px solid currentColor;
|
| 700 |
+
vertical-align: middle;
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
.math-formula sup,
|
| 704 |
+
.math-formula sub,
|
| 705 |
+
.description-paragraph sup,
|
| 706 |
+
.description-paragraph sub,
|
| 707 |
+
.description-list li sup,
|
| 708 |
+
.description-list li sub {
|
| 709 |
+
font-size: 0.75em;
|
| 710 |
+
line-height: 0;
|
| 711 |
+
position: relative;
|
| 712 |
+
vertical-align: baseline;
|
| 713 |
+
font-weight: normal;
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
.math-formula sup,
|
| 717 |
+
.description-paragraph sup,
|
| 718 |
+
.description-list li sup {
|
| 719 |
+
top: -0.5em;
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
.math-formula sub,
|
| 723 |
+
.description-paragraph sub,
|
| 724 |
+
.description-list li sub {
|
| 725 |
+
bottom: -0.25em;
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
/* Ensure proper rendering of subscripts and superscripts in all contexts */
|
| 729 |
+
.doc-content sup,
|
| 730 |
+
.doc-content sub {
|
| 731 |
+
font-size: 0.75em;
|
| 732 |
+
line-height: 0;
|
| 733 |
+
position: relative;
|
| 734 |
+
vertical-align: baseline;
|
| 735 |
+
}
|
| 736 |
+
|
| 737 |
+
.doc-content sup {
|
| 738 |
+
top: -0.5em;
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
.doc-content sub {
|
| 742 |
+
bottom: -0.25em;
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
.doc-content sub {
|
| 746 |
+
font-size: 0.8em;
|
| 747 |
+
vertical-align: sub;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
.doc-content code {
|
| 751 |
+
background: #f1f3f5;
|
| 752 |
+
padding: 0.2rem 0.4rem;
|
| 753 |
+
border-radius: 3px;
|
| 754 |
+
font-family: 'Courier New', monospace;
|
| 755 |
+
font-size: 0.9em;
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
/* Related CVs */
|
| 759 |
+
.related-cvs {
|
| 760 |
+
display: flex;
|
| 761 |
+
flex-wrap: wrap;
|
| 762 |
+
gap: 0.5rem;
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
.related-cv-badge {
|
| 766 |
+
display: inline-block;
|
| 767 |
+
padding: 0.5rem 1rem;
|
| 768 |
+
background: #e3f2fd;
|
| 769 |
+
color: #1976d2;
|
| 770 |
+
border-radius: 20px;
|
| 771 |
+
cursor: pointer;
|
| 772 |
+
transition: all 0.3s ease;
|
| 773 |
+
font-weight: 500;
|
| 774 |
+
border: 1px solid #90caf9;
|
| 775 |
+
}
|
| 776 |
+
|
| 777 |
+
.related-cv-badge:hover {
|
| 778 |
+
background: #1976d2;
|
| 779 |
+
color: white;
|
| 780 |
+
transform: translateY(-2px);
|
| 781 |
+
box-shadow: 0 2px 8px rgba(25, 118, 210, 0.3);
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
/* Responsive Design */
|
| 785 |
+
@media (max-width: 1024px) {
|
| 786 |
+
.plumed-container {
|
| 787 |
+
flex-direction: column;
|
| 788 |
+
}
|
| 789 |
+
|
| 790 |
+
.plumed-sidebar {
|
| 791 |
+
width: 100%;
|
| 792 |
+
max-height: 300px;
|
| 793 |
+
}
|
| 794 |
+
}
|
| 795 |
+
|
| 796 |
+
/* Scrollbar Styling */
|
| 797 |
+
.cv-list::-webkit-scrollbar,
|
| 798 |
+
.plumed-content::-webkit-scrollbar {
|
| 799 |
+
width: 8px;
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
.cv-list::-webkit-scrollbar-track,
|
| 803 |
+
.plumed-content::-webkit-scrollbar-track {
|
| 804 |
+
background: #f1f1f1;
|
| 805 |
+
border-radius: 4px;
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
.cv-list::-webkit-scrollbar-thumb,
|
| 809 |
+
.plumed-content::-webkit-scrollbar-thumb {
|
| 810 |
+
background: #bdc3c7;
|
| 811 |
+
border-radius: 4px;
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
.cv-list::-webkit-scrollbar-thumb:hover,
|
| 815 |
+
.plumed-content::-webkit-scrollbar-thumb:hover {
|
| 816 |
+
background: #95a5a6;
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
/* PLUMED Section Cards */
|
| 820 |
+
.plumed-section-card {
|
| 821 |
+
margin-bottom: 1.5rem;
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
.plumed-section-card:last-child {
|
| 825 |
+
margin-bottom: 0;
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
/* PLUMED Section Headers - Smaller font size */
|
| 829 |
+
.plumed-section-card h2 {
|
| 830 |
+
font-size: 1.4rem !important;
|
| 831 |
+
}
|
| 832 |
+
|
| 833 |
+
/* Collapsed state - hide content but keep header visible */
|
| 834 |
+
.plumed-section-card.collapsed .section-description,
|
| 835 |
+
.plumed-section-card.collapsed .plumed-container,
|
| 836 |
+
.plumed-section-card.collapsed .plumed-generate-section,
|
| 837 |
+
.plumed-section-card.collapsed .generate-simulation-files-section {
|
| 838 |
+
max-height: 0;
|
| 839 |
+
opacity: 0;
|
| 840 |
+
margin-top: 0;
|
| 841 |
+
margin-bottom: 0;
|
| 842 |
+
padding-top: 0;
|
| 843 |
+
padding-bottom: 0;
|
| 844 |
+
overflow: hidden;
|
| 845 |
+
transition: max-height 0.4s ease, opacity 0.3s ease, margin 0.3s ease, padding 0.3s ease;
|
| 846 |
+
}
|
| 847 |
+
|
| 848 |
+
/* Keep header visible even when collapsed */
|
| 849 |
+
.plumed-section-card.collapsed .plumed-toggle-header {
|
| 850 |
+
margin-bottom: 0;
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
/* Collapsible Header */
|
| 854 |
+
.plumed-toggle-header {
|
| 855 |
+
cursor: pointer;
|
| 856 |
+
user-select: none;
|
| 857 |
+
display: flex;
|
| 858 |
+
align-items: center;
|
| 859 |
+
justify-content: space-between;
|
| 860 |
+
transition: color 0.3s ease;
|
| 861 |
+
padding: 0.5rem;
|
| 862 |
+
margin: -0.5rem;
|
| 863 |
+
border-radius: 4px;
|
| 864 |
+
text-align: left !important;
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
.plumed-toggle-header > i:first-of-type {
|
| 868 |
+
margin-right: 0.5rem;
|
| 869 |
+
}
|
| 870 |
+
|
| 871 |
+
/* Ensure left alignment for custom PLUMED header */
|
| 872 |
+
#custom-plumed-card h2,
|
| 873 |
+
#custom-plumed-card .plumed-toggle-header {
|
| 874 |
+
text-align: left !important;
|
| 875 |
+
justify-content: flex-start !important;
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
#custom-plumed-card .plumed-toggle-header {
|
| 879 |
+
display: flex !important;
|
| 880 |
+
align-items: center;
|
| 881 |
+
}
|
| 882 |
+
|
| 883 |
+
#custom-plumed-card .plumed-toggle-header > *:first-child {
|
| 884 |
+
margin-right: 0.5rem;
|
| 885 |
+
}
|
| 886 |
+
|
| 887 |
+
#custom-plumed-card .plumed-toggle-header .toggle-icon {
|
| 888 |
+
margin-left: auto;
|
| 889 |
+
margin-right: 0;
|
| 890 |
+
}
|
| 891 |
+
|
| 892 |
+
/* Generate Simulation Files Section - Left Aligned */
|
| 893 |
+
#generate-simulation-files-card h2,
|
| 894 |
+
#generate-simulation-files-card .plumed-toggle-header {
|
| 895 |
+
text-align: left !important;
|
| 896 |
+
justify-content: flex-start !important;
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
#generate-simulation-files-card .plumed-toggle-header {
|
| 900 |
+
display: flex !important;
|
| 901 |
+
align-items: center;
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
#generate-simulation-files-card .plumed-toggle-header > *:first-child {
|
| 905 |
+
margin-right: 0.5rem;
|
| 906 |
+
}
|
| 907 |
+
|
| 908 |
+
#generate-simulation-files-card .plumed-toggle-header .toggle-icon {
|
| 909 |
+
margin-left: auto;
|
| 910 |
+
margin-right: 0;
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
.plumed-toggle-header:hover {
|
| 914 |
+
background: rgba(52, 152, 219, 0.1);
|
| 915 |
+
color: #3498db;
|
| 916 |
+
}
|
| 917 |
+
|
| 918 |
+
.toggle-icon {
|
| 919 |
+
transition: transform 0.3s ease;
|
| 920 |
+
font-size: 0.9em;
|
| 921 |
+
color: #7f8c8d;
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
.plumed-toggle-header.collapsed .toggle-icon {
|
| 925 |
+
transform: rotate(-90deg);
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
.plumed-container,
|
| 929 |
+
.plumed-generate-section {
|
| 930 |
+
transition: max-height 0.4s ease, opacity 0.3s ease, margin 0.3s ease;
|
| 931 |
+
overflow: hidden;
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
.section-description {
|
| 935 |
+
transition: max-height 0.4s ease, opacity 0.3s ease, margin 0.3s ease, padding 0.3s ease;
|
| 936 |
+
overflow: hidden;
|
| 937 |
+
}
|
| 938 |
+
|
| 939 |
+
/* Generate Files Section */
|
| 940 |
+
.plumed-generate-section {
|
| 941 |
+
margin-top: 1.5rem;
|
| 942 |
+
padding-top: 1.5rem;
|
| 943 |
+
}
|
| 944 |
+
|
| 945 |
+
.generate-content {
|
| 946 |
+
display: flex;
|
| 947 |
+
flex-direction: column;
|
| 948 |
+
gap: 1.5rem;
|
| 949 |
+
}
|
| 950 |
+
|
| 951 |
+
.generate-options {
|
| 952 |
+
display: flex;
|
| 953 |
+
gap: 1rem;
|
| 954 |
+
flex-wrap: wrap;
|
| 955 |
+
}
|
| 956 |
+
|
| 957 |
+
.generate-options .btn {
|
| 958 |
+
padding: 0.75rem 1.5rem;
|
| 959 |
+
font-size: 1rem;
|
| 960 |
+
display: flex;
|
| 961 |
+
align-items: center;
|
| 962 |
+
gap: 0.5rem;
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
.generated-file-preview {
|
| 966 |
+
background: #f8f9fa;
|
| 967 |
+
border: 1px solid #dee2e6;
|
| 968 |
+
border-radius: 8px;
|
| 969 |
+
padding: 1.5rem;
|
| 970 |
+
margin-top: 1rem;
|
| 971 |
+
}
|
| 972 |
+
|
| 973 |
+
.generated-file-preview h4 {
|
| 974 |
+
margin: 0 0 1rem 0;
|
| 975 |
+
color: #2c3e50;
|
| 976 |
+
font-size: 1.1rem;
|
| 977 |
+
}
|
| 978 |
+
|
| 979 |
+
.file-preview {
|
| 980 |
+
background: #2c3e50;
|
| 981 |
+
color: #ecf0f1;
|
| 982 |
+
padding: 1rem;
|
| 983 |
+
border-radius: 4px;
|
| 984 |
+
overflow-x: auto;
|
| 985 |
+
font-family: 'Courier New', monospace;
|
| 986 |
+
font-size: 0.9rem;
|
| 987 |
+
line-height: 1.6;
|
| 988 |
+
margin: 0;
|
| 989 |
+
max-height: 400px;
|
| 990 |
+
overflow-y: auto;
|
| 991 |
+
}
|
| 992 |
+
|
| 993 |
+
/* Custom PLUMED File Section */
|
| 994 |
+
.custom-plumed-section {
|
| 995 |
+
margin-top: 1.5rem;
|
| 996 |
+
transition: max-height 0.4s ease, opacity 0.3s ease, margin 0.3s ease, padding 0.3s ease;
|
| 997 |
+
overflow: hidden;
|
| 998 |
+
}
|
| 999 |
+
|
| 1000 |
+
/* Collapsed state for custom PLUMED section */
|
| 1001 |
+
#custom-plumed-card.collapsed .section-description,
|
| 1002 |
+
#custom-plumed-card.collapsed .custom-plumed-section {
|
| 1003 |
+
max-height: 0;
|
| 1004 |
+
opacity: 0;
|
| 1005 |
+
margin-top: 0;
|
| 1006 |
+
margin-bottom: 0;
|
| 1007 |
+
padding-top: 0;
|
| 1008 |
+
padding-bottom: 0;
|
| 1009 |
+
overflow: hidden;
|
| 1010 |
+
}
|
| 1011 |
+
|
| 1012 |
+
/* Keep header visible even when collapsed */
|
| 1013 |
+
#custom-plumed-card.collapsed .plumed-toggle-header {
|
| 1014 |
+
margin-bottom: 0;
|
| 1015 |
+
}
|
| 1016 |
+
|
| 1017 |
+
.custom-editor-header {
|
| 1018 |
+
display: flex;
|
| 1019 |
+
justify-content: space-between;
|
| 1020 |
+
align-items: center;
|
| 1021 |
+
margin-bottom: 1rem;
|
| 1022 |
+
padding-bottom: 0.75rem;
|
| 1023 |
+
border-bottom: 2px solid #dee2e6;
|
| 1024 |
+
}
|
| 1025 |
+
|
| 1026 |
+
.custom-editor-header h4 {
|
| 1027 |
+
margin: 0;
|
| 1028 |
+
color: #2c3e50;
|
| 1029 |
+
font-size: 1.1rem;
|
| 1030 |
+
display: flex;
|
| 1031 |
+
align-items: center;
|
| 1032 |
+
gap: 0.5rem;
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
.custom-editor-header h4 i {
|
| 1036 |
+
color: #3498db;
|
| 1037 |
+
}
|
| 1038 |
+
|
| 1039 |
+
.custom-plumed-editor {
|
| 1040 |
+
width: 100%;
|
| 1041 |
+
min-height: 400px;
|
| 1042 |
+
padding: 1rem;
|
| 1043 |
+
border: 2px solid #dee2e6;
|
| 1044 |
+
border-radius: 8px;
|
| 1045 |
+
font-family: 'Courier New', monospace;
|
| 1046 |
+
font-size: 0.95rem;
|
| 1047 |
+
line-height: 1.6;
|
| 1048 |
+
resize: vertical;
|
| 1049 |
+
background: #ffffff;
|
| 1050 |
+
color: #2c3e50;
|
| 1051 |
+
transition: border-color 0.3s ease;
|
| 1052 |
+
}
|
| 1053 |
+
|
| 1054 |
+
.custom-plumed-editor:focus {
|
| 1055 |
+
outline: none;
|
| 1056 |
+
border-color: #3498db;
|
| 1057 |
+
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
| 1058 |
+
}
|
| 1059 |
+
|
| 1060 |
+
.custom-plumed-editor::placeholder {
|
| 1061 |
+
color: #95a5a6;
|
| 1062 |
+
font-style: italic;
|
| 1063 |
+
}
|
| 1064 |
+
|
ambermdflow/css/styles.css
ADDED
|
@@ -0,0 +1,2029 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Reset and Base Styles */
|
| 2 |
+
* {
|
| 3 |
+
margin: 0;
|
| 4 |
+
padding: 0;
|
| 5 |
+
box-sizing: border-box;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
body {
|
| 9 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 10 |
+
line-height: 1.6;
|
| 11 |
+
color: #333;
|
| 12 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 13 |
+
min-height: 100vh;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.container {
|
| 17 |
+
max-width: 1400px;
|
| 18 |
+
margin: 0 auto;
|
| 19 |
+
background: white;
|
| 20 |
+
min-height: 100vh;
|
| 21 |
+
box-shadow: 0 0 30px rgba(0, 0, 0, 0.1);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
/* Header Styles */
|
| 25 |
+
.header {
|
| 26 |
+
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
| 27 |
+
color: white;
|
| 28 |
+
padding: 2rem 0;
|
| 29 |
+
text-align: center;
|
| 30 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
.header-content h1 {
|
| 34 |
+
font-size: 2.5rem;
|
| 35 |
+
margin-bottom: 0.5rem;
|
| 36 |
+
font-weight: 300;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.header-content p {
|
| 40 |
+
font-size: 1.1rem;
|
| 41 |
+
opacity: 0.9;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.header i {
|
| 45 |
+
margin-right: 0.5rem;
|
| 46 |
+
color: #3498db;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/* Tab Navigation */
|
| 50 |
+
.tab-navigation {
|
| 51 |
+
display: flex;
|
| 52 |
+
background: #34495e;
|
| 53 |
+
border-bottom: 3px solid #3498db;
|
| 54 |
+
overflow-x: auto;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/* Step Navigation Controls */
|
| 58 |
+
.step-navigation {
|
| 59 |
+
display: flex;
|
| 60 |
+
justify-content: space-between;
|
| 61 |
+
align-items: center;
|
| 62 |
+
background: #ffffff;
|
| 63 |
+
padding: 20px 30px;
|
| 64 |
+
border-top: 1px solid #dee2e6;
|
| 65 |
+
box-shadow: 0 -2px 8px rgba(0,0,0,0.1);
|
| 66 |
+
position: sticky;
|
| 67 |
+
bottom: 0;
|
| 68 |
+
z-index: 100;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* Checkbox with Button Layout */
|
| 72 |
+
.checkbox-with-button {
|
| 73 |
+
display: flex;
|
| 74 |
+
align-items: center;
|
| 75 |
+
gap: 15px;
|
| 76 |
+
margin-bottom: 10px;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.checkbox-with-button .checkbox-container {
|
| 80 |
+
margin-bottom: 0;
|
| 81 |
+
flex: 1;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.btn-sm {
|
| 85 |
+
padding: 6px 12px;
|
| 86 |
+
font-size: 12px;
|
| 87 |
+
border-radius: 4px;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
.btn-outline-primary {
|
| 91 |
+
color: #007bff;
|
| 92 |
+
border: 1px solid #007bff;
|
| 93 |
+
background: transparent;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
.btn-outline-primary:hover:not(:disabled) {
|
| 97 |
+
color: white;
|
| 98 |
+
background: #007bff;
|
| 99 |
+
border-color: #007bff;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.btn-outline-primary:disabled {
|
| 103 |
+
color: #6c757d;
|
| 104 |
+
border-color: #6c757d;
|
| 105 |
+
background: transparent;
|
| 106 |
+
cursor: not-allowed;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.nav-btn {
|
| 110 |
+
display: flex;
|
| 111 |
+
align-items: center;
|
| 112 |
+
gap: 8px;
|
| 113 |
+
padding: 10px 20px;
|
| 114 |
+
border: 2px solid #007bff;
|
| 115 |
+
background: #007bff;
|
| 116 |
+
color: white;
|
| 117 |
+
border-radius: 25px;
|
| 118 |
+
font-weight: 600;
|
| 119 |
+
cursor: pointer;
|
| 120 |
+
transition: all 0.3s ease;
|
| 121 |
+
font-size: 14px;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.nav-btn:hover:not(:disabled) {
|
| 125 |
+
background: #0056b3;
|
| 126 |
+
border-color: #0056b3;
|
| 127 |
+
transform: translateY(-2px);
|
| 128 |
+
box-shadow: 0 4px 8px rgba(0,123,255,0.3);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.nav-btn:disabled {
|
| 132 |
+
background: #6c757d;
|
| 133 |
+
border-color: #6c757d;
|
| 134 |
+
cursor: not-allowed;
|
| 135 |
+
opacity: 0.6;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.step-indicator {
|
| 139 |
+
display: flex;
|
| 140 |
+
align-items: center;
|
| 141 |
+
gap: 5px;
|
| 142 |
+
font-weight: 600;
|
| 143 |
+
color: #495057;
|
| 144 |
+
font-size: 16px;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.step-indicator span {
|
| 148 |
+
background: #e9ecef;
|
| 149 |
+
padding: 8px 12px;
|
| 150 |
+
border-radius: 20px;
|
| 151 |
+
min-width: 30px;
|
| 152 |
+
text-align: center;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.step-indicator #current-step {
|
| 156 |
+
background: #007bff;
|
| 157 |
+
color: white;
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
.tab-button {
|
| 161 |
+
flex: 1;
|
| 162 |
+
background: none;
|
| 163 |
+
border: none;
|
| 164 |
+
color: white;
|
| 165 |
+
padding: 1rem 1.5rem;
|
| 166 |
+
cursor: pointer;
|
| 167 |
+
font-size: 1rem;
|
| 168 |
+
font-weight: 500;
|
| 169 |
+
transition: all 0.3s ease;
|
| 170 |
+
border-bottom: 3px solid transparent;
|
| 171 |
+
min-width: 200px;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.tab-button:hover {
|
| 175 |
+
background: rgba(255, 255, 255, 0.1);
|
| 176 |
+
transform: translateY(-2px);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.tab-button.active {
|
| 180 |
+
background: #3498db;
|
| 181 |
+
border-bottom-color: #e74c3c;
|
| 182 |
+
transform: translateY(-2px);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.tab-button i {
|
| 186 |
+
margin-right: 0.5rem;
|
| 187 |
+
font-size: 1.1rem;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
/* Main Content */
|
| 191 |
+
.main-content {
|
| 192 |
+
padding: 2rem;
|
| 193 |
+
min-height: 600px;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
.tab-content {
|
| 197 |
+
display: none;
|
| 198 |
+
animation: fadeIn 0.5s ease-in-out;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.tab-content.active {
|
| 202 |
+
display: block;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
@keyframes fadeIn {
|
| 206 |
+
from { opacity: 0; transform: translateY(20px); }
|
| 207 |
+
to { opacity: 1; transform: translateY(0); }
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
/* Card Styles */
|
| 211 |
+
.card {
|
| 212 |
+
background: white;
|
| 213 |
+
border-radius: 12px;
|
| 214 |
+
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
|
| 215 |
+
padding: 2rem;
|
| 216 |
+
margin-bottom: 2rem;
|
| 217 |
+
border: 1px solid #e1e8ed;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.card h2 {
|
| 221 |
+
color: #2c3e50;
|
| 222 |
+
margin-bottom: 1.5rem;
|
| 223 |
+
font-size: 1.8rem;
|
| 224 |
+
font-weight: 600;
|
| 225 |
+
border-bottom: 2px solid #3498db;
|
| 226 |
+
padding-bottom: 0.5rem;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.card h2 i {
|
| 230 |
+
margin-right: 0.5rem;
|
| 231 |
+
color: #3498db;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
/* File Generation Note */
|
| 235 |
+
.file-generation-note {
|
| 236 |
+
background: #fff3cd;
|
| 237 |
+
border: 2px solid #ffc107;
|
| 238 |
+
border-left: 5px solid #ffc107;
|
| 239 |
+
border-radius: 8px;
|
| 240 |
+
padding: 1rem 1.5rem;
|
| 241 |
+
margin-bottom: 1.5rem;
|
| 242 |
+
display: flex;
|
| 243 |
+
align-items: flex-start;
|
| 244 |
+
gap: 1rem;
|
| 245 |
+
box-shadow: 0 2px 4px rgba(255, 193, 7, 0.1);
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.file-generation-note i {
|
| 249 |
+
color: #856404;
|
| 250 |
+
font-size: 1.5rem;
|
| 251 |
+
margin-top: 0.2rem;
|
| 252 |
+
flex-shrink: 0;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.file-generation-note .note-content {
|
| 256 |
+
flex: 1;
|
| 257 |
+
color: #856404;
|
| 258 |
+
line-height: 1.6;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.file-generation-note .note-content p {
|
| 262 |
+
margin: 0;
|
| 263 |
+
font-size: 0.95rem;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.file-generation-note .note-content strong {
|
| 267 |
+
color: #664d03;
|
| 268 |
+
font-weight: 600;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
/* Protein Loading Styles */
|
| 272 |
+
.input-methods {
|
| 273 |
+
display: grid;
|
| 274 |
+
grid-template-columns: 1fr auto 1fr;
|
| 275 |
+
gap: 2rem;
|
| 276 |
+
align-items: center;
|
| 277 |
+
margin-bottom: 2rem;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.method-option {
|
| 281 |
+
background: #f8f9fa;
|
| 282 |
+
padding: 1.5rem;
|
| 283 |
+
border-radius: 8px;
|
| 284 |
+
border: 2px dashed #dee2e6;
|
| 285 |
+
transition: all 0.3s ease;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.method-option:hover {
|
| 289 |
+
border-color: #3498db;
|
| 290 |
+
background: #f0f8ff;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.method-option h3 {
|
| 294 |
+
color: #2c3e50;
|
| 295 |
+
margin-bottom: 1rem;
|
| 296 |
+
font-size: 1.2rem;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.method-option h3 i {
|
| 300 |
+
margin-right: 0.5rem;
|
| 301 |
+
color: #3498db;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.divider {
|
| 305 |
+
text-align: center;
|
| 306 |
+
font-weight: bold;
|
| 307 |
+
color: #7f8c8d;
|
| 308 |
+
font-size: 1.1rem;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.divider::before,
|
| 312 |
+
.divider::after {
|
| 313 |
+
content: '';
|
| 314 |
+
display: inline-block;
|
| 315 |
+
width: 50px;
|
| 316 |
+
height: 2px;
|
| 317 |
+
background: #bdc3c7;
|
| 318 |
+
vertical-align: middle;
|
| 319 |
+
margin: 0 1rem;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
/* File Upload Area */
|
| 323 |
+
.file-upload-area {
|
| 324 |
+
border: 2px dashed #3498db;
|
| 325 |
+
border-radius: 8px;
|
| 326 |
+
padding: 2rem;
|
| 327 |
+
text-align: center;
|
| 328 |
+
cursor: pointer;
|
| 329 |
+
transition: all 0.3s ease;
|
| 330 |
+
background: #f8f9fa;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.file-upload-area:hover {
|
| 334 |
+
background: #e3f2fd;
|
| 335 |
+
border-color: #2980b9;
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
.file-upload-area i {
|
| 339 |
+
font-size: 3rem;
|
| 340 |
+
color: #3498db;
|
| 341 |
+
margin-bottom: 1rem;
|
| 342 |
+
display: block;
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.file-upload-area p {
|
| 346 |
+
margin-bottom: 1rem;
|
| 347 |
+
color: #7f8c8d;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.file-info {
|
| 351 |
+
background: #d4edda;
|
| 352 |
+
border: 1px solid #c3e6cb;
|
| 353 |
+
border-radius: 4px;
|
| 354 |
+
padding: 1rem;
|
| 355 |
+
margin-top: 1rem;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
.file-info p {
|
| 359 |
+
margin: 0.25rem 0;
|
| 360 |
+
color: #155724;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
/* PDB Fetch */
|
| 364 |
+
.pdb-fetch {
|
| 365 |
+
margin-top: 1rem;
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
.input-group {
|
| 369 |
+
display: flex;
|
| 370 |
+
gap: 1rem;
|
| 371 |
+
align-items: end;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.input-group label {
|
| 375 |
+
font-weight: 600;
|
| 376 |
+
color: #2c3e50;
|
| 377 |
+
margin-bottom: 0.5rem;
|
| 378 |
+
display: block;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
.input-group input {
|
| 382 |
+
flex: 1;
|
| 383 |
+
padding: 0.75rem;
|
| 384 |
+
border: 2px solid #dee2e6;
|
| 385 |
+
border-radius: 4px;
|
| 386 |
+
font-size: 1rem;
|
| 387 |
+
transition: border-color 0.3s ease;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.input-group input:focus {
|
| 391 |
+
outline: none;
|
| 392 |
+
border-color: #3498db;
|
| 393 |
+
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
/* Status Messages */
|
| 397 |
+
.status-message {
|
| 398 |
+
margin-top: 1rem;
|
| 399 |
+
padding: 0.75rem;
|
| 400 |
+
border-radius: 4px;
|
| 401 |
+
font-weight: 500;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
.status-message.success {
|
| 405 |
+
background: #d4edda;
|
| 406 |
+
color: #155724;
|
| 407 |
+
border: 1px solid #c3e6cb;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
.status-message.error {
|
| 411 |
+
background: #f8d7da;
|
| 412 |
+
color: #721c24;
|
| 413 |
+
border: 1px solid #f5c6cb;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.status-message.info {
|
| 417 |
+
background: #d1ecf1;
|
| 418 |
+
color: #0c5460;
|
| 419 |
+
border: 1px solid #bee5eb;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
/* Protein Preview */
|
| 423 |
+
.protein-preview {
|
| 424 |
+
margin-top: 2rem;
|
| 425 |
+
background: #f8f9fa;
|
| 426 |
+
border-radius: 8px;
|
| 427 |
+
padding: 1.5rem;
|
| 428 |
+
border: 1px solid #dee2e6;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
.preview-content {
|
| 432 |
+
display: grid;
|
| 433 |
+
grid-template-columns: 1fr 1fr;
|
| 434 |
+
gap: 2rem;
|
| 435 |
+
margin-top: 1rem;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
/* Structure comparison container - force side by side and centered */
|
| 439 |
+
.structure-comparison-container {
|
| 440 |
+
display: flex !important;
|
| 441 |
+
flex-direction: row !important;
|
| 442 |
+
width: 100% !important;
|
| 443 |
+
max-width: 1400px !important;
|
| 444 |
+
gap: 20px !important;
|
| 445 |
+
margin: 0 auto !important;
|
| 446 |
+
justify-content: center !important;
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
.structure-comparison-container .comparison-viewer {
|
| 450 |
+
flex: 0 1 48% !important;
|
| 451 |
+
min-width: 450px !important;
|
| 452 |
+
max-width: 48% !important;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
/* Override preview-content for comparison view */
|
| 456 |
+
#completed-structure-preview .preview-content {
|
| 457 |
+
display: flex !important;
|
| 458 |
+
justify-content: center !important;
|
| 459 |
+
width: 100% !important;
|
| 460 |
+
padding: 0 !important;
|
| 461 |
+
grid-template-columns: none !important;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
.protein-info p {
|
| 465 |
+
margin: 0.5rem 0;
|
| 466 |
+
font-size: 1rem;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
.protein-visualization {
|
| 470 |
+
background: white;
|
| 471 |
+
border-radius: 4px;
|
| 472 |
+
padding: 1rem;
|
| 473 |
+
border: 1px solid #dee2e6;
|
| 474 |
+
min-height: 300px;
|
| 475 |
+
position: relative;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
/* Ensure NGL viewer controls overlay within both original and prepared viewers */
|
| 479 |
+
.molecule-viewer {
|
| 480 |
+
position: relative;
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
#ngl-viewer {
|
| 484 |
+
border-radius: 4px;
|
| 485 |
+
background: #f8f9fa;
|
| 486 |
+
border: 1px solid #dee2e6;
|
| 487 |
+
}
|
| 488 |
+
|
| 489 |
+
.viewer-controls {
|
| 490 |
+
position: absolute;
|
| 491 |
+
top: 10px;
|
| 492 |
+
right: 10px;
|
| 493 |
+
display: flex;
|
| 494 |
+
gap: 0.5rem;
|
| 495 |
+
z-index: 10;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
.viewer-controls .btn {
|
| 499 |
+
padding: 0.25rem 0.5rem;
|
| 500 |
+
font-size: 0.8rem;
|
| 501 |
+
border-radius: 3px;
|
| 502 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
.viewer-controls .btn:hover {
|
| 506 |
+
transform: translateY(-1px);
|
| 507 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
/* Superimposed structure preview: controls inside the viewer box (parent has position: relative) */
|
| 511 |
+
#superimposed-molecule-viewer .viewer-controls {
|
| 512 |
+
position: absolute;
|
| 513 |
+
top: 10px;
|
| 514 |
+
right: 10px;
|
| 515 |
+
z-index: 10;
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
/* Simulation Parameters */
|
| 519 |
+
.params-grid {
|
| 520 |
+
display: grid;
|
| 521 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 522 |
+
gap: 2rem;
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
.param-section {
|
| 526 |
+
background: #f8f9fa;
|
| 527 |
+
padding: 1.5rem;
|
| 528 |
+
border-radius: 8px;
|
| 529 |
+
border: 1px solid #dee2e6;
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
.param-section h3 {
|
| 533 |
+
color: #2c3e50;
|
| 534 |
+
margin-bottom: 1rem;
|
| 535 |
+
font-size: 1.2rem;
|
| 536 |
+
border-bottom: 1px solid #bdc3c7;
|
| 537 |
+
padding-bottom: 0.5rem;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
.param-section h3 i {
|
| 541 |
+
margin-right: 0.5rem;
|
| 542 |
+
color: #3498db;
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
.form-group {
|
| 546 |
+
margin-bottom: 1rem;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.form-group label {
|
| 550 |
+
display: block;
|
| 551 |
+
margin-bottom: 0.5rem;
|
| 552 |
+
font-weight: 600;
|
| 553 |
+
color: #2c3e50;
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
.form-group input,
|
| 557 |
+
.form-group select {
|
| 558 |
+
width: 100%;
|
| 559 |
+
padding: 0.75rem;
|
| 560 |
+
border: 2px solid #dee2e6;
|
| 561 |
+
border-radius: 4px;
|
| 562 |
+
font-size: 1rem;
|
| 563 |
+
transition: border-color 0.3s ease;
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
.form-group input:focus,
|
| 567 |
+
.form-group select:focus {
|
| 568 |
+
outline: none;
|
| 569 |
+
border-color: #3498db;
|
| 570 |
+
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
/* Simulation Steps */
|
| 574 |
+
.steps-container {
|
| 575 |
+
display: flex;
|
| 576 |
+
flex-direction: column;
|
| 577 |
+
gap: 1.5rem;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
.step-item {
|
| 581 |
+
background: #f8f9fa;
|
| 582 |
+
border-radius: 8px;
|
| 583 |
+
border: 1px solid #dee2e6;
|
| 584 |
+
overflow: hidden;
|
| 585 |
+
transition: all 0.3s ease;
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
.step-item:hover {
|
| 589 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
| 590 |
+
transform: translateY(-2px);
|
| 591 |
+
}
|
| 592 |
+
|
| 593 |
+
.step-header {
|
| 594 |
+
display: flex;
|
| 595 |
+
justify-content: space-between;
|
| 596 |
+
align-items: center;
|
| 597 |
+
padding: 1.5rem;
|
| 598 |
+
background: #34495e;
|
| 599 |
+
color: white;
|
| 600 |
+
cursor: pointer;
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
.step-header h3 {
|
| 604 |
+
margin: 0;
|
| 605 |
+
font-size: 1.2rem;
|
| 606 |
+
font-weight: 600;
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
.step-header h3 i {
|
| 610 |
+
margin-right: 0.5rem;
|
| 611 |
+
color: #3498db;
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.step-content {
|
| 615 |
+
padding: 1.5rem;
|
| 616 |
+
background: white;
|
| 617 |
+
display: none;
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
.step-content.active {
|
| 621 |
+
display: block;
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
.form-row {
|
| 625 |
+
display: grid;
|
| 626 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 627 |
+
gap: 1rem;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
/* Toggle Switch */
|
| 631 |
+
.switch {
|
| 632 |
+
position: relative;
|
| 633 |
+
display: inline-block;
|
| 634 |
+
width: 60px;
|
| 635 |
+
height: 34px;
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
.switch input {
|
| 639 |
+
opacity: 0;
|
| 640 |
+
width: 0;
|
| 641 |
+
height: 0;
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
.slider {
|
| 645 |
+
position: absolute;
|
| 646 |
+
cursor: pointer;
|
| 647 |
+
top: 0;
|
| 648 |
+
left: 0;
|
| 649 |
+
right: 0;
|
| 650 |
+
bottom: 0;
|
| 651 |
+
background-color: #ccc;
|
| 652 |
+
transition: .4s;
|
| 653 |
+
border-radius: 34px;
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
.slider:before {
|
| 657 |
+
position: absolute;
|
| 658 |
+
content: "";
|
| 659 |
+
height: 26px;
|
| 660 |
+
width: 26px;
|
| 661 |
+
left: 4px;
|
| 662 |
+
bottom: 4px;
|
| 663 |
+
background-color: white;
|
| 664 |
+
transition: .4s;
|
| 665 |
+
border-radius: 50%;
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
input:checked + .slider {
|
| 669 |
+
background-color: #3498db;
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
input:checked + .slider:before {
|
| 673 |
+
transform: translateX(26px);
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
/* File Generation */
|
| 677 |
+
.generation-controls {
|
| 678 |
+
display: flex;
|
| 679 |
+
gap: 1rem;
|
| 680 |
+
margin-bottom: 2rem;
|
| 681 |
+
flex-wrap: wrap;
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
.files-preview {
|
| 685 |
+
margin-bottom: 2rem;
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.files-list {
|
| 689 |
+
display: grid;
|
| 690 |
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
| 691 |
+
gap: 1rem;
|
| 692 |
+
margin-top: 1rem;
|
| 693 |
+
}
|
| 694 |
+
|
| 695 |
+
.file-item {
|
| 696 |
+
background: #f8f9fa;
|
| 697 |
+
border: 1px solid #dee2e6;
|
| 698 |
+
border-radius: 8px;
|
| 699 |
+
padding: 1rem;
|
| 700 |
+
transition: all 0.3s ease;
|
| 701 |
+
}
|
| 702 |
+
|
| 703 |
+
.file-item:hover {
|
| 704 |
+
background: #e3f2fd;
|
| 705 |
+
border-color: #3498db;
|
| 706 |
+
transform: translateY(-2px);
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
.file-item h4 {
|
| 710 |
+
color: #2c3e50;
|
| 711 |
+
margin-bottom: 0.5rem;
|
| 712 |
+
font-size: 1.1rem;
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
.file-item p {
|
| 716 |
+
color: #7f8c8d;
|
| 717 |
+
font-size: 0.9rem;
|
| 718 |
+
margin: 0.25rem 0;
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
.download-section {
|
| 722 |
+
margin-bottom: 2rem;
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.download-options {
|
| 726 |
+
display: flex;
|
| 727 |
+
gap: 1rem;
|
| 728 |
+
flex-wrap: wrap;
|
| 729 |
+
margin-top: 1rem;
|
| 730 |
+
}
|
| 731 |
+
|
| 732 |
+
.simulation-summary {
|
| 733 |
+
background: #f8f9fa;
|
| 734 |
+
border-radius: 8px;
|
| 735 |
+
padding: 1.5rem;
|
| 736 |
+
border: 1px solid #dee2e6;
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
.summary-content {
|
| 740 |
+
display: grid;
|
| 741 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 742 |
+
gap: 1rem;
|
| 743 |
+
margin-top: 1rem;
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
.summary-item {
|
| 747 |
+
background: white;
|
| 748 |
+
padding: 1rem;
|
| 749 |
+
border-radius: 4px;
|
| 750 |
+
border: 1px solid #dee2e6;
|
| 751 |
+
}
|
| 752 |
+
|
| 753 |
+
.summary-item h4 {
|
| 754 |
+
color: #2c3e50;
|
| 755 |
+
margin-bottom: 0.5rem;
|
| 756 |
+
font-size: 1rem;
|
| 757 |
+
}
|
| 758 |
+
|
| 759 |
+
.summary-item p {
|
| 760 |
+
color: #7f8c8d;
|
| 761 |
+
margin: 0.25rem 0;
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
/* Checkbox Group Styles */
|
| 765 |
+
.checkbox-group {
|
| 766 |
+
display: flex;
|
| 767 |
+
gap: 1rem;
|
| 768 |
+
margin-top: 0.5rem;
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
.checkbox-container {
|
| 772 |
+
display: flex;
|
| 773 |
+
align-items: center;
|
| 774 |
+
cursor: pointer;
|
| 775 |
+
font-size: 0.9rem;
|
| 776 |
+
color: #495057;
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
.checkbox-container input[type="checkbox"] {
|
| 780 |
+
margin-right: 0.5rem;
|
| 781 |
+
transform: scale(1.2);
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
.checkbox-container:hover {
|
| 785 |
+
color: #007bff;
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
/* Ion Controls */
|
| 789 |
+
.ion-controls {
|
| 790 |
+
display: flex;
|
| 791 |
+
gap: 0.5rem;
|
| 792 |
+
align-items: center;
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
.ion-controls select {
|
| 796 |
+
flex: 1;
|
| 797 |
+
}
|
| 798 |
+
|
| 799 |
+
|
| 800 |
+
|
| 801 |
+
|
| 802 |
+
#ligand-forcefield-section {
|
| 803 |
+
transition: all 0.3s ease;
|
| 804 |
+
}
|
| 805 |
+
|
| 806 |
+
#ligand-forcefield-section.disabled {
|
| 807 |
+
opacity: 0.5;
|
| 808 |
+
pointer-events: none;
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
/* Tooltip styling for better text wrapping */
|
| 812 |
+
.tooltip {
|
| 813 |
+
max-width: 300px;
|
| 814 |
+
}
|
| 815 |
+
|
| 816 |
+
.tooltip-inner {
|
| 817 |
+
text-align: left;
|
| 818 |
+
white-space: normal;
|
| 819 |
+
word-wrap: break-word;
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
/* Button Styles */
|
| 823 |
+
.btn {
|
| 824 |
+
display: inline-flex;
|
| 825 |
+
align-items: center;
|
| 826 |
+
padding: 0.75rem 1.5rem;
|
| 827 |
+
border: none;
|
| 828 |
+
border-radius: 6px;
|
| 829 |
+
font-size: 1rem;
|
| 830 |
+
font-weight: 600;
|
| 831 |
+
cursor: pointer;
|
| 832 |
+
transition: all 0.3s ease;
|
| 833 |
+
text-decoration: none;
|
| 834 |
+
gap: 0.5rem;
|
| 835 |
+
}
|
| 836 |
+
|
| 837 |
+
.btn:hover {
|
| 838 |
+
transform: translateY(-2px);
|
| 839 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
.btn:active {
|
| 843 |
+
transform: translateY(0);
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
.btn-primary {
|
| 847 |
+
background: linear-gradient(135deg, #3498db, #2980b9);
|
| 848 |
+
color: white;
|
| 849 |
+
}
|
| 850 |
+
|
| 851 |
+
.btn-primary:hover {
|
| 852 |
+
background: linear-gradient(135deg, #2980b9, #1f618d);
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
.btn-secondary {
|
| 856 |
+
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
|
| 857 |
+
color: white;
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
.btn-secondary:hover {
|
| 861 |
+
background: linear-gradient(135deg, #7f8c8d, #6c7b7d);
|
| 862 |
+
}
|
| 863 |
+
|
| 864 |
+
.btn-success {
|
| 865 |
+
background: linear-gradient(135deg, #27ae60, #229954);
|
| 866 |
+
color: white;
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
.btn-success:hover {
|
| 870 |
+
background: linear-gradient(135deg, #229954, #1e8449);
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
.btn-info {
|
| 874 |
+
background: linear-gradient(135deg, #17a2b8, #138496);
|
| 875 |
+
color: white;
|
| 876 |
+
}
|
| 877 |
+
|
| 878 |
+
.btn-info:hover {
|
| 879 |
+
background: linear-gradient(135deg, #138496, #117a8b);
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
.btn i {
|
| 883 |
+
font-size: 1rem;
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
/* Footer */
|
| 887 |
+
.footer {
|
| 888 |
+
background: #2c3e50;
|
| 889 |
+
color: white;
|
| 890 |
+
text-align: center;
|
| 891 |
+
padding: 2rem;
|
| 892 |
+
margin-top: 2rem;
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
.footer p {
|
| 896 |
+
margin: 0;
|
| 897 |
+
opacity: 0.8;
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
/* Responsive Design */
|
| 901 |
+
@media (max-width: 768px) {
|
| 902 |
+
.container {
|
| 903 |
+
margin: 0;
|
| 904 |
+
box-shadow: none;
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
.header-content h1 {
|
| 908 |
+
font-size: 2rem;
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
.tab-navigation {
|
| 912 |
+
flex-direction: column;
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
.tab-button {
|
| 916 |
+
min-width: auto;
|
| 917 |
+
border-bottom: 1px solid #2c3e50;
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
.main-content {
|
| 921 |
+
padding: 1rem;
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
.input-methods {
|
| 925 |
+
grid-template-columns: 1fr;
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
.divider {
|
| 929 |
+
order: 2;
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
.preview-content {
|
| 933 |
+
grid-template-columns: 1fr;
|
| 934 |
+
}
|
| 935 |
+
|
| 936 |
+
.params-grid {
|
| 937 |
+
grid-template-columns: 1fr;
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
.form-row {
|
| 941 |
+
grid-template-columns: 1fr;
|
| 942 |
+
}
|
| 943 |
+
|
| 944 |
+
.generation-controls {
|
| 945 |
+
flex-direction: column;
|
| 946 |
+
}
|
| 947 |
+
|
| 948 |
+
.download-options {
|
| 949 |
+
flex-direction: column;
|
| 950 |
+
}
|
| 951 |
+
|
| 952 |
+
.summary-content {
|
| 953 |
+
grid-template-columns: 1fr;
|
| 954 |
+
}
|
| 955 |
+
}
|
| 956 |
+
|
| 957 |
+
@media (max-width: 480px) {
|
| 958 |
+
.header-content h1 {
|
| 959 |
+
font-size: 1.5rem;
|
| 960 |
+
}
|
| 961 |
+
|
| 962 |
+
.card {
|
| 963 |
+
padding: 1rem;
|
| 964 |
+
}
|
| 965 |
+
|
| 966 |
+
.card h2 {
|
| 967 |
+
font-size: 1.5rem;
|
| 968 |
+
}
|
| 969 |
+
|
| 970 |
+
.btn {
|
| 971 |
+
padding: 0.5rem 1rem;
|
| 972 |
+
font-size: 0.9rem;
|
| 973 |
+
}
|
| 974 |
+
}
|
| 975 |
+
|
| 976 |
+
/* Loading Animation */
|
| 977 |
+
.loading {
|
| 978 |
+
display: inline-block;
|
| 979 |
+
width: 20px;
|
| 980 |
+
height: 20px;
|
| 981 |
+
border: 3px solid #f3f3f3;
|
| 982 |
+
border-top: 3px solid #3498db;
|
| 983 |
+
border-radius: 50%;
|
| 984 |
+
animation: spin 1s linear infinite;
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
@keyframes spin {
|
| 988 |
+
0% { transform: rotate(0deg); }
|
| 989 |
+
100% { transform: rotate(360deg); }
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
/* Structure Preparation Styles */
|
| 993 |
+
.card-description {
|
| 994 |
+
color: #7f8c8d;
|
| 995 |
+
margin-bottom: 2rem;
|
| 996 |
+
font-style: italic;
|
| 997 |
+
}
|
| 998 |
+
|
| 999 |
+
.prep-sections {
|
| 1000 |
+
display: grid;
|
| 1001 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 1002 |
+
gap: 2rem;
|
| 1003 |
+
margin-bottom: 2rem;
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
+
.prep-section {
|
| 1007 |
+
background: #f8f9fa;
|
| 1008 |
+
border-radius: 8px;
|
| 1009 |
+
padding: 1.5rem;
|
| 1010 |
+
border: 1px solid #dee2e6;
|
| 1011 |
+
}
|
| 1012 |
+
|
| 1013 |
+
.prep-section h3 {
|
| 1014 |
+
color: #2c3e50;
|
| 1015 |
+
margin-bottom: 1rem;
|
| 1016 |
+
font-size: 1.2rem;
|
| 1017 |
+
border-bottom: 1px solid #bdc3c7;
|
| 1018 |
+
padding-bottom: 0.5rem;
|
| 1019 |
+
}
|
| 1020 |
+
|
| 1021 |
+
.prep-section h3 i {
|
| 1022 |
+
margin-right: 0.5rem;
|
| 1023 |
+
color: #3498db;
|
| 1024 |
+
}
|
| 1025 |
+
|
| 1026 |
+
.prep-section-fullwidth {
|
| 1027 |
+
width: 100%;
|
| 1028 |
+
margin-top: 1rem;
|
| 1029 |
+
margin-bottom: 2rem;
|
| 1030 |
+
}
|
| 1031 |
+
|
| 1032 |
+
.prep-options {
|
| 1033 |
+
display: flex;
|
| 1034 |
+
flex-direction: column;
|
| 1035 |
+
gap: 1rem;
|
| 1036 |
+
}
|
| 1037 |
+
|
| 1038 |
+
.prep-option {
|
| 1039 |
+
background: white;
|
| 1040 |
+
border-radius: 6px;
|
| 1041 |
+
padding: 1rem;
|
| 1042 |
+
border: 1px solid #e1e8ed;
|
| 1043 |
+
transition: all 0.3s ease;
|
| 1044 |
+
}
|
| 1045 |
+
|
| 1046 |
+
.prep-option:hover {
|
| 1047 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 1048 |
+
transform: translateY(-1px);
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
.checkbox-container {
|
| 1052 |
+
display: flex;
|
| 1053 |
+
align-items: center;
|
| 1054 |
+
cursor: pointer;
|
| 1055 |
+
font-weight: 600;
|
| 1056 |
+
color: #2c3e50;
|
| 1057 |
+
margin-bottom: 0.5rem;
|
| 1058 |
+
}
|
| 1059 |
+
|
| 1060 |
+
.checkbox-container input[type="checkbox"] {
|
| 1061 |
+
margin-right: 0.75rem;
|
| 1062 |
+
transform: scale(1.2);
|
| 1063 |
+
}
|
| 1064 |
+
|
| 1065 |
+
.option-description {
|
| 1066 |
+
color: #7f8c8d;
|
| 1067 |
+
font-size: 0.9rem;
|
| 1068 |
+
margin: 0;
|
| 1069 |
+
margin-left: 1.5rem;
|
| 1070 |
+
}
|
| 1071 |
+
|
| 1072 |
+
.prep-actions {
|
| 1073 |
+
display: flex;
|
| 1074 |
+
gap: 1rem;
|
| 1075 |
+
margin-bottom: 2rem;
|
| 1076 |
+
flex-wrap: wrap;
|
| 1077 |
+
}
|
| 1078 |
+
|
| 1079 |
+
.prep-status {
|
| 1080 |
+
background: #e8f5e8;
|
| 1081 |
+
border: 1px solid #c3e6cb;
|
| 1082 |
+
border-radius: 8px;
|
| 1083 |
+
padding: 1.5rem;
|
| 1084 |
+
margin-bottom: 2rem;
|
| 1085 |
+
}
|
| 1086 |
+
|
| 1087 |
+
.prep-status h3 {
|
| 1088 |
+
color: #155724;
|
| 1089 |
+
margin-bottom: 1rem;
|
| 1090 |
+
}
|
| 1091 |
+
|
| 1092 |
+
.status-content {
|
| 1093 |
+
color: #155724;
|
| 1094 |
+
}
|
| 1095 |
+
|
| 1096 |
+
.prepared-structure-preview {
|
| 1097 |
+
background: #f8f9fa;
|
| 1098 |
+
border-radius: 8px;
|
| 1099 |
+
padding: 1.5rem;
|
| 1100 |
+
border: 1px solid #dee2e6;
|
| 1101 |
+
margin-top: 2rem;
|
| 1102 |
+
width: 100%;
|
| 1103 |
+
box-sizing: border-box;
|
| 1104 |
+
}
|
| 1105 |
+
|
| 1106 |
+
#sequence-viewer-section {
|
| 1107 |
+
width: 100%;
|
| 1108 |
+
box-sizing: border-box;
|
| 1109 |
+
}
|
| 1110 |
+
|
| 1111 |
+
.prepared-structure-preview h3 {
|
| 1112 |
+
color: #2c3e50;
|
| 1113 |
+
margin-bottom: 1rem;
|
| 1114 |
+
font-size: 1.2rem;
|
| 1115 |
+
}
|
| 1116 |
+
|
| 1117 |
+
.structure-info p {
|
| 1118 |
+
margin: 0.5rem 0;
|
| 1119 |
+
font-size: 1rem;
|
| 1120 |
+
}
|
| 1121 |
+
|
| 1122 |
+
.structure-visualization {
|
| 1123 |
+
margin-top: 1rem;
|
| 1124 |
+
}
|
| 1125 |
+
|
| 1126 |
+
#prepared-ngl-viewer {
|
| 1127 |
+
border-radius: 4px;
|
| 1128 |
+
background: #f8f9fa;
|
| 1129 |
+
border: 1px solid #dee2e6;
|
| 1130 |
+
}
|
| 1131 |
+
|
| 1132 |
+
/* Custom Checkbox Styles */
|
| 1133 |
+
.checkbox-container input[type="checkbox"] {
|
| 1134 |
+
appearance: none;
|
| 1135 |
+
width: 20px;
|
| 1136 |
+
height: 20px;
|
| 1137 |
+
border: 2px solid #bdc3c7;
|
| 1138 |
+
border-radius: 4px;
|
| 1139 |
+
background: white;
|
| 1140 |
+
position: relative;
|
| 1141 |
+
cursor: pointer;
|
| 1142 |
+
transition: all 0.3s ease;
|
| 1143 |
+
}
|
| 1144 |
+
|
| 1145 |
+
.checkbox-container input[type="checkbox"]:checked {
|
| 1146 |
+
background: #3498db;
|
| 1147 |
+
border-color: #3498db;
|
| 1148 |
+
}
|
| 1149 |
+
|
| 1150 |
+
.checkbox-container input[type="checkbox"]:checked::after {
|
| 1151 |
+
content: '✓';
|
| 1152 |
+
position: absolute;
|
| 1153 |
+
top: 50%;
|
| 1154 |
+
left: 50%;
|
| 1155 |
+
transform: translate(-50%, -50%);
|
| 1156 |
+
color: white;
|
| 1157 |
+
font-weight: bold;
|
| 1158 |
+
font-size: 14px;
|
| 1159 |
+
}
|
| 1160 |
+
|
| 1161 |
+
.checkbox-container input[type="checkbox"]:hover {
|
| 1162 |
+
border-color: #3498db;
|
| 1163 |
+
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
| 1164 |
+
}
|
| 1165 |
+
|
| 1166 |
+
/* Responsive Design for Structure Prep */
|
| 1167 |
+
@media (max-width: 768px) {
|
| 1168 |
+
.prep-sections {
|
| 1169 |
+
grid-template-columns: 1fr;
|
| 1170 |
+
}
|
| 1171 |
+
|
| 1172 |
+
.prep-actions {
|
| 1173 |
+
flex-direction: column;
|
| 1174 |
+
}
|
| 1175 |
+
|
| 1176 |
+
.prep-option {
|
| 1177 |
+
padding: 0.75rem;
|
| 1178 |
+
}
|
| 1179 |
+
|
| 1180 |
+
.option-description {
|
| 1181 |
+
margin-left: 1.25rem;
|
| 1182 |
+
}
|
| 1183 |
+
}
|
| 1184 |
+
|
| 1185 |
+
/* Utility Classes */
|
| 1186 |
+
.text-center { text-align: center; }
|
| 1187 |
+
.text-left { text-align: left; }
|
| 1188 |
+
.text-right { text-align: right; }
|
| 1189 |
+
.mt-1 { margin-top: 0.5rem; }
|
| 1190 |
+
.mt-2 { margin-top: 1rem; }
|
| 1191 |
+
.mt-3 { margin-top: 1.5rem; }
|
| 1192 |
+
.mb-1 { margin-bottom: 0.5rem; }
|
| 1193 |
+
.mb-2 { margin-bottom: 1rem; }
|
| 1194 |
+
.mb-3 { margin-bottom: 1.5rem; }
|
| 1195 |
+
.p-1 { padding: 0.5rem; }
|
| 1196 |
+
.p-2 { padding: 1rem; }
|
| 1197 |
+
.p-3 { padding: 1.5rem; }
|
| 1198 |
+
|
| 1199 |
+
/* Missing Residues Horizontal Display */
|
| 1200 |
+
.missing-residues-horizontal {
|
| 1201 |
+
display: inline;
|
| 1202 |
+
color: #155724;
|
| 1203 |
+
font-weight: bold;
|
| 1204 |
+
font-size: 0.95rem;
|
| 1205 |
+
word-wrap: break-word;
|
| 1206 |
+
white-space: normal;
|
| 1207 |
+
line-height: 1.6;
|
| 1208 |
+
margin: 0;
|
| 1209 |
+
padding: 0;
|
| 1210 |
+
}
|
| 1211 |
+
|
| 1212 |
+
.chain-missing-info {
|
| 1213 |
+
margin-bottom: 1.5rem;
|
| 1214 |
+
}
|
| 1215 |
+
|
| 1216 |
+
.chain-missing-info h4 {
|
| 1217 |
+
color: #155724;
|
| 1218 |
+
margin-bottom: 0.5rem;
|
| 1219 |
+
}
|
| 1220 |
+
|
| 1221 |
+
/* Sequence Viewer Styles */
|
| 1222 |
+
.sequence-viewer-container {
|
| 1223 |
+
background: #ffffff;
|
| 1224 |
+
border: 1px solid #dee2e6;
|
| 1225 |
+
border-radius: 8px;
|
| 1226 |
+
padding: 1.5rem;
|
| 1227 |
+
max-height: 600px;
|
| 1228 |
+
overflow-y: auto;
|
| 1229 |
+
font-family: 'Courier New', monospace;
|
| 1230 |
+
width: 100%;
|
| 1231 |
+
box-sizing: border-box;
|
| 1232 |
+
}
|
| 1233 |
+
|
| 1234 |
+
.sequence-chain-container {
|
| 1235 |
+
margin-bottom: 2rem;
|
| 1236 |
+
border-bottom: 2px solid #e9ecef;
|
| 1237 |
+
padding-bottom: 1.5rem;
|
| 1238 |
+
}
|
| 1239 |
+
|
| 1240 |
+
.sequence-chain-container:last-child {
|
| 1241 |
+
border-bottom: none;
|
| 1242 |
+
margin-bottom: 0;
|
| 1243 |
+
}
|
| 1244 |
+
|
| 1245 |
+
.sequence-chain-header {
|
| 1246 |
+
margin-bottom: 1rem;
|
| 1247 |
+
}
|
| 1248 |
+
|
| 1249 |
+
.sequence-chain-header h4 {
|
| 1250 |
+
margin: 0;
|
| 1251 |
+
font-size: 1.1rem;
|
| 1252 |
+
font-weight: 600;
|
| 1253 |
+
}
|
| 1254 |
+
|
| 1255 |
+
.sequence-display {
|
| 1256 |
+
background: #f8f9fa;
|
| 1257 |
+
border: 1px solid #dee2e6;
|
| 1258 |
+
border-radius: 4px;
|
| 1259 |
+
padding: 1rem;
|
| 1260 |
+
font-size: 14px;
|
| 1261 |
+
line-height: 1.8;
|
| 1262 |
+
width: 100%;
|
| 1263 |
+
box-sizing: border-box;
|
| 1264 |
+
}
|
| 1265 |
+
|
| 1266 |
+
.sequence-line {
|
| 1267 |
+
display: flex;
|
| 1268 |
+
margin-bottom: 2px;
|
| 1269 |
+
white-space: nowrap;
|
| 1270 |
+
}
|
| 1271 |
+
|
| 1272 |
+
.sequence-line-number {
|
| 1273 |
+
color: #6c757d;
|
| 1274 |
+
margin-right: 1rem;
|
| 1275 |
+
min-width: 60px;
|
| 1276 |
+
text-align: right;
|
| 1277 |
+
font-size: 12px;
|
| 1278 |
+
user-select: none;
|
| 1279 |
+
}
|
| 1280 |
+
|
| 1281 |
+
.sequence-characters {
|
| 1282 |
+
letter-spacing: 2px;
|
| 1283 |
+
word-spacing: 0;
|
| 1284 |
+
flex: 1;
|
| 1285 |
+
overflow-x: auto;
|
| 1286 |
+
min-width: 0;
|
| 1287 |
+
}
|
| 1288 |
+
|
| 1289 |
+
.sequence-char {
|
| 1290 |
+
display: inline-block;
|
| 1291 |
+
padding: 2px 1px;
|
| 1292 |
+
transition: background-color 0.2s;
|
| 1293 |
+
}
|
| 1294 |
+
|
| 1295 |
+
.sequence-char:hover {
|
| 1296 |
+
background-color: rgba(0, 0, 0, 0.1);
|
| 1297 |
+
border-radius: 2px;
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
+
/* Trim Residues Section */
|
| 1301 |
+
.trim-info-box {
|
| 1302 |
+
background: #e7f3ff;
|
| 1303 |
+
border: 1px solid #b3d9ff;
|
| 1304 |
+
border-radius: 6px;
|
| 1305 |
+
padding: 1rem;
|
| 1306 |
+
margin-top: 1rem;
|
| 1307 |
+
margin-bottom: 1rem;
|
| 1308 |
+
color: #004085;
|
| 1309 |
+
font-size: 0.9rem;
|
| 1310 |
+
line-height: 1.6;
|
| 1311 |
+
}
|
| 1312 |
+
|
| 1313 |
+
.trim-info-box i {
|
| 1314 |
+
color: #0066cc;
|
| 1315 |
+
margin-right: 0.5rem;
|
| 1316 |
+
}
|
| 1317 |
+
|
| 1318 |
+
.trim-info-box strong {
|
| 1319 |
+
color: #003366;
|
| 1320 |
+
}
|
| 1321 |
+
|
| 1322 |
+
.trim-residues-container {
|
| 1323 |
+
margin-top: 1rem;
|
| 1324 |
+
display: grid;
|
| 1325 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 1326 |
+
gap: 1rem;
|
| 1327 |
+
}
|
| 1328 |
+
|
| 1329 |
+
.trim-chain-controls {
|
| 1330 |
+
background: #f8f9fa;
|
| 1331 |
+
border: 1px solid #dee2e6;
|
| 1332 |
+
border-radius: 6px;
|
| 1333 |
+
padding: 1rem;
|
| 1334 |
+
}
|
| 1335 |
+
|
| 1336 |
+
.trim-chain-controls h5 {
|
| 1337 |
+
color: #2c3e50;
|
| 1338 |
+
margin-bottom: 0.75rem;
|
| 1339 |
+
font-size: 1rem;
|
| 1340 |
+
}
|
| 1341 |
+
|
| 1342 |
+
.trim-inputs {
|
| 1343 |
+
display: flex;
|
| 1344 |
+
gap: 1.5rem;
|
| 1345 |
+
align-items: center;
|
| 1346 |
+
flex-wrap: wrap;
|
| 1347 |
+
}
|
| 1348 |
+
|
| 1349 |
+
.trim-input-group {
|
| 1350 |
+
display: flex;
|
| 1351 |
+
align-items: center;
|
| 1352 |
+
gap: 0.5rem;
|
| 1353 |
+
}
|
| 1354 |
+
|
| 1355 |
+
.trim-input-group label {
|
| 1356 |
+
font-weight: 600;
|
| 1357 |
+
color: #495057;
|
| 1358 |
+
font-size: 0.9rem;
|
| 1359 |
+
min-width: 100px;
|
| 1360 |
+
}
|
| 1361 |
+
|
| 1362 |
+
.trim-limit {
|
| 1363 |
+
font-weight: normal;
|
| 1364 |
+
color: #6c757d;
|
| 1365 |
+
font-size: 0.85rem;
|
| 1366 |
+
font-style: italic;
|
| 1367 |
+
}
|
| 1368 |
+
|
| 1369 |
+
.trim-input-group input[type="number"] {
|
| 1370 |
+
width: 80px;
|
| 1371 |
+
padding: 0.5rem;
|
| 1372 |
+
border: 1px solid #ced4da;
|
| 1373 |
+
border-radius: 4px;
|
| 1374 |
+
font-size: 0.9rem;
|
| 1375 |
+
}
|
| 1376 |
+
|
| 1377 |
+
.trim-input-group input[type="number"]:focus {
|
| 1378 |
+
outline: none;
|
| 1379 |
+
border-color: #3498db;
|
| 1380 |
+
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
| 1381 |
+
}
|
| 1382 |
+
|
| 1383 |
+
.trim-info {
|
| 1384 |
+
font-size: 0.85rem;
|
| 1385 |
+
color: #6c757d;
|
| 1386 |
+
margin-top: 0.5rem;
|
| 1387 |
+
font-style: italic;
|
| 1388 |
+
}
|
| 1389 |
+
|
| 1390 |
+
/* Log Modal Styles */
|
| 1391 |
+
.log-modal {
|
| 1392 |
+
display: none;
|
| 1393 |
+
position: fixed;
|
| 1394 |
+
z-index: 10000;
|
| 1395 |
+
left: 0;
|
| 1396 |
+
top: 0;
|
| 1397 |
+
width: 100%;
|
| 1398 |
+
height: 100%;
|
| 1399 |
+
background-color: rgba(0, 0, 0, 0.5);
|
| 1400 |
+
animation: fadeIn 0.3s;
|
| 1401 |
+
}
|
| 1402 |
+
|
| 1403 |
+
@keyframes fadeIn {
|
| 1404 |
+
from { opacity: 0; }
|
| 1405 |
+
to { opacity: 1; }
|
| 1406 |
+
}
|
| 1407 |
+
|
| 1408 |
+
.log-modal-content {
|
| 1409 |
+
background-color: #fefefe;
|
| 1410 |
+
margin: 2% auto;
|
| 1411 |
+
padding: 0;
|
| 1412 |
+
border: 1px solid #888;
|
| 1413 |
+
border-radius: 8px;
|
| 1414 |
+
width: 90%;
|
| 1415 |
+
max-width: 900px;
|
| 1416 |
+
max-height: 90vh;
|
| 1417 |
+
display: flex;
|
| 1418 |
+
flex-direction: column;
|
| 1419 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
| 1420 |
+
animation: slideDown 0.3s;
|
| 1421 |
+
}
|
| 1422 |
+
|
| 1423 |
+
@keyframes slideDown {
|
| 1424 |
+
from {
|
| 1425 |
+
transform: translateY(-50px);
|
| 1426 |
+
opacity: 0;
|
| 1427 |
+
}
|
| 1428 |
+
to {
|
| 1429 |
+
transform: translateY(0);
|
| 1430 |
+
opacity: 1;
|
| 1431 |
+
}
|
| 1432 |
+
}
|
| 1433 |
+
|
| 1434 |
+
.log-modal-header {
|
| 1435 |
+
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
| 1436 |
+
color: white;
|
| 1437 |
+
padding: 1rem 1.5rem;
|
| 1438 |
+
display: flex;
|
| 1439 |
+
justify-content: space-between;
|
| 1440 |
+
align-items: center;
|
| 1441 |
+
border-radius: 8px 8px 0 0;
|
| 1442 |
+
}
|
| 1443 |
+
|
| 1444 |
+
.log-modal-header h3 {
|
| 1445 |
+
margin: 0;
|
| 1446 |
+
font-size: 1.3rem;
|
| 1447 |
+
font-weight: 500;
|
| 1448 |
+
}
|
| 1449 |
+
|
| 1450 |
+
.log-modal-close {
|
| 1451 |
+
color: white;
|
| 1452 |
+
font-size: 2rem;
|
| 1453 |
+
font-weight: bold;
|
| 1454 |
+
background: none;
|
| 1455 |
+
border: none;
|
| 1456 |
+
cursor: pointer;
|
| 1457 |
+
padding: 0;
|
| 1458 |
+
width: 30px;
|
| 1459 |
+
height: 30px;
|
| 1460 |
+
display: flex;
|
| 1461 |
+
align-items: center;
|
| 1462 |
+
justify-content: center;
|
| 1463 |
+
border-radius: 50%;
|
| 1464 |
+
transition: background-color 0.2s;
|
| 1465 |
+
}
|
| 1466 |
+
|
| 1467 |
+
.log-modal-close:hover {
|
| 1468 |
+
background-color: rgba(255, 255, 255, 0.2);
|
| 1469 |
+
}
|
| 1470 |
+
|
| 1471 |
+
.log-container {
|
| 1472 |
+
flex: 1;
|
| 1473 |
+
overflow-y: auto;
|
| 1474 |
+
padding: 1rem;
|
| 1475 |
+
background-color: #1e1e1e;
|
| 1476 |
+
color: #d4d4d4;
|
| 1477 |
+
font-family: 'Courier New', monospace;
|
| 1478 |
+
font-size: 0.9rem;
|
| 1479 |
+
line-height: 1.6;
|
| 1480 |
+
max-height: calc(90vh - 80px);
|
| 1481 |
+
}
|
| 1482 |
+
|
| 1483 |
+
.log-content {
|
| 1484 |
+
min-height: 100%;
|
| 1485 |
+
}
|
| 1486 |
+
|
| 1487 |
+
.log-line {
|
| 1488 |
+
padding: 0.3rem 0;
|
| 1489 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
| 1490 |
+
word-wrap: break-word;
|
| 1491 |
+
}
|
| 1492 |
+
|
| 1493 |
+
.log-line:last-child {
|
| 1494 |
+
border-bottom: none;
|
| 1495 |
+
}
|
| 1496 |
+
|
| 1497 |
+
.log-time {
|
| 1498 |
+
color: #858585;
|
| 1499 |
+
margin-right: 0.5rem;
|
| 1500 |
+
}
|
| 1501 |
+
|
| 1502 |
+
.log-icon {
|
| 1503 |
+
margin-right: 0.5rem;
|
| 1504 |
+
}
|
| 1505 |
+
|
| 1506 |
+
.log-message {
|
| 1507 |
+
color: #d4d4d4;
|
| 1508 |
+
}
|
| 1509 |
+
|
| 1510 |
+
.log-info .log-message {
|
| 1511 |
+
color: #d4d4d4;
|
| 1512 |
+
}
|
| 1513 |
+
|
| 1514 |
+
.log-success .log-message {
|
| 1515 |
+
color: #4ec9b0;
|
| 1516 |
+
}
|
| 1517 |
+
|
| 1518 |
+
.log-warning .log-message {
|
| 1519 |
+
color: #dcdcaa;
|
| 1520 |
+
}
|
| 1521 |
+
|
| 1522 |
+
.log-error .log-message {
|
| 1523 |
+
color: #f48771;
|
| 1524 |
+
}
|
| 1525 |
+
|
| 1526 |
+
.log-result {
|
| 1527 |
+
margin-top: 1.5rem;
|
| 1528 |
+
padding: 1rem;
|
| 1529 |
+
background-color: #252526;
|
| 1530 |
+
border-left: 4px solid #4ec9b0;
|
| 1531 |
+
border-radius: 4px;
|
| 1532 |
+
}
|
| 1533 |
+
|
| 1534 |
+
.log-result h4 {
|
| 1535 |
+
color: #4ec9b0;
|
| 1536 |
+
margin-bottom: 0.5rem;
|
| 1537 |
+
}
|
| 1538 |
+
|
| 1539 |
+
.log-result p {
|
| 1540 |
+
color: #d4d4d4;
|
| 1541 |
+
margin: 0.5rem 0;
|
| 1542 |
+
}
|
| 1543 |
+
|
| 1544 |
+
.log-result ul {
|
| 1545 |
+
margin: 0.5rem 0;
|
| 1546 |
+
padding-left: 1.5rem;
|
| 1547 |
+
color: #d4d4d4;
|
| 1548 |
+
}
|
| 1549 |
+
|
| 1550 |
+
.log-result li {
|
| 1551 |
+
margin: 0.5rem 0;
|
| 1552 |
+
}
|
| 1553 |
+
|
| 1554 |
+
/* File Editor Modal Styles */
|
| 1555 |
+
#file-content-modal {
|
| 1556 |
+
animation: fadeIn 0.3s;
|
| 1557 |
+
}
|
| 1558 |
+
|
| 1559 |
+
#file-content-modal > div {
|
| 1560 |
+
animation: slideDown 0.3s;
|
| 1561 |
+
}
|
| 1562 |
+
|
| 1563 |
+
#modal-content-edit {
|
| 1564 |
+
line-height: 1.6;
|
| 1565 |
+
tab-size: 4;
|
| 1566 |
+
-moz-tab-size: 4;
|
| 1567 |
+
}
|
| 1568 |
+
|
| 1569 |
+
#modal-content-edit:focus {
|
| 1570 |
+
outline: none;
|
| 1571 |
+
border-color: #0056b3;
|
| 1572 |
+
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
|
| 1573 |
+
}
|
| 1574 |
+
|
| 1575 |
+
#save-status {
|
| 1576 |
+
font-weight: 500;
|
| 1577 |
+
animation: slideUp 0.3s;
|
| 1578 |
+
}
|
| 1579 |
+
|
| 1580 |
+
@keyframes slideUp {
|
| 1581 |
+
from {
|
| 1582 |
+
opacity: 0;
|
| 1583 |
+
transform: translateY(10px);
|
| 1584 |
+
}
|
| 1585 |
+
to {
|
| 1586 |
+
opacity: 1;
|
| 1587 |
+
transform: translateY(0);
|
| 1588 |
+
}
|
| 1589 |
+
}
|
| 1590 |
+
|
| 1591 |
+
#edit-file-btn:hover {
|
| 1592 |
+
background: #0056b3 !important;
|
| 1593 |
+
}
|
| 1594 |
+
|
| 1595 |
+
#save-file-btn:hover {
|
| 1596 |
+
background: #218838 !important;
|
| 1597 |
+
}
|
| 1598 |
+
|
| 1599 |
+
#cancel-edit-btn:hover {
|
| 1600 |
+
background: #5a6268 !important;
|
| 1601 |
+
}
|
| 1602 |
+
|
| 1603 |
+
/* Frozen/Disabled Checkboxes - Gray only the checkbox, keep text bold and normal */
|
| 1604 |
+
.checkbox-container input[type="checkbox"]:disabled {
|
| 1605 |
+
opacity: 0.5 !important;
|
| 1606 |
+
cursor: not-allowed !important;
|
| 1607 |
+
pointer-events: none !important;
|
| 1608 |
+
-webkit-appearance: none !important;
|
| 1609 |
+
appearance: none !important;
|
| 1610 |
+
}
|
| 1611 |
+
|
| 1612 |
+
.checkbox-container input[type="checkbox"]:disabled + .checkmark {
|
| 1613 |
+
opacity: 0.5 !important;
|
| 1614 |
+
}
|
| 1615 |
+
|
| 1616 |
+
/* Keep text labels bold and normal color when checkbox is disabled */
|
| 1617 |
+
.checkbox-container:has(input[type="checkbox"]:disabled) {
|
| 1618 |
+
cursor: not-allowed !important;
|
| 1619 |
+
pointer-events: none !important;
|
| 1620 |
+
opacity: 1 !important;
|
| 1621 |
+
color: #2c3e50 !important;
|
| 1622 |
+
font-weight: 600 !important;
|
| 1623 |
+
}
|
| 1624 |
+
|
| 1625 |
+
.checkbox-container:has(input[type="checkbox"]:disabled):hover {
|
| 1626 |
+
color: #2c3e50 !important;
|
| 1627 |
+
}
|
| 1628 |
+
|
| 1629 |
+
/* Prevent interaction on disabled checkbox inputs */
|
| 1630 |
+
.checkbox-container input[type="checkbox"]:disabled {
|
| 1631 |
+
cursor: not-allowed !important;
|
| 1632 |
+
pointer-events: none !important;
|
| 1633 |
+
opacity: 0.5 !important;
|
| 1634 |
+
}
|
| 1635 |
+
|
| 1636 |
+
/* Gray out the checkmark for disabled checkboxes */
|
| 1637 |
+
.checkbox-container input[type="checkbox"]:disabled:checked {
|
| 1638 |
+
background-color: #6c757d !important;
|
| 1639 |
+
border-color: #6c757d !important;
|
| 1640 |
+
}
|
| 1641 |
+
|
| 1642 |
+
.checkbox-container input[type="checkbox"]:disabled:checked::after {
|
| 1643 |
+
color: #ffffff !important;
|
| 1644 |
+
}
|
| 1645 |
+
|
| 1646 |
+
/* Frozen/Disabled Toggle Switches - Gray only the toggle, keep it hidden */
|
| 1647 |
+
.switch input[type="checkbox"]:disabled {
|
| 1648 |
+
opacity: 0 !important; /* Keep the input completely hidden */
|
| 1649 |
+
cursor: not-allowed !important;
|
| 1650 |
+
pointer-events: none !important;
|
| 1651 |
+
width: 0 !important;
|
| 1652 |
+
height: 0 !important;
|
| 1653 |
+
}
|
| 1654 |
+
|
| 1655 |
+
.switch input[type="checkbox"]:disabled + .slider {
|
| 1656 |
+
opacity: 0.5 !important;
|
| 1657 |
+
cursor: not-allowed !important;
|
| 1658 |
+
pointer-events: none !important;
|
| 1659 |
+
background-color: #95a5a6 !important; /* Gray color for disabled toggles */
|
| 1660 |
+
}
|
| 1661 |
+
|
| 1662 |
+
.switch input[type="checkbox"]:disabled:checked + .slider {
|
| 1663 |
+
background-color: #95a5a6 !important; /* Gray color even when checked */
|
| 1664 |
+
}
|
| 1665 |
+
|
| 1666 |
+
.switch input[type="checkbox"]:disabled:not(:checked) + .slider {
|
| 1667 |
+
background-color: #bdc3c7 !important; /* Lighter gray when unchecked */
|
| 1668 |
+
}
|
| 1669 |
+
|
| 1670 |
+
.switch:has(input[type="checkbox"]:disabled) {
|
| 1671 |
+
cursor: not-allowed !important;
|
| 1672 |
+
pointer-events: none !important;
|
| 1673 |
+
opacity: 1 !important; /* Keep switch container visible */
|
| 1674 |
+
}
|
| 1675 |
+
|
| 1676 |
+
.switch:has(input[type="checkbox"]:disabled) .slider {
|
| 1677 |
+
cursor: not-allowed !important;
|
| 1678 |
+
pointer-events: none !important;
|
| 1679 |
+
}
|
| 1680 |
+
|
| 1681 |
+
/* Ensure disabled switch input stays completely hidden */
|
| 1682 |
+
.switch input[type="checkbox"]:disabled {
|
| 1683 |
+
position: absolute !important;
|
| 1684 |
+
opacity: 0 !important;
|
| 1685 |
+
width: 0 !important;
|
| 1686 |
+
height: 0 !important;
|
| 1687 |
+
margin: 0 !important;
|
| 1688 |
+
padding: 0 !important;
|
| 1689 |
+
}
|
| 1690 |
+
|
| 1691 |
+
/* Docking Section Collapsible Styles */
|
| 1692 |
+
#docking-section.collapsed .section-description,
|
| 1693 |
+
#docking-section.collapsed .custom-plumed-section {
|
| 1694 |
+
max-height: 0;
|
| 1695 |
+
opacity: 0;
|
| 1696 |
+
margin-top: 0;
|
| 1697 |
+
margin-bottom: 0;
|
| 1698 |
+
padding-top: 0;
|
| 1699 |
+
padding-bottom: 0;
|
| 1700 |
+
overflow: hidden;
|
| 1701 |
+
transition: max-height 0.4s ease, opacity 0.3s ease, margin 0.3s ease, padding 0.3s ease;
|
| 1702 |
+
}
|
| 1703 |
+
|
| 1704 |
+
#docking-section.collapsed .plumed-toggle-header {
|
| 1705 |
+
margin-bottom: 0;
|
| 1706 |
+
}
|
| 1707 |
+
|
| 1708 |
+
.docking-box-row {
|
| 1709 |
+
display: flex;
|
| 1710 |
+
gap: 10px;
|
| 1711 |
+
margin-bottom: 10px;
|
| 1712 |
+
}
|
| 1713 |
+
|
| 1714 |
+
.docking-box-row .form-group {
|
| 1715 |
+
flex: 1;
|
| 1716 |
+
}
|
| 1717 |
+
|
| 1718 |
+
.docking-setup-entry {
|
| 1719 |
+
margin-bottom: 15px;
|
| 1720 |
+
padding: 10px;
|
| 1721 |
+
background: white;
|
| 1722 |
+
border-radius: 5px;
|
| 1723 |
+
border: 1px solid #dee2e6;
|
| 1724 |
+
}
|
| 1725 |
+
|
| 1726 |
+
.docking-setup-header {
|
| 1727 |
+
display: flex;
|
| 1728 |
+
justify-content: space-between;
|
| 1729 |
+
align-items: center;
|
| 1730 |
+
margin-bottom: 10px;
|
| 1731 |
+
}
|
| 1732 |
+
|
| 1733 |
+
.docking-setup-chains {
|
| 1734 |
+
font-size: 0.9em;
|
| 1735 |
+
color: #6c757d;
|
| 1736 |
+
}
|
| 1737 |
+
|
| 1738 |
+
/* Docking Ligand Selection Row Styles */
|
| 1739 |
+
.docking-ligand-row {
|
| 1740 |
+
transition: all 0.2s ease;
|
| 1741 |
+
}
|
| 1742 |
+
|
| 1743 |
+
.docking-ligand-row:hover {
|
| 1744 |
+
background: #e9ecef !important;
|
| 1745 |
+
border-color: #6f42c1 !important;
|
| 1746 |
+
}
|
| 1747 |
+
|
| 1748 |
+
.docking-ligand-row .checkbox-container {
|
| 1749 |
+
display: flex;
|
| 1750 |
+
align-items: center;
|
| 1751 |
+
gap: 8px;
|
| 1752 |
+
}
|
| 1753 |
+
|
| 1754 |
+
/* Docking Box Controls Compact Layout */
|
| 1755 |
+
#docking-setup-list {
|
| 1756 |
+
display: flex;
|
| 1757 |
+
flex-wrap: wrap;
|
| 1758 |
+
gap: 15px;
|
| 1759 |
+
}
|
| 1760 |
+
|
| 1761 |
+
.docking-setup-entry {
|
| 1762 |
+
transition: all 0.2s ease;
|
| 1763 |
+
}
|
| 1764 |
+
|
| 1765 |
+
.docking-setup-entry:hover {
|
| 1766 |
+
border-color: #6f42c1 !important;
|
| 1767 |
+
box-shadow: 0 2px 8px rgba(111, 66, 193, 0.15);
|
| 1768 |
+
}
|
| 1769 |
+
|
| 1770 |
+
.docking-setup-entry input[type="number"] {
|
| 1771 |
+
transition: border-color 0.2s ease;
|
| 1772 |
+
}
|
| 1773 |
+
|
| 1774 |
+
.docking-setup-entry input[type="number"]:focus {
|
| 1775 |
+
border-color: #6f42c1;
|
| 1776 |
+
outline: none;
|
| 1777 |
+
box-shadow: 0 0 0 2px rgba(111, 66, 193, 0.1);
|
| 1778 |
+
}
|
| 1779 |
+
|
| 1780 |
+
/* Small checkmarks for chain selection */
|
| 1781 |
+
.docking-ligand-row .checkmark {
|
| 1782 |
+
width: 16px !important;
|
| 1783 |
+
height: 16px !important;
|
| 1784 |
+
}
|
| 1785 |
+
|
| 1786 |
+
.docking-ligand-row .checkmark:after {
|
| 1787 |
+
left: 5px !important;
|
| 1788 |
+
top: 2px !important;
|
| 1789 |
+
width: 4px !important;
|
| 1790 |
+
height: 8px !important;
|
| 1791 |
+
}
|
| 1792 |
+
|
| 1793 |
+
|
| 1794 |
+
/* Docking Setup Collapsible Section */
|
| 1795 |
+
.docking-setup-collapsible {
|
| 1796 |
+
transition: all 0.3s ease;
|
| 1797 |
+
}
|
| 1798 |
+
|
| 1799 |
+
.docking-setup-header {
|
| 1800 |
+
user-select: none;
|
| 1801 |
+
transition: background 0.2s ease;
|
| 1802 |
+
}
|
| 1803 |
+
|
| 1804 |
+
.docking-setup-header:hover {
|
| 1805 |
+
background: linear-gradient(135deg, #5a31a8 0%, #7d4bc7 100%) !important;
|
| 1806 |
+
}
|
| 1807 |
+
|
| 1808 |
+
.docking-setup-content {
|
| 1809 |
+
transition: all 0.3s ease;
|
| 1810 |
+
}
|
| 1811 |
+
|
| 1812 |
+
#docking-setup-toggle-icon {
|
| 1813 |
+
transition: transform 0.3s ease;
|
| 1814 |
+
}
|
| 1815 |
+
|
| 1816 |
+
/* Docking Poses Viewer Styles */
|
| 1817 |
+
.docking-ligand-tabs {
|
| 1818 |
+
display: flex;
|
| 1819 |
+
gap: 8px;
|
| 1820 |
+
margin-bottom: 15px;
|
| 1821 |
+
flex-wrap: wrap;
|
| 1822 |
+
}
|
| 1823 |
+
|
| 1824 |
+
.docking-ligand-tab {
|
| 1825 |
+
padding: 8px 16px;
|
| 1826 |
+
border: 2px solid #dee2e6;
|
| 1827 |
+
border-radius: 6px;
|
| 1828 |
+
background: #f8f9fa;
|
| 1829 |
+
cursor: pointer;
|
| 1830 |
+
font-weight: 500;
|
| 1831 |
+
transition: all 0.2s ease;
|
| 1832 |
+
display: flex;
|
| 1833 |
+
align-items: center;
|
| 1834 |
+
gap: 8px;
|
| 1835 |
+
}
|
| 1836 |
+
|
| 1837 |
+
.docking-ligand-tab:hover {
|
| 1838 |
+
border-color: #6f42c1;
|
| 1839 |
+
background: #f3e8ff;
|
| 1840 |
+
}
|
| 1841 |
+
|
| 1842 |
+
.docking-ligand-tab.active {
|
| 1843 |
+
border-color: #6f42c1;
|
| 1844 |
+
background: linear-gradient(135deg, #6f42c1 0%, #9b59b6 100%);
|
| 1845 |
+
color: white;
|
| 1846 |
+
}
|
| 1847 |
+
|
| 1848 |
+
.docking-ligand-tab .ligand-color-dot {
|
| 1849 |
+
width: 12px;
|
| 1850 |
+
height: 12px;
|
| 1851 |
+
border-radius: 50%;
|
| 1852 |
+
display: inline-block;
|
| 1853 |
+
}
|
| 1854 |
+
|
| 1855 |
+
.docking-poses-viewer-wrapper {
|
| 1856 |
+
position: relative;
|
| 1857 |
+
margin-bottom: 15px;
|
| 1858 |
+
max-width: 700px;
|
| 1859 |
+
margin-left: auto;
|
| 1860 |
+
margin-right: auto;
|
| 1861 |
+
}
|
| 1862 |
+
|
| 1863 |
+
.docking-poses-viewer {
|
| 1864 |
+
width: 100%;
|
| 1865 |
+
max-width: 700px;
|
| 1866 |
+
height: 500px;
|
| 1867 |
+
background: #fff;
|
| 1868 |
+
border-radius: 5px;
|
| 1869 |
+
border: 2px solid #6f42c1;
|
| 1870 |
+
overflow: hidden;
|
| 1871 |
+
margin: 0 auto;
|
| 1872 |
+
}
|
| 1873 |
+
|
| 1874 |
+
.pose-nav-controls {
|
| 1875 |
+
position: absolute;
|
| 1876 |
+
bottom: 0;
|
| 1877 |
+
left: 0;
|
| 1878 |
+
right: 0;
|
| 1879 |
+
display: flex;
|
| 1880 |
+
justify-content: center;
|
| 1881 |
+
align-items: center;
|
| 1882 |
+
gap: 20px;
|
| 1883 |
+
padding: 15px;
|
| 1884 |
+
background: linear-gradient(to top, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0) 100%);
|
| 1885 |
+
border-radius: 0 0 5px 5px;
|
| 1886 |
+
}
|
| 1887 |
+
|
| 1888 |
+
.pose-nav-btn {
|
| 1889 |
+
width: 50px;
|
| 1890 |
+
height: 50px;
|
| 1891 |
+
border-radius: 50%;
|
| 1892 |
+
border: 2px solid #6f42c1;
|
| 1893 |
+
background: white;
|
| 1894 |
+
color: #6f42c1;
|
| 1895 |
+
cursor: pointer;
|
| 1896 |
+
transition: all 0.2s ease;
|
| 1897 |
+
display: flex;
|
| 1898 |
+
align-items: center;
|
| 1899 |
+
justify-content: center;
|
| 1900 |
+
font-size: 1.2rem;
|
| 1901 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
| 1902 |
+
}
|
| 1903 |
+
|
| 1904 |
+
.pose-nav-btn:hover:not(:disabled) {
|
| 1905 |
+
background: #6f42c1;
|
| 1906 |
+
border-color: #6f42c1;
|
| 1907 |
+
color: white;
|
| 1908 |
+
transform: scale(1.1);
|
| 1909 |
+
}
|
| 1910 |
+
|
| 1911 |
+
.pose-nav-btn:disabled {
|
| 1912 |
+
opacity: 0.3;
|
| 1913 |
+
cursor: not-allowed;
|
| 1914 |
+
}
|
| 1915 |
+
|
| 1916 |
+
.pose-info-display {
|
| 1917 |
+
text-align: center;
|
| 1918 |
+
min-width: 200px;
|
| 1919 |
+
}
|
| 1920 |
+
|
| 1921 |
+
.pose-mode-label {
|
| 1922 |
+
font-size: 1.1rem;
|
| 1923 |
+
font-weight: 600;
|
| 1924 |
+
color: #333;
|
| 1925 |
+
}
|
| 1926 |
+
|
| 1927 |
+
.pose-energy-label {
|
| 1928 |
+
font-size: 0.95rem;
|
| 1929 |
+
color: #28a745;
|
| 1930 |
+
font-weight: 500;
|
| 1931 |
+
}
|
| 1932 |
+
|
| 1933 |
+
.pose-color-legend {
|
| 1934 |
+
display: flex;
|
| 1935 |
+
gap: 20px;
|
| 1936 |
+
justify-content: center;
|
| 1937 |
+
margin-top: 8px;
|
| 1938 |
+
font-size: 0.85rem;
|
| 1939 |
+
}
|
| 1940 |
+
|
| 1941 |
+
.pose-color-legend span {
|
| 1942 |
+
display: flex;
|
| 1943 |
+
align-items: center;
|
| 1944 |
+
gap: 6px;
|
| 1945 |
+
color: #555;
|
| 1946 |
+
}
|
| 1947 |
+
|
| 1948 |
+
.legend-dot {
|
| 1949 |
+
width: 12px;
|
| 1950 |
+
height: 12px;
|
| 1951 |
+
border-radius: 50%;
|
| 1952 |
+
display: inline-block;
|
| 1953 |
+
}
|
| 1954 |
+
|
| 1955 |
+
.legend-dot.original {
|
| 1956 |
+
background: #00ff00;
|
| 1957 |
+
}
|
| 1958 |
+
|
| 1959 |
+
.legend-dot.docked {
|
| 1960 |
+
background: #ff6b6b;
|
| 1961 |
+
}
|
| 1962 |
+
|
| 1963 |
+
.docking-poses-selection {
|
| 1964 |
+
background: #f8f9fa;
|
| 1965 |
+
border-radius: 8px;
|
| 1966 |
+
padding: 15px;
|
| 1967 |
+
border: 1px solid #dee2e6;
|
| 1968 |
+
}
|
| 1969 |
+
|
| 1970 |
+
.docking-poses-selection h5 {
|
| 1971 |
+
margin-bottom: 10px;
|
| 1972 |
+
color: #495057;
|
| 1973 |
+
font-size: 0.95rem;
|
| 1974 |
+
}
|
| 1975 |
+
|
| 1976 |
+
.pose-selection-row {
|
| 1977 |
+
display: flex;
|
| 1978 |
+
align-items: center;
|
| 1979 |
+
gap: 15px;
|
| 1980 |
+
padding: 10px;
|
| 1981 |
+
background: white;
|
| 1982 |
+
border-radius: 6px;
|
| 1983 |
+
margin-bottom: 8px;
|
| 1984 |
+
border: 1px solid #e9ecef;
|
| 1985 |
+
}
|
| 1986 |
+
|
| 1987 |
+
.pose-selection-row:last-child {
|
| 1988 |
+
margin-bottom: 0;
|
| 1989 |
+
}
|
| 1990 |
+
|
| 1991 |
+
.pose-selection-label {
|
| 1992 |
+
font-weight: 500;
|
| 1993 |
+
min-width: 100px;
|
| 1994 |
+
}
|
| 1995 |
+
|
| 1996 |
+
.pose-selection-options {
|
| 1997 |
+
display: flex;
|
| 1998 |
+
gap: 15px;
|
| 1999 |
+
flex-wrap: wrap;
|
| 2000 |
+
}
|
| 2001 |
+
|
| 2002 |
+
.pose-selection-option {
|
| 2003 |
+
display: flex;
|
| 2004 |
+
align-items: center;
|
| 2005 |
+
gap: 6px;
|
| 2006 |
+
cursor: pointer;
|
| 2007 |
+
padding: 4px 8px;
|
| 2008 |
+
border-radius: 4px;
|
| 2009 |
+
transition: background 0.2s ease;
|
| 2010 |
+
}
|
| 2011 |
+
|
| 2012 |
+
.pose-selection-option:hover {
|
| 2013 |
+
background: #e9ecef;
|
| 2014 |
+
}
|
| 2015 |
+
|
| 2016 |
+
.pose-selection-option input[type="radio"] {
|
| 2017 |
+
accent-color: #6f42c1;
|
| 2018 |
+
}
|
| 2019 |
+
|
| 2020 |
+
.pose-selection-option.selected {
|
| 2021 |
+
background: #f3e8ff;
|
| 2022 |
+
border: 1px solid #6f42c1;
|
| 2023 |
+
}
|
| 2024 |
+
|
| 2025 |
+
.pose-selection-energy {
|
| 2026 |
+
font-size: 0.85rem;
|
| 2027 |
+
color: #28a745;
|
| 2028 |
+
font-weight: 500;
|
| 2029 |
+
}
|
ambermdflow/docking.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Step 1 obabel -i pdb ../4_ligands_corrected_1.pdb -o sdf -O ligand.sdf
|
| 2 |
+
|
| 3 |
+
# Step 2 Run tleap on 1_protein_no_hydrogens.pdb to protonate and add hydrogen to the pdb file
|
| 4 |
+
# leap file content:
|
| 5 |
+
# source leaprc.protein.ff14SB
|
| 6 |
+
# protein = loadpdb 1_protein_no_hydrogens.pdb
|
| 7 |
+
# savepdb protein protein.pdb
|
| 8 |
+
# quit
|
| 9 |
+
|
| 10 |
+
# Step 3 pdb4amber -i receptor.pdb -o receptor_fixed.pdb run this command on protein to add element names
|
| 11 |
+
|
| 12 |
+
# Step 4 mk_prepare_ligand.py -i ligand.sdf -o ligand.pdbqt run this command on ligand to get pdbqt file for selected ligand
|
| 13 |
+
|
| 14 |
+
# Step 4 mk_prepare_receptor.py -i receptor.pdb -o receptor -p run this command on protein to get pdbqt file for selected protein chain
|
| 15 |
+
|
| 16 |
+
# Now we are ready to run the docking
|
| 17 |
+
|
| 18 |
+
# find the center of the ligand
|
| 19 |
+
# run this script
|
| 20 |
+
#from MDAnalysis import Universe
|
| 21 |
+
#import numpy as np
|
| 22 |
+
#
|
| 23 |
+
#u = Universe("../output/4_ligands_corrected_1.pdb")
|
| 24 |
+
#
|
| 25 |
+
## replace 'LIG' with your ligand residue name
|
| 26 |
+
#ligand = u.select_atoms("all")
|
| 27 |
+
#coords = ligand.positions
|
| 28 |
+
#
|
| 29 |
+
## compute center of ligand
|
| 30 |
+
#center = coords.mean(axis=0)
|
| 31 |
+
#print("Center of ligand:", center)
|
| 32 |
+
|
| 33 |
+
#then run this vina script
|
| 34 |
+
#vina \
|
| 35 |
+
# --receptor receptor_ready.pdbqt \
|
| 36 |
+
# --ligand ligand_1.pdbqt \
|
| 37 |
+
# --center_x 34.3124 \
|
| 38 |
+
# --center_y 4.95463 \
|
| 39 |
+
# --center_z 1.774217 \
|
| 40 |
+
# --size_x 18 \
|
| 41 |
+
# --size_y 18 \
|
| 42 |
+
# --size_z 18 \
|
| 43 |
+
# --out ligand_1_docked.pdbqt \
|
| 44 |
+
# --log ligand_1_docked.log
|
| 45 |
+
|
| 46 |
+
#vina_split --input ligand_1_docked.pdbqt --ligand ligand_1_mode
|
| 47 |
+
|
| 48 |
+
#Now we need to turn back pdbqt file to pdb file for ligand
|
| 49 |
+
#run this command to do that obabel ligand_1_mode1.pdbqt -O ligand_1_mode1.pdb -p 7.4
|
| 50 |
+
#now we need to add remaining hydrogens in it using pymol. pymol command is h_add ligand_1_mode1.pdb
|
| 51 |
+
|
| 52 |
+
#Now we need to make sure the residue name is correct like the name in the original ligand and then
|
| 53 |
+
#we need to rename the atoms names to give this ligand to antechamber like C1, N1, .. like the way '4_ligands_corrected_1.pdb' formated
|
| 54 |
+
#Now this ligand is ready to be used by antechambe to generate force field parameters
|
ambermdflow/docking_utils.py
ADDED
|
@@ -0,0 +1,639 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Docking Utilities for AmberMDFlow
|
| 4 |
+
|
| 5 |
+
This module contains all the Python functions needed for the docking workflow:
|
| 6 |
+
1. Compute ligand center
|
| 7 |
+
2. Prepare receptor (tleap + pdb4amber + meeko)
|
| 8 |
+
3. Prepare ligand (obabel + meeko)
|
| 9 |
+
4. Run Vina docking
|
| 10 |
+
5. Split docked poses (vina_split)
|
| 11 |
+
6. Convert poses to PDB (obabel)
|
| 12 |
+
7. Sanitize docked poses for use in MD workflow
|
| 13 |
+
|
| 14 |
+
Usage:
|
| 15 |
+
from docking_utils import (
|
| 16 |
+
compute_ligand_center,
|
| 17 |
+
prepare_receptor,
|
| 18 |
+
prepare_ligand,
|
| 19 |
+
run_vina_docking,
|
| 20 |
+
split_docked_poses,
|
| 21 |
+
convert_pdbqt_to_pdb,
|
| 22 |
+
sanitize_docked_pose
|
| 23 |
+
)
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
import subprocess
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
import logging
|
| 29 |
+
|
| 30 |
+
logger = logging.getLogger(__name__)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def compute_ligand_center(pdb_path: str) -> tuple:
|
| 34 |
+
"""
|
| 35 |
+
Compute the geometric center of all atoms in a ligand PDB file.
|
| 36 |
+
|
| 37 |
+
Args:
|
| 38 |
+
pdb_path: Path to the ligand PDB file
|
| 39 |
+
|
| 40 |
+
Returns:
|
| 41 |
+
Tuple of (x, y, z) center coordinates
|
| 42 |
+
"""
|
| 43 |
+
try:
|
| 44 |
+
import MDAnalysis as mda
|
| 45 |
+
import numpy as np
|
| 46 |
+
except ImportError as e:
|
| 47 |
+
raise RuntimeError(
|
| 48 |
+
"MDAnalysis and NumPy are required. Install with: "
|
| 49 |
+
"conda install -c conda-forge mdanalysis numpy"
|
| 50 |
+
) from e
|
| 51 |
+
|
| 52 |
+
pdb_path = Path(pdb_path)
|
| 53 |
+
if not pdb_path.exists():
|
| 54 |
+
raise FileNotFoundError(f"Ligand file not found: {pdb_path}")
|
| 55 |
+
|
| 56 |
+
u = mda.Universe(str(pdb_path))
|
| 57 |
+
if u.atoms.n_atoms == 0:
|
| 58 |
+
raise ValueError(f"No atoms found in ligand file {pdb_path}")
|
| 59 |
+
|
| 60 |
+
coords = u.atoms.positions.astype(float)
|
| 61 |
+
center = coords.mean(axis=0)
|
| 62 |
+
|
| 63 |
+
logger.info(f"Ligand center for {pdb_path.name}: ({center[0]:.3f}, {center[1]:.3f}, {center[2]:.3f})")
|
| 64 |
+
return float(center[0]), float(center[1]), float(center[2])
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def prepare_receptor(protein_pdb: str, output_dir: str) -> tuple:
|
| 68 |
+
"""
|
| 69 |
+
Prepare receptor for docking:
|
| 70 |
+
1. Run tleap to add hydrogens
|
| 71 |
+
2. Run pdb4amber to fix element names
|
| 72 |
+
3. Run mk_prepare_receptor.py to create PDBQT
|
| 73 |
+
|
| 74 |
+
Args:
|
| 75 |
+
protein_pdb: Path to protein PDB file (typically 1_protein_no_hydrogens.pdb)
|
| 76 |
+
output_dir: Directory to store output files
|
| 77 |
+
|
| 78 |
+
Returns:
|
| 79 |
+
Tuple of (receptor_fixed_pdb_path, receptor_pdbqt_path)
|
| 80 |
+
"""
|
| 81 |
+
protein_pdb = Path(protein_pdb).resolve()
|
| 82 |
+
output_dir = Path(output_dir)
|
| 83 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 84 |
+
|
| 85 |
+
if not protein_pdb.exists():
|
| 86 |
+
raise FileNotFoundError(f"Protein PDB not found: {protein_pdb}")
|
| 87 |
+
|
| 88 |
+
# Step 1: tleap - add hydrogens
|
| 89 |
+
tleap_in = output_dir / "prepare_receptor.in"
|
| 90 |
+
receptor_pdb = output_dir / "receptor.pdb"
|
| 91 |
+
|
| 92 |
+
if not receptor_pdb.exists():
|
| 93 |
+
logger.info("Step 1: Running tleap to add hydrogens to protein...")
|
| 94 |
+
with open(tleap_in, "w") as f:
|
| 95 |
+
f.write("source leaprc.protein.ff14SB\n")
|
| 96 |
+
f.write(f"protein = loadpdb {protein_pdb}\n")
|
| 97 |
+
f.write("savepdb protein receptor.pdb\n")
|
| 98 |
+
f.write("quit\n")
|
| 99 |
+
|
| 100 |
+
result = subprocess.run(
|
| 101 |
+
["tleap", "-f", tleap_in.name],
|
| 102 |
+
cwd=output_dir,
|
| 103 |
+
capture_output=True,
|
| 104 |
+
text=True,
|
| 105 |
+
)
|
| 106 |
+
if result.returncode != 0 or not receptor_pdb.exists():
|
| 107 |
+
raise RuntimeError(
|
| 108 |
+
f"tleap failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
| 109 |
+
)
|
| 110 |
+
logger.info(f" Created: {receptor_pdb}")
|
| 111 |
+
|
| 112 |
+
# Step 2: pdb4amber - fix element names
|
| 113 |
+
receptor_fixed = output_dir / "receptor_fixed.pdb"
|
| 114 |
+
|
| 115 |
+
if not receptor_fixed.exists():
|
| 116 |
+
logger.info("Step 2: Running pdb4amber to add element names...")
|
| 117 |
+
result = subprocess.run(
|
| 118 |
+
["pdb4amber", "-i", str(receptor_pdb), "-o", str(receptor_fixed)],
|
| 119 |
+
capture_output=True,
|
| 120 |
+
text=True,
|
| 121 |
+
)
|
| 122 |
+
if result.returncode != 0 or not receptor_fixed.exists():
|
| 123 |
+
raise RuntimeError(
|
| 124 |
+
f"pdb4amber failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
| 125 |
+
)
|
| 126 |
+
logger.info(f" Created: {receptor_fixed}")
|
| 127 |
+
|
| 128 |
+
# Step 3: Meeko receptor preparation
|
| 129 |
+
receptor_pdbqt = output_dir / "receptor.pdbqt"
|
| 130 |
+
|
| 131 |
+
if not receptor_pdbqt.exists():
|
| 132 |
+
logger.info("Step 3: Running mk_prepare_receptor.py to create PDBQT...")
|
| 133 |
+
result = subprocess.run(
|
| 134 |
+
["mk_prepare_receptor.py", "-i", str(receptor_fixed), "-o", "receptor", "-p"],
|
| 135 |
+
cwd=output_dir,
|
| 136 |
+
capture_output=True,
|
| 137 |
+
text=True,
|
| 138 |
+
)
|
| 139 |
+
if result.returncode != 0 or not receptor_pdbqt.exists():
|
| 140 |
+
raise RuntimeError(
|
| 141 |
+
f"mk_prepare_receptor.py failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
| 142 |
+
)
|
| 143 |
+
logger.info(f" Created: {receptor_pdbqt}")
|
| 144 |
+
|
| 145 |
+
return str(receptor_fixed), str(receptor_pdbqt)
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def prepare_ligand(ligand_pdb: str, output_dir: str, ligand_index: int = 1) -> str:
|
| 149 |
+
"""
|
| 150 |
+
Prepare ligand for docking:
|
| 151 |
+
1. Convert PDB to SDF using obabel
|
| 152 |
+
2. Convert SDF to PDBQT using mk_prepare_ligand.py
|
| 153 |
+
|
| 154 |
+
Args:
|
| 155 |
+
ligand_pdb: Path to ligand PDB file
|
| 156 |
+
output_dir: Directory to store output files
|
| 157 |
+
ligand_index: Index number for naming output files
|
| 158 |
+
|
| 159 |
+
Returns:
|
| 160 |
+
Path to ligand PDBQT file
|
| 161 |
+
"""
|
| 162 |
+
ligand_pdb = Path(ligand_pdb)
|
| 163 |
+
output_dir = Path(output_dir)
|
| 164 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 165 |
+
|
| 166 |
+
if not ligand_pdb.exists():
|
| 167 |
+
raise FileNotFoundError(f"Ligand PDB not found: {ligand_pdb}")
|
| 168 |
+
|
| 169 |
+
# Step 1: obabel PDB -> SDF
|
| 170 |
+
sdf_path = output_dir / f"ligand_{ligand_index}.sdf"
|
| 171 |
+
|
| 172 |
+
logger.info(f"Step 1: Converting ligand {ligand_index} PDB to SDF...")
|
| 173 |
+
result = subprocess.run(
|
| 174 |
+
["obabel", "-i", "pdb", str(ligand_pdb), "-o", "sdf", "-O", str(sdf_path)],
|
| 175 |
+
capture_output=True,
|
| 176 |
+
text=True,
|
| 177 |
+
)
|
| 178 |
+
if result.returncode != 0 or not sdf_path.exists():
|
| 179 |
+
raise RuntimeError(
|
| 180 |
+
f"obabel failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
| 181 |
+
)
|
| 182 |
+
logger.info(f" Created: {sdf_path}")
|
| 183 |
+
|
| 184 |
+
# Step 2: Meeko ligand preparation -> PDBQT
|
| 185 |
+
pdbqt_path = output_dir / f"ligand_{ligand_index}.pdbqt"
|
| 186 |
+
|
| 187 |
+
logger.info(f"Step 2: Converting ligand {ligand_index} SDF to PDBQT...")
|
| 188 |
+
result = subprocess.run(
|
| 189 |
+
["mk_prepare_ligand.py", "-i", str(sdf_path), "-o", str(pdbqt_path)],
|
| 190 |
+
capture_output=True,
|
| 191 |
+
text=True,
|
| 192 |
+
)
|
| 193 |
+
if result.returncode != 0 or not pdbqt_path.exists():
|
| 194 |
+
raise RuntimeError(
|
| 195 |
+
f"mk_prepare_ligand.py failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
| 196 |
+
)
|
| 197 |
+
logger.info(f" Created: {pdbqt_path}")
|
| 198 |
+
|
| 199 |
+
return str(pdbqt_path)
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
def run_vina_docking(
|
| 203 |
+
receptor_pdbqt: str,
|
| 204 |
+
ligand_pdbqt: str,
|
| 205 |
+
center_x: float,
|
| 206 |
+
center_y: float,
|
| 207 |
+
center_z: float,
|
| 208 |
+
size_x: float = 18.0,
|
| 209 |
+
size_y: float = 18.0,
|
| 210 |
+
size_z: float = 18.0,
|
| 211 |
+
output_dir: str = None,
|
| 212 |
+
ligand_index: int = 1,
|
| 213 |
+
exhaustiveness: int = 8,
|
| 214 |
+
num_modes: int = 9,
|
| 215 |
+
) -> tuple:
|
| 216 |
+
"""
|
| 217 |
+
Run AutoDock Vina docking.
|
| 218 |
+
|
| 219 |
+
Args:
|
| 220 |
+
receptor_pdbqt: Path to receptor PDBQT file
|
| 221 |
+
ligand_pdbqt: Path to ligand PDBQT file
|
| 222 |
+
center_x, center_y, center_z: Box center coordinates (Angstroms)
|
| 223 |
+
size_x, size_y, size_z: Box dimensions (Angstroms)
|
| 224 |
+
output_dir: Directory for output files (default: same as ligand)
|
| 225 |
+
ligand_index: Index for naming output files
|
| 226 |
+
exhaustiveness: Search exhaustiveness (default: 8)
|
| 227 |
+
num_modes: Maximum number of binding modes (default: 9)
|
| 228 |
+
|
| 229 |
+
Returns:
|
| 230 |
+
Tuple of (docked_pdbqt_path, log_file_path)
|
| 231 |
+
"""
|
| 232 |
+
ligand_pdbqt = Path(ligand_pdbqt)
|
| 233 |
+
output_dir = Path(output_dir) if output_dir else ligand_pdbqt.parent
|
| 234 |
+
|
| 235 |
+
docked_pdbqt = output_dir / f"ligand_{ligand_index}_docked.pdbqt"
|
| 236 |
+
log_file = output_dir / f"ligand_{ligand_index}_docked.log"
|
| 237 |
+
|
| 238 |
+
logger.info(f"Running Vina docking for ligand {ligand_index}...")
|
| 239 |
+
logger.info(f" Center: ({center_x:.3f}, {center_y:.3f}, {center_z:.3f})")
|
| 240 |
+
logger.info(f" Size: ({size_x:.1f}, {size_y:.1f}, {size_z:.1f})")
|
| 241 |
+
|
| 242 |
+
cmd = [
|
| 243 |
+
"vina",
|
| 244 |
+
"--receptor", str(receptor_pdbqt),
|
| 245 |
+
"--ligand", str(ligand_pdbqt),
|
| 246 |
+
"--center_x", str(center_x),
|
| 247 |
+
"--center_y", str(center_y),
|
| 248 |
+
"--center_z", str(center_z),
|
| 249 |
+
"--size_x", str(size_x),
|
| 250 |
+
"--size_y", str(size_y),
|
| 251 |
+
"--size_z", str(size_z),
|
| 252 |
+
"--out", str(docked_pdbqt),
|
| 253 |
+
"--log", str(log_file),
|
| 254 |
+
"--exhaustiveness", str(exhaustiveness),
|
| 255 |
+
"--num_modes", str(num_modes),
|
| 256 |
+
]
|
| 257 |
+
|
| 258 |
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 259 |
+
|
| 260 |
+
if result.returncode != 0 or not docked_pdbqt.exists():
|
| 261 |
+
raise RuntimeError(
|
| 262 |
+
f"Vina docking failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
logger.info(f" Created: {docked_pdbqt}")
|
| 266 |
+
logger.info(f" Log: {log_file}")
|
| 267 |
+
|
| 268 |
+
return str(docked_pdbqt), str(log_file)
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def parse_vina_log(log_path: str) -> list:
|
| 272 |
+
"""
|
| 273 |
+
Parse Vina log file to extract binding energies for each mode.
|
| 274 |
+
|
| 275 |
+
Args:
|
| 276 |
+
log_path: Path to Vina log file
|
| 277 |
+
|
| 278 |
+
Returns:
|
| 279 |
+
List of dicts with 'mode', 'affinity', 'rmsd_lb', 'rmsd_ub' for each pose
|
| 280 |
+
"""
|
| 281 |
+
log_path = Path(log_path)
|
| 282 |
+
if not log_path.exists():
|
| 283 |
+
return []
|
| 284 |
+
|
| 285 |
+
energies = []
|
| 286 |
+
in_results = False
|
| 287 |
+
|
| 288 |
+
with open(log_path, "r") as f:
|
| 289 |
+
for line in f:
|
| 290 |
+
line = line.strip()
|
| 291 |
+
if "-----+------------+----------+----------" in line:
|
| 292 |
+
in_results = True
|
| 293 |
+
continue
|
| 294 |
+
if in_results and line and line[0].isdigit():
|
| 295 |
+
parts = line.split()
|
| 296 |
+
if len(parts) >= 4:
|
| 297 |
+
try:
|
| 298 |
+
energies.append({
|
| 299 |
+
'mode': int(parts[0]),
|
| 300 |
+
'affinity': float(parts[1]),
|
| 301 |
+
'rmsd_lb': float(parts[2]),
|
| 302 |
+
'rmsd_ub': float(parts[3]),
|
| 303 |
+
})
|
| 304 |
+
except (ValueError, IndexError):
|
| 305 |
+
continue
|
| 306 |
+
elif in_results and not line:
|
| 307 |
+
break
|
| 308 |
+
|
| 309 |
+
return energies
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
def split_docked_poses(docked_pdbqt: str, output_prefix: str = None) -> list:
|
| 313 |
+
"""
|
| 314 |
+
Split docked PDBQT into individual pose files using vina_split.
|
| 315 |
+
|
| 316 |
+
Args:
|
| 317 |
+
docked_pdbqt: Path to docked PDBQT file with multiple poses
|
| 318 |
+
output_prefix: Prefix for output files (default: derived from input)
|
| 319 |
+
|
| 320 |
+
Returns:
|
| 321 |
+
List of paths to individual pose PDBQT files
|
| 322 |
+
"""
|
| 323 |
+
docked_pdbqt = Path(docked_pdbqt)
|
| 324 |
+
if not docked_pdbqt.exists():
|
| 325 |
+
raise FileNotFoundError(f"Docked PDBQT not found: {docked_pdbqt}")
|
| 326 |
+
|
| 327 |
+
output_dir = docked_pdbqt.parent
|
| 328 |
+
if output_prefix is None:
|
| 329 |
+
output_prefix = docked_pdbqt.stem.replace("_docked", "_mode")
|
| 330 |
+
|
| 331 |
+
logger.info(f"Splitting docked poses from {docked_pdbqt.name}...")
|
| 332 |
+
|
| 333 |
+
result = subprocess.run(
|
| 334 |
+
["vina_split", "--input", str(docked_pdbqt), "--ligand", output_prefix],
|
| 335 |
+
cwd=output_dir,
|
| 336 |
+
capture_output=True,
|
| 337 |
+
text=True,
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
if result.returncode != 0:
|
| 341 |
+
raise RuntimeError(
|
| 342 |
+
f"vina_split failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
# Find all generated mode files
|
| 346 |
+
pose_files = sorted(output_dir.glob(f"{output_prefix}*.pdbqt"))
|
| 347 |
+
logger.info(f" Split into {len(pose_files)} pose files")
|
| 348 |
+
|
| 349 |
+
return [str(f) for f in pose_files]
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def convert_pdbqt_to_pdb(pdbqt_path: str, ph: float = 7.4) -> str:
|
| 353 |
+
"""
|
| 354 |
+
Convert PDBQT file to PDB using obabel.
|
| 355 |
+
|
| 356 |
+
Args:
|
| 357 |
+
pdbqt_path: Path to PDBQT file
|
| 358 |
+
ph: pH for protonation (default: 7.4)
|
| 359 |
+
|
| 360 |
+
Returns:
|
| 361 |
+
Path to output PDB file
|
| 362 |
+
"""
|
| 363 |
+
pdbqt_path = Path(pdbqt_path)
|
| 364 |
+
if not pdbqt_path.exists():
|
| 365 |
+
raise FileNotFoundError(f"PDBQT file not found: {pdbqt_path}")
|
| 366 |
+
|
| 367 |
+
pdb_path = pdbqt_path.with_suffix(".pdb")
|
| 368 |
+
|
| 369 |
+
logger.info(f"Converting {pdbqt_path.name} to PDB...")
|
| 370 |
+
|
| 371 |
+
result = subprocess.run(
|
| 372 |
+
["obabel", str(pdbqt_path), "-O", str(pdb_path), "-p", str(ph)],
|
| 373 |
+
capture_output=True,
|
| 374 |
+
text=True,
|
| 375 |
+
)
|
| 376 |
+
|
| 377 |
+
if result.returncode != 0 or not pdb_path.exists():
|
| 378 |
+
raise RuntimeError(
|
| 379 |
+
f"obabel conversion failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
logger.info(f" Created: {pdb_path}")
|
| 383 |
+
return str(pdb_path)
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
def sanitize_docked_pose(original_ligand: str, pose_pdb: str) -> str:
|
| 387 |
+
"""
|
| 388 |
+
Sanitize a docked pose PDB to match the original ligand format:
|
| 389 |
+
- Restore residue name, chain ID, and residue number from original
|
| 390 |
+
- Convert ATOM to HETATM
|
| 391 |
+
- Rename atoms to match original format (C1, N1, etc.)
|
| 392 |
+
- Remove CONECT/MASTER records
|
| 393 |
+
|
| 394 |
+
Args:
|
| 395 |
+
original_ligand: Path to original ligand PDB file
|
| 396 |
+
pose_pdb: Path to docked pose PDB file
|
| 397 |
+
|
| 398 |
+
Returns:
|
| 399 |
+
Path to sanitized pose PDB (same as pose_pdb, modified in place)
|
| 400 |
+
"""
|
| 401 |
+
original_ligand = Path(original_ligand)
|
| 402 |
+
pose_pdb = Path(pose_pdb)
|
| 403 |
+
|
| 404 |
+
if not original_ligand.exists():
|
| 405 |
+
raise FileNotFoundError(f"Original ligand not found: {original_ligand}")
|
| 406 |
+
if not pose_pdb.exists():
|
| 407 |
+
raise FileNotFoundError(f"Pose PDB not found: {pose_pdb}")
|
| 408 |
+
|
| 409 |
+
# Extract residue info from original ligand
|
| 410 |
+
resname = "LIG"
|
| 411 |
+
chain = "X"
|
| 412 |
+
resnum = 1
|
| 413 |
+
|
| 414 |
+
with open(original_ligand, "r") as f:
|
| 415 |
+
for line in f:
|
| 416 |
+
if line.startswith(("ATOM", "HETATM")):
|
| 417 |
+
resname = line[17:20].strip() or "LIG"
|
| 418 |
+
chain = line[21] if len(line) > 21 and line[21].strip() else "X"
|
| 419 |
+
try:
|
| 420 |
+
resnum = int(line[22:26].strip())
|
| 421 |
+
except ValueError:
|
| 422 |
+
resnum = 1
|
| 423 |
+
break
|
| 424 |
+
|
| 425 |
+
logger.info(f"Sanitizing pose with resname={resname}, chain={chain}, resnum={resnum}")
|
| 426 |
+
|
| 427 |
+
# Process pose PDB
|
| 428 |
+
new_lines = []
|
| 429 |
+
atom_counter = 0
|
| 430 |
+
element_counts = {}
|
| 431 |
+
|
| 432 |
+
with open(pose_pdb, "r") as f:
|
| 433 |
+
for line in f:
|
| 434 |
+
if line.startswith(("CONECT", "MASTER")):
|
| 435 |
+
continue
|
| 436 |
+
if line.startswith(("ATOM", "HETATM")):
|
| 437 |
+
atom_counter += 1
|
| 438 |
+
|
| 439 |
+
# Extract element from line or atom name
|
| 440 |
+
element = line[76:78].strip() if len(line) > 77 else ""
|
| 441 |
+
if not element:
|
| 442 |
+
# Try to get from atom name
|
| 443 |
+
atom_name = line[12:16].strip()
|
| 444 |
+
element = ''.join(c for c in atom_name if c.isalpha())[:2]
|
| 445 |
+
if len(element) > 1:
|
| 446 |
+
element = element[0].upper() + element[1].lower()
|
| 447 |
+
|
| 448 |
+
if not element:
|
| 449 |
+
element = "C" # Default fallback
|
| 450 |
+
|
| 451 |
+
# Generate new atom name (C1, C2, N1, etc.)
|
| 452 |
+
element_counts[element] = element_counts.get(element, 0) + 1
|
| 453 |
+
new_atom_name = f"{element}{element_counts[element]}"
|
| 454 |
+
new_atom_name = f"{new_atom_name:<4}" # Left-justified, 4 chars
|
| 455 |
+
|
| 456 |
+
# Build new line as HETATM
|
| 457 |
+
new_line = (
|
| 458 |
+
f"HETATM{atom_counter:5d} {new_atom_name}"
|
| 459 |
+
f"{resname:>3s} {chain}{resnum:4d} "
|
| 460 |
+
f"{line[30:54]}" # Coordinates
|
| 461 |
+
f"{line[54:66] if len(line) > 54 else ' 1.00 0.00'}" # Occupancy, B-factor
|
| 462 |
+
f" {element:>2s}\n"
|
| 463 |
+
)
|
| 464 |
+
new_lines.append(new_line)
|
| 465 |
+
elif line.startswith("END"):
|
| 466 |
+
new_lines.append("END\n")
|
| 467 |
+
|
| 468 |
+
# Write sanitized file
|
| 469 |
+
with open(pose_pdb, "w") as f:
|
| 470 |
+
f.writelines(new_lines)
|
| 471 |
+
|
| 472 |
+
logger.info(f" Sanitized: {pose_pdb}")
|
| 473 |
+
return str(pose_pdb)
|
| 474 |
+
|
| 475 |
+
|
| 476 |
+
def run_full_docking_workflow(
|
| 477 |
+
protein_pdb: str,
|
| 478 |
+
ligand_pdbs: list,
|
| 479 |
+
output_dir: str,
|
| 480 |
+
box_configs: dict = None,
|
| 481 |
+
) -> dict:
|
| 482 |
+
"""
|
| 483 |
+
Run the complete docking workflow for multiple ligands.
|
| 484 |
+
|
| 485 |
+
Args:
|
| 486 |
+
protein_pdb: Path to protein PDB file (1_protein_no_hydrogens.pdb)
|
| 487 |
+
ligand_pdbs: List of paths to ligand PDB files
|
| 488 |
+
output_dir: Base output directory for docking results
|
| 489 |
+
box_configs: Optional dict of {ligand_index: {'center': (x,y,z), 'size': (sx,sy,sz)}}
|
| 490 |
+
|
| 491 |
+
Returns:
|
| 492 |
+
Dict with results for each ligand including poses and energies
|
| 493 |
+
"""
|
| 494 |
+
output_dir = Path(output_dir)
|
| 495 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 496 |
+
box_configs = box_configs or {}
|
| 497 |
+
|
| 498 |
+
results = {
|
| 499 |
+
'success': True,
|
| 500 |
+
'ligands': [],
|
| 501 |
+
'warnings': [],
|
| 502 |
+
'errors': [],
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
# Step 1: Prepare receptor (only once for all ligands)
|
| 506 |
+
logger.info("=" * 60)
|
| 507 |
+
logger.info("STEP 1: Preparing receptor for docking")
|
| 508 |
+
logger.info("=" * 60)
|
| 509 |
+
|
| 510 |
+
try:
|
| 511 |
+
receptor_fixed, receptor_pdbqt = prepare_receptor(protein_pdb, str(output_dir))
|
| 512 |
+
except Exception as e:
|
| 513 |
+
results['success'] = False
|
| 514 |
+
results['errors'].append(f"Receptor preparation failed: {str(e)}")
|
| 515 |
+
return results
|
| 516 |
+
|
| 517 |
+
# Step 2: Process each ligand
|
| 518 |
+
for idx, ligand_pdb in enumerate(ligand_pdbs, start=1):
|
| 519 |
+
ligand_pdb = Path(ligand_pdb)
|
| 520 |
+
logger.info("")
|
| 521 |
+
logger.info("=" * 60)
|
| 522 |
+
logger.info(f"STEP 2.{idx}: Processing ligand {idx}: {ligand_pdb.name}")
|
| 523 |
+
logger.info("=" * 60)
|
| 524 |
+
|
| 525 |
+
lig_dir = output_dir / f"ligand_{idx}"
|
| 526 |
+
lig_dir.mkdir(parents=True, exist_ok=True)
|
| 527 |
+
|
| 528 |
+
ligand_result = {
|
| 529 |
+
'index': idx,
|
| 530 |
+
'original_file': str(ligand_pdb),
|
| 531 |
+
'poses': [],
|
| 532 |
+
'energies': [],
|
| 533 |
+
'success': True,
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
try:
|
| 537 |
+
# Copy original ligand for reference
|
| 538 |
+
original_copy = lig_dir / "original_ligand.pdb"
|
| 539 |
+
if not original_copy.exists():
|
| 540 |
+
original_copy.write_text(ligand_pdb.read_text())
|
| 541 |
+
|
| 542 |
+
# Prepare ligand PDBQT
|
| 543 |
+
ligand_pdbqt = prepare_ligand(str(ligand_pdb), str(lig_dir), idx)
|
| 544 |
+
|
| 545 |
+
# Get box configuration
|
| 546 |
+
cfg = box_configs.get(idx, {})
|
| 547 |
+
center = cfg.get('center')
|
| 548 |
+
size = cfg.get('size', (18.0, 18.0, 18.0))
|
| 549 |
+
|
| 550 |
+
if center is None:
|
| 551 |
+
# Compute center from ligand
|
| 552 |
+
cx, cy, cz = compute_ligand_center(str(ligand_pdb))
|
| 553 |
+
else:
|
| 554 |
+
cx, cy, cz = center
|
| 555 |
+
|
| 556 |
+
sx, sy, sz = size
|
| 557 |
+
|
| 558 |
+
# Run Vina docking
|
| 559 |
+
docked_pdbqt, log_file = run_vina_docking(
|
| 560 |
+
receptor_pdbqt, ligand_pdbqt,
|
| 561 |
+
cx, cy, cz, sx, sy, sz,
|
| 562 |
+
str(lig_dir), idx
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
# Parse binding energies
|
| 566 |
+
energies = parse_vina_log(log_file)
|
| 567 |
+
ligand_result['energies'] = energies
|
| 568 |
+
|
| 569 |
+
# Split poses
|
| 570 |
+
pose_pdbqts = split_docked_poses(docked_pdbqt)
|
| 571 |
+
|
| 572 |
+
# Convert each pose to PDB and sanitize
|
| 573 |
+
for pose_pdbqt in pose_pdbqts:
|
| 574 |
+
pose_pdb = convert_pdbqt_to_pdb(pose_pdbqt)
|
| 575 |
+
sanitize_docked_pose(str(original_copy), pose_pdb)
|
| 576 |
+
ligand_result['poses'].append(pose_pdb)
|
| 577 |
+
|
| 578 |
+
except Exception as e:
|
| 579 |
+
ligand_result['success'] = False
|
| 580 |
+
ligand_result['error'] = str(e)
|
| 581 |
+
results['errors'].append(f"Ligand {idx}: {str(e)}")
|
| 582 |
+
logger.error(f"Error processing ligand {idx}: {e}")
|
| 583 |
+
|
| 584 |
+
results['ligands'].append(ligand_result)
|
| 585 |
+
|
| 586 |
+
# Check overall success
|
| 587 |
+
results['success'] = all(lig['success'] for lig in results['ligands'])
|
| 588 |
+
|
| 589 |
+
logger.info("")
|
| 590 |
+
logger.info("=" * 60)
|
| 591 |
+
logger.info("DOCKING WORKFLOW COMPLETE")
|
| 592 |
+
logger.info("=" * 60)
|
| 593 |
+
|
| 594 |
+
return results
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
# Example usage / CLI interface
|
| 598 |
+
if __name__ == "__main__":
|
| 599 |
+
import argparse
|
| 600 |
+
|
| 601 |
+
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
| 602 |
+
|
| 603 |
+
parser = argparse.ArgumentParser(description="Run AutoDock Vina docking workflow")
|
| 604 |
+
parser.add_argument("--protein", required=True, help="Path to protein PDB file")
|
| 605 |
+
parser.add_argument("--ligands", nargs="+", required=True, help="Paths to ligand PDB files")
|
| 606 |
+
parser.add_argument("--output", required=True, help="Output directory")
|
| 607 |
+
parser.add_argument("--center", nargs=3, type=float, help="Box center (x y z)")
|
| 608 |
+
parser.add_argument("--size", nargs=3, type=float, default=[18, 18, 18], help="Box size (x y z)")
|
| 609 |
+
|
| 610 |
+
args = parser.parse_args()
|
| 611 |
+
|
| 612 |
+
box_configs = {}
|
| 613 |
+
if args.center:
|
| 614 |
+
for i in range(1, len(args.ligands) + 1):
|
| 615 |
+
box_configs[i] = {
|
| 616 |
+
'center': tuple(args.center),
|
| 617 |
+
'size': tuple(args.size),
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
results = run_full_docking_workflow(
|
| 621 |
+
args.protein,
|
| 622 |
+
args.ligands,
|
| 623 |
+
args.output,
|
| 624 |
+
box_configs
|
| 625 |
+
)
|
| 626 |
+
|
| 627 |
+
print("\n" + "=" * 60)
|
| 628 |
+
print("RESULTS SUMMARY")
|
| 629 |
+
print("=" * 60)
|
| 630 |
+
print(f"Overall success: {results['success']}")
|
| 631 |
+
for lig in results['ligands']:
|
| 632 |
+
print(f"\nLigand {lig['index']}:")
|
| 633 |
+
print(f" Success: {lig['success']}")
|
| 634 |
+
if lig['success']:
|
| 635 |
+
print(f" Poses generated: {len(lig['poses'])}")
|
| 636 |
+
if lig['energies']:
|
| 637 |
+
print(f" Best binding energy: {lig['energies'][0]['affinity']} kcal/mol")
|
| 638 |
+
else:
|
| 639 |
+
print(f" Error: {lig.get('error', 'Unknown')}")
|
ambermdflow/html/index.html
ADDED
|
@@ -0,0 +1,1145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>MD Simulation Pipeline</title>
|
| 7 |
+
<link rel="stylesheet" href="../css/styles.css">
|
| 8 |
+
<link rel="stylesheet" href="../css/plumed.css">
|
| 9 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
| 10 |
+
<!-- THREE.js (needed for docking box visualization) - using r95 to match NGL's bundled version -->
|
| 11 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r95/three.min.js"></script>
|
| 12 |
+
<!-- NGL 3D Molecular Viewer -->
|
| 13 |
+
<script src="https://unpkg.com/ngl@2.0.0-dev.35/dist/ngl.js"></script>
|
| 14 |
+
</head>
|
| 15 |
+
<body>
|
| 16 |
+
<div class="container">
|
| 17 |
+
<!-- Header -->
|
| 18 |
+
<header class="header">
|
| 19 |
+
<div class="header-content">
|
| 20 |
+
<h1><i class="fas fa-atom"></i> MD Simulation Pipeline</h1>
|
| 21 |
+
<p>Molecular Dynamics Simulation Setup and File Generation</p>
|
| 22 |
+
</div>
|
| 23 |
+
</header>
|
| 24 |
+
|
| 25 |
+
<!-- Navigation Tabs -->
|
| 26 |
+
<nav class="tab-navigation">
|
| 27 |
+
<button class="tab-button active" data-tab="protein-loading">
|
| 28 |
+
<i class="fas fa-upload"></i> Protein Loading
|
| 29 |
+
</button>
|
| 30 |
+
<button class="tab-button" data-tab="fill-missing">
|
| 31 |
+
<i class="fas fa-puzzle-piece"></i> Fill Missing Residues
|
| 32 |
+
</button>
|
| 33 |
+
<button class="tab-button" data-tab="structure-prep">
|
| 34 |
+
<i class="fas fa-tools"></i> Structure Preparation
|
| 35 |
+
</button>
|
| 36 |
+
<button class="tab-button" data-tab="simulation-params">
|
| 37 |
+
<i class="fas fa-cogs"></i> Simulation Parameters
|
| 38 |
+
</button>
|
| 39 |
+
<button class="tab-button" data-tab="simulation-steps">
|
| 40 |
+
<i class="fas fa-list-ol"></i> Simulation Steps
|
| 41 |
+
</button>
|
| 42 |
+
<button class="tab-button" data-tab="file-generation">
|
| 43 |
+
<i class="fas fa-file-download"></i> Generate Files
|
| 44 |
+
</button>
|
| 45 |
+
<button class="tab-button" data-tab="plumed">
|
| 46 |
+
<i class="fas fa-chart-line"></i> PLUMED
|
| 47 |
+
</button>
|
| 48 |
+
</nav>
|
| 49 |
+
|
| 50 |
+
<!-- Main Content -->
|
| 51 |
+
<main class="main-content">
|
| 52 |
+
<!-- Protein Loading Tab -->
|
| 53 |
+
<div id="protein-loading" class="tab-content active">
|
| 54 |
+
<div class="card">
|
| 55 |
+
<h2><i class="fas fa-dna"></i> Protein Structure Input</h2>
|
| 56 |
+
|
| 57 |
+
<div class="input-methods">
|
| 58 |
+
<div class="method-option">
|
| 59 |
+
<h3><i class="fas fa-file-upload"></i> Upload PDB File</h3>
|
| 60 |
+
<div class="file-upload-area" id="file-upload-area">
|
| 61 |
+
<i class="fas fa-cloud-upload-alt"></i>
|
| 62 |
+
<p>Drag and drop your PDB file here or click to browse</p>
|
| 63 |
+
<input type="file" id="pdb-file" accept=".pdb,.ent" style="display: none;">
|
| 64 |
+
<button type="button" class="btn btn-secondary" id="choose-file-btn">
|
| 65 |
+
Choose File
|
| 66 |
+
</button>
|
| 67 |
+
</div>
|
| 68 |
+
<div id="file-info" class="file-info" style="display: none;">
|
| 69 |
+
<p><strong>Selected file:</strong> <span id="file-name"></span></p>
|
| 70 |
+
<p><strong>Size:</strong> <span id="file-size"></span></p>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
<div class="divider">
|
| 75 |
+
<span>OR</span>
|
| 76 |
+
</div>
|
| 77 |
+
|
| 78 |
+
<div class="method-option">
|
| 79 |
+
<h3><i class="fas fa-database"></i> Fetch from PDB</h3>
|
| 80 |
+
<div class="pdb-fetch">
|
| 81 |
+
<div class="input-group">
|
| 82 |
+
<label for="pdb-id">PDB ID:</label>
|
| 83 |
+
<input type="text" id="pdb-id" placeholder="e.g., 1CRN, 1HTM" maxlength="4">
|
| 84 |
+
<button type="button" class="btn btn-primary" id="fetch-pdb">
|
| 85 |
+
<i class="fas fa-download"></i> Fetch
|
| 86 |
+
</button>
|
| 87 |
+
</div>
|
| 88 |
+
<div id="pdb-status" class="status-message"></div>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
<div class="protein-preview" id="protein-preview" style="display: none;">
|
| 94 |
+
<h3><i class="fas fa-eye"></i> Protein Preview</h3>
|
| 95 |
+
<div class="preview-content">
|
| 96 |
+
<div class="protein-info">
|
| 97 |
+
<p><strong>Structure ID:</strong> <span id="structure-id"></span></p>
|
| 98 |
+
<p><strong>Number of atoms (Protein):</strong> <span id="atom-count"></span></p>
|
| 99 |
+
<p><strong>Chains:</strong> <span id="chain-info"></span></p>
|
| 100 |
+
<p><strong>Residues:</strong> <span id="residue-count"></span></p>
|
| 101 |
+
<p><strong>Water molecules:</strong> <span id="water-count"></span></p>
|
| 102 |
+
<p><strong>Ions:</strong> <span id="ion-count"></span></p>
|
| 103 |
+
<p><strong>Ligands:</strong> <span id="ligand-info"></span></p>
|
| 104 |
+
<p><strong>HETATM entries:</strong> <span id="hetatm-count"></span></p>
|
| 105 |
+
</div>
|
| 106 |
+
<div class="protein-visualization">
|
| 107 |
+
<div id="molecule-viewer" class="molecule-viewer">
|
| 108 |
+
<!-- 3D visualization will be added here -->
|
| 109 |
+
<div id="ngl-viewer" style="width: 100%; height: 100%; min-height: 300px;"></div>
|
| 110 |
+
<div id="viewer-controls" class="viewer-controls" style="display: none;">
|
| 111 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.resetView()">
|
| 112 |
+
<i class="fas fa-home"></i> Reset View
|
| 113 |
+
</button>
|
| 114 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleRepresentation()">
|
| 115 |
+
<i class="fas fa-eye"></i> <span id="style-text">Mixed View</span>
|
| 116 |
+
</button>
|
| 117 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleSpin()">
|
| 118 |
+
<i class="fas fa-sync"></i> Spin
|
| 119 |
+
</button>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
|
| 128 |
+
<!-- Fill Missing Residues Tab -->
|
| 129 |
+
<div id="fill-missing" class="tab-content">
|
| 130 |
+
<div class="card">
|
| 131 |
+
<h2><i class="fas fa-puzzle-piece"></i> Fill Missing Residues</h2>
|
| 132 |
+
<p class="card-description">
|
| 133 |
+
Detect missing residues in the experimental structure using RCSB annotations and complete them
|
| 134 |
+
with ESMFold. You can choose which chains to include in the completion.
|
| 135 |
+
</p>
|
| 136 |
+
<p class="form-text text-muted" style="margin-top: 6px; font-size: 0.9em;">
|
| 137 |
+
<i class="fas fa-book"></i> If you use this workflow in your research, please cite: <a href="https://esmatlas.com/about" target="_blank" rel="noopener noreferrer">ESM Atlas</a>
|
| 138 |
+
</p>
|
| 139 |
+
|
| 140 |
+
<div class="prep-sections">
|
| 141 |
+
<div class="prep-section">
|
| 142 |
+
<h3><i class="fas fa-search"></i> Detect Missing Residues</h3>
|
| 143 |
+
<button class="btn btn-primary" id="detect-missing-residues">
|
| 144 |
+
<i class="fas fa-search"></i> Analyze Missing Residues
|
| 145 |
+
</button>
|
| 146 |
+
<div id="missing-status" class="status-message" style="margin-top: 10px;"></div>
|
| 147 |
+
</div>
|
| 148 |
+
|
| 149 |
+
<div class="prep-section" id="missing-chains-section" style="display: none;">
|
| 150 |
+
<h3><i class="fas fa-link"></i> Select Chains for Completion</h3>
|
| 151 |
+
<p class="option-description">
|
| 152 |
+
Chains listed below have missing residues. Select which chains you want to rebuild with ESMFold.
|
| 153 |
+
</p>
|
| 154 |
+
<div id="missing-chains-list" class="multi-checkbox-group">
|
| 155 |
+
<!-- Missing chains checkboxes will be rendered here -->
|
| 156 |
+
</div>
|
| 157 |
+
<small class="form-help">
|
| 158 |
+
At least one chain must be selected to build a completed structure.
|
| 159 |
+
</small>
|
| 160 |
+
</div>
|
| 161 |
+
</div>
|
| 162 |
+
|
| 163 |
+
<div class="prep-section prep-section-fullwidth" id="trim-residues-section" style="display: none;">
|
| 164 |
+
<h3><i class="fas fa-cut"></i> Trim Residues from Edges (Optional)</h3>
|
| 165 |
+
<p class="option-description">
|
| 166 |
+
Optionally trim residues from the N-terminal and/or C-terminal edges of selected chains.
|
| 167 |
+
This only removes residues from the edges, not from loops in between.
|
| 168 |
+
</p>
|
| 169 |
+
<div class="trim-info-box" id="trim-info-box-content">
|
| 170 |
+
<i class="fas fa-info-circle"></i>
|
| 171 |
+
<strong>Note:</strong> Only missing residues at the <strong>N-terminal edge</strong> (beginning of sequence) and <strong>C-terminal edge</strong> (end of sequence) can be trimmed.
|
| 172 |
+
Missing residues in internal loops (discontinuities in the middle of the sequence) cannot be trimmed using this tool and will be filled by ESMFold.
|
| 173 |
+
</div>
|
| 174 |
+
<div id="trim-residues-list" class="trim-residues-container">
|
| 175 |
+
<!-- Trim controls for each chain will be rendered here -->
|
| 176 |
+
</div>
|
| 177 |
+
<button class="btn btn-secondary" id="apply-trim" style="margin-top: 15px;">
|
| 178 |
+
<i class="fas fa-check"></i> Apply Trimming
|
| 179 |
+
</button>
|
| 180 |
+
<div id="trim-status" class="status-message" style="margin-top: 10px; display: none;"></div>
|
| 181 |
+
</div>
|
| 182 |
+
|
| 183 |
+
<!-- Chain Minimization Option -->
|
| 184 |
+
<div class="prep-section" id="chain-minimization-section" style="display: none; margin-top: 20px; padding: 15px; background: #f8f9fa; border-radius: 5px; border: 1px solid #dee2e6;">
|
| 185 |
+
<h3><i class="fas fa-compress-arrows-alt"></i> Energy Minimization (Optional)</h3>
|
| 186 |
+
<div class="form-check" style="display: flex; align-items: center; gap: 10px; margin-top: 10px;">
|
| 187 |
+
<input class="form-check-input" type="checkbox" id="minimize-chains-checkbox" style="width: 20px; height: 20px; cursor: pointer;">
|
| 188 |
+
<label class="form-check-label" for="minimize-chains-checkbox" style="cursor: pointer; font-weight: 500; flex: 1;">
|
| 189 |
+
<strong>Energy minimize ESMFold-generated chains</strong>
|
| 190 |
+
</label>
|
| 191 |
+
</div>
|
| 192 |
+
<small class="form-text text-muted" style="display: block; margin-top: 8px; margin-left: 30px;">
|
| 193 |
+
<i class="fas fa-info-circle"></i> Recommended: Minimization resolve structural clashes
|
| 194 |
+
</small>
|
| 195 |
+
<div id="minimization-chains-list" style="display: none; margin-top: 15px; margin-left: 30px;">
|
| 196 |
+
<strong>Select chains to minimize:</strong>
|
| 197 |
+
<div id="minimization-chains-checkboxes" class="multi-checkbox-group" style="margin-top: 10px;">
|
| 198 |
+
<!-- Chain checkboxes will be populated here -->
|
| 199 |
+
</div>
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
|
| 203 |
+
<div class="prep-actions">
|
| 204 |
+
<button class="btn btn-primary" id="build-complete-structure" disabled>
|
| 205 |
+
<i class="fas fa-magic"></i> Build Completed Structure
|
| 206 |
+
</button>
|
| 207 |
+
<button class="btn btn-secondary" id="preview-completed-structure" disabled>
|
| 208 |
+
<i class="fas fa-eye"></i> Preview Completed Structure
|
| 209 |
+
</button>
|
| 210 |
+
<button class="btn btn-secondary" id="preview-superimposed-structure" disabled>
|
| 211 |
+
<i class="fas fa-layer-group"></i> View Superimposed Structures
|
| 212 |
+
</button>
|
| 213 |
+
<button class="btn btn-info" id="download-completed-structure" disabled>
|
| 214 |
+
<i class="fas fa-download"></i> Download Completed PDB
|
| 215 |
+
</button>
|
| 216 |
+
</div>
|
| 217 |
+
|
| 218 |
+
<div class="prep-status" id="missing-summary" style="display: none;">
|
| 219 |
+
<h3><i class="fas fa-info-circle"></i> Missing Residues Summary</h3>
|
| 220 |
+
<div class="status-content" id="missing-summary-content">
|
| 221 |
+
<!-- Missing residues summary will be displayed here -->
|
| 222 |
+
</div>
|
| 223 |
+
</div>
|
| 224 |
+
|
| 225 |
+
<div class="prep-actions" id="sequence-viewer-actions" style="display: none; margin-top: 1rem;">
|
| 226 |
+
<button class="btn btn-secondary" id="view-protein-sequences">
|
| 227 |
+
<i class="fas fa-dna"></i> View Protein Sequences
|
| 228 |
+
</button>
|
| 229 |
+
</div>
|
| 230 |
+
|
| 231 |
+
<div class="prepared-structure-preview" id="sequence-viewer-section" style="display: none;">
|
| 232 |
+
<h3><i class="fas fa-dna"></i> Protein Sequence Viewer</h3>
|
| 233 |
+
<p class="card-description" style="margin-bottom: 15px;">
|
| 234 |
+
View protein sequences for all chains. Chain colors match the structure visualization. Missing residues are shown in grey.
|
| 235 |
+
</p>
|
| 236 |
+
<div id="sequence-viewer-content" class="sequence-viewer-container">
|
| 237 |
+
<!-- Sequence viewer will be rendered here -->
|
| 238 |
+
</div>
|
| 239 |
+
</div>
|
| 240 |
+
|
| 241 |
+
<div class="prepared-structure-preview" id="completed-structure-preview" style="display: none;">
|
| 242 |
+
<h3><i class="fas fa-eye"></i> Structure Comparison Preview</h3>
|
| 243 |
+
<p class="card-description" style="margin-bottom: 15px;">
|
| 244 |
+
Compare the completed structure (right) with the original crystal structure (left) to see the added missing residues.
|
| 245 |
+
</p>
|
| 246 |
+
<div class="preview-content" style="display: flex; justify-content: center; width: 100%; padding: 0;">
|
| 247 |
+
<div class="structure-comparison-container" style="display: flex; gap: 20px; flex-direction: row; width: 100%; max-width: 1400px; align-items: flex-start; justify-content: center;">
|
| 248 |
+
<!-- Original Structure Viewer -->
|
| 249 |
+
<div class="comparison-viewer" style="flex: 0 1 48%; min-width: 450px; max-width: 48%;">
|
| 250 |
+
<h4 style="text-align: center; margin-bottom: 10px; font-size: 14px;">
|
| 251 |
+
<i class="fas fa-dna"></i> Original Crystal Structure
|
| 252 |
+
</h4>
|
| 253 |
+
<div id="original-molecule-viewer" class="molecule-viewer" style="border: 2px solid #007bff; border-radius: 5px; width: 100%; height: 500px; position: relative;">
|
| 254 |
+
<div id="original-ngl-viewer" style="width: 100%; height: 100%; position: absolute; top: 0; left: 0;"></div>
|
| 255 |
+
<div id="original-viewer-controls" class="viewer-controls" style="display: none; justify-content: center; position: relative; z-index: 10;">
|
| 256 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.resetOriginalView()">
|
| 257 |
+
<i class="fas fa-home"></i> Reset
|
| 258 |
+
</button>
|
| 259 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleOriginalRepresentation()">
|
| 260 |
+
<i class="fas fa-eye"></i> <span id="original-style-text">Mixed</span>
|
| 261 |
+
</button>
|
| 262 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleOriginalSpin()">
|
| 263 |
+
<i class="fas fa-sync"></i> Spin
|
| 264 |
+
</button>
|
| 265 |
+
</div>
|
| 266 |
+
</div>
|
| 267 |
+
</div>
|
| 268 |
+
|
| 269 |
+
<!-- Completed Structure Viewer -->
|
| 270 |
+
<div class="comparison-viewer" style="flex: 0 1 48%; min-width: 450px; max-width: 48%;">
|
| 271 |
+
<h4 style="text-align: center; margin-bottom: 10px; font-size: 14px;">
|
| 272 |
+
<i class="fas fa-puzzle-piece"></i> Completed Structure
|
| 273 |
+
</h4>
|
| 274 |
+
<div id="completed-molecule-viewer" class="molecule-viewer" style="border: 2px solid #28a745; border-radius: 5px; width: 100%; height: 500px; position: relative;">
|
| 275 |
+
<div id="completed-ngl-viewer" style="width: 100%; height: 100%; position: absolute; top: 0; left: 0;"></div>
|
| 276 |
+
<div id="completed-viewer-controls" class="viewer-controls" style="display: none; justify-content: center; position: relative; z-index: 10;">
|
| 277 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.resetCompletedView()">
|
| 278 |
+
<i class="fas fa-home"></i> Reset
|
| 279 |
+
</button>
|
| 280 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleCompletedRepresentation()">
|
| 281 |
+
<i class="fas fa-eye"></i> <span id="completed-style-text">Mixed</span>
|
| 282 |
+
</button>
|
| 283 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleCompletedSpin()">
|
| 284 |
+
<i class="fas fa-sync"></i> Spin
|
| 285 |
+
</button>
|
| 286 |
+
</div>
|
| 287 |
+
</div>
|
| 288 |
+
</div>
|
| 289 |
+
</div>
|
| 290 |
+
</div>
|
| 291 |
+
</div>
|
| 292 |
+
|
| 293 |
+
<div class="prepared-structure-preview" id="superimposed-structure-preview" style="display: none;">
|
| 294 |
+
<h3><i class="fas fa-layer-group"></i> Superimposed Structure View</h3>
|
| 295 |
+
<p class="card-description" style="margin-bottom: 15px;">
|
| 296 |
+
View both the original crystal structure (original colors) and completed structure (different chain colors) superimposed in the same viewer to see which residues were filled.
|
| 297 |
+
</p>
|
| 298 |
+
<div class="preview-content" style="display: flex; justify-content: center; width: 100%; padding: 0;">
|
| 299 |
+
<div class="structure-comparison-container" style="width: 100%; max-width: 1400px;">
|
| 300 |
+
<!-- Superimposed Structure Viewer -->
|
| 301 |
+
<div class="comparison-viewer" style="width: 100%;">
|
| 302 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
| 303 |
+
<div>
|
| 304 |
+
<span style="color: #007bff; font-weight: 600;"><i class="fas fa-dna"></i> Original Crystal Structure</span>
|
| 305 |
+
<span style="margin: 0 10px;">|</span>
|
| 306 |
+
<span style="color: #28a745; font-weight: 600;"><i class="fas fa-puzzle-piece"></i> Completed Structure</span>
|
| 307 |
+
</div>
|
| 308 |
+
</div>
|
| 309 |
+
<div id="superimposed-molecule-viewer" class="molecule-viewer" style="border: 2px solid #007bff; border-radius: 5px; width: 100%; height: 600px; position: relative;">
|
| 310 |
+
<div id="superimposed-ngl-viewer" style="width: 100%; height: 100%; position: absolute; top: 0; left: 0;"></div>
|
| 311 |
+
<div id="superimposed-viewer-controls" class="viewer-controls" style="display: none; gap: 10px;">
|
| 312 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.resetSuperimposedView()">
|
| 313 |
+
<i class="fas fa-home"></i> Reset
|
| 314 |
+
</button>
|
| 315 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleSuperimposedRepresentation()">
|
| 316 |
+
<i class="fas fa-eye"></i> <span id="superimposed-style-text">Cartoon</span>
|
| 317 |
+
</button>
|
| 318 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleSuperimposedSpin()">
|
| 319 |
+
<i class="fas fa-sync"></i> Spin
|
| 320 |
+
</button>
|
| 321 |
+
</div>
|
| 322 |
+
</div>
|
| 323 |
+
</div>
|
| 324 |
+
</div>
|
| 325 |
+
</div>
|
| 326 |
+
</div>
|
| 327 |
+
</div>
|
| 328 |
+
</div>
|
| 329 |
+
|
| 330 |
+
<!-- Structure Preparation Tab -->
|
| 331 |
+
<div id="structure-prep" class="tab-content">
|
| 332 |
+
<div class="card">
|
| 333 |
+
<h2><i class="fas fa-tools"></i> Structure Preparation for AMBER</h2>
|
| 334 |
+
<p class="card-description">Prepare the protein structure for AMBER force field generation by cleaning and modifying the PDB file.</p>
|
| 335 |
+
|
| 336 |
+
<div class="prep-sections">
|
| 337 |
+
<div class="prep-section">
|
| 338 |
+
<h3><i class="fas fa-trash"></i> Remove Components</h3>
|
| 339 |
+
<div class="prep-options">
|
| 340 |
+
<div class="prep-option">
|
| 341 |
+
<label class="checkbox-container">
|
| 342 |
+
<input type="checkbox" id="remove-water" checked disabled>
|
| 343 |
+
<span class="checkmark"></span>
|
| 344 |
+
Remove water molecules
|
| 345 |
+
</label>
|
| 346 |
+
<p class="option-description">Remove all water molecules (HOH, WAT, TIP3, etc.) from the structure</p>
|
| 347 |
+
</div>
|
| 348 |
+
|
| 349 |
+
<div class="prep-option">
|
| 350 |
+
<label class="checkbox-container">
|
| 351 |
+
<input type="checkbox" id="remove-ions" checked disabled>
|
| 352 |
+
<span class="checkmark"></span>
|
| 353 |
+
Remove ions
|
| 354 |
+
</label>
|
| 355 |
+
<p class="option-description">Remove all ions (Na+, Cl-, K+, Mg2+, etc.) from the structure</p>
|
| 356 |
+
</div>
|
| 357 |
+
|
| 358 |
+
<div class="prep-option">
|
| 359 |
+
<label class="checkbox-container">
|
| 360 |
+
<input type="checkbox" id="remove-hydrogens" checked disabled>
|
| 361 |
+
<span class="checkmark"></span>
|
| 362 |
+
Remove hydrogen atoms
|
| 363 |
+
</label>
|
| 364 |
+
<p class="option-description">Remove all hydrogen atoms from the protein structure</p>
|
| 365 |
+
</div>
|
| 366 |
+
</div>
|
| 367 |
+
</div>
|
| 368 |
+
|
| 369 |
+
<div class="prep-section">
|
| 370 |
+
<h3><i class="fas fa-plus-circle"></i> Add Capping Groups and Select Protein Chains</h3>
|
| 371 |
+
<div class="prep-options">
|
| 372 |
+
<div class="prep-option">
|
| 373 |
+
<label class="checkbox-container">
|
| 374 |
+
<input type="checkbox" id="add-nme" checked>
|
| 375 |
+
<span class="checkmark"></span>
|
| 376 |
+
Add NME group (C-terminal)
|
| 377 |
+
</label>
|
| 378 |
+
<p class="option-description">Add N-methyl amide (NME) group to C-terminal residues</p>
|
| 379 |
+
</div>
|
| 380 |
+
|
| 381 |
+
<div class="prep-option">
|
| 382 |
+
<label class="checkbox-container">
|
| 383 |
+
<input type="checkbox" id="add-ace" checked>
|
| 384 |
+
<span class="checkmark"></span>
|
| 385 |
+
Add ACE group (N-terminal)
|
| 386 |
+
</label>
|
| 387 |
+
<p class="option-description">Add acetyl (ACE) group to N-terminal residues</p>
|
| 388 |
+
</div>
|
| 389 |
+
|
| 390 |
+
<div class="form-group">
|
| 391 |
+
<label>Preserve Chains for FF Generation:</label>
|
| 392 |
+
<div id="chain-selection" class="multi-checkbox-group">
|
| 393 |
+
<!-- Chain checkboxes will be rendered here -->
|
| 394 |
+
</div>
|
| 395 |
+
<small class="form-help">Select one or more protein chains to include in preparation</small>
|
| 396 |
+
</div>
|
| 397 |
+
</div>
|
| 398 |
+
</div>
|
| 399 |
+
|
| 400 |
+
<div class="prep-section">
|
| 401 |
+
<h3><i class="fas fa-pills"></i> Ligand Handling</h3>
|
| 402 |
+
<div class="prep-options">
|
| 403 |
+
<div class="prep-option">
|
| 404 |
+
<label class="checkbox-container">
|
| 405 |
+
<input type="checkbox" id="preserve-ligands">
|
| 406 |
+
<span class="checkmark"></span>
|
| 407 |
+
Preserve ligands
|
| 408 |
+
</label>
|
| 409 |
+
<p class="option-description">Keep ligands in the structure and append them at the end</p>
|
| 410 |
+
</div>
|
| 411 |
+
|
| 412 |
+
<div class="prep-option">
|
| 413 |
+
<div class="checkbox-with-button">
|
| 414 |
+
<label class="checkbox-container">
|
| 415 |
+
<input type="checkbox" id="separate-ligands">
|
| 416 |
+
<span class="checkmark"></span>
|
| 417 |
+
Create separate ligand file
|
| 418 |
+
</label>
|
| 419 |
+
<button class="btn btn-sm btn-outline-primary" id="download-ligand" disabled>
|
| 420 |
+
<i class="fas fa-download"></i>
|
| 421 |
+
</button>
|
| 422 |
+
</div>
|
| 423 |
+
<p class="option-description">Extract ligands to a separate PDB file for individual processing</p>
|
| 424 |
+
</div>
|
| 425 |
+
|
| 426 |
+
<div class="form-group">
|
| 427 |
+
<label>Select Ligands to Preserve</label>
|
| 428 |
+
<div id="ligand-selection" class="multi-checkbox-group">
|
| 429 |
+
<!-- Ligand checkboxes will be rendered here -->
|
| 430 |
+
</div>
|
| 431 |
+
<small class="form-help">Tick ligands to include. Unselected ligands will be ignored.</small>
|
| 432 |
+
</div>
|
| 433 |
+
</div>
|
| 434 |
+
</div>
|
| 435 |
+
|
| 436 |
+
</div>
|
| 437 |
+
|
| 438 |
+
<div class="prep-actions">
|
| 439 |
+
<button class="btn btn-primary" id="prepare-structure">
|
| 440 |
+
<i class="fas fa-magic"></i> Prepare Structure
|
| 441 |
+
</button>
|
| 442 |
+
<button class="btn btn-secondary" id="preview-prepared">
|
| 443 |
+
<i class="fas fa-eye"></i> Preview Prepared Structure
|
| 444 |
+
</button>
|
| 445 |
+
<button class="btn btn-info" id="download-prepared">
|
| 446 |
+
<i class="fas fa-download"></i> Download Prepared PDB
|
| 447 |
+
</button>
|
| 448 |
+
</div>
|
| 449 |
+
|
| 450 |
+
<div class="prep-status" id="prep-status" style="display: none;">
|
| 451 |
+
<h3><i class="fas fa-info-circle"></i> Preparation Status</h3>
|
| 452 |
+
<div class="status-content" id="prep-status-content">
|
| 453 |
+
<!-- Status information will be displayed here -->
|
| 454 |
+
</div>
|
| 455 |
+
</div>
|
| 456 |
+
|
| 457 |
+
<div class="prepared-structure-preview" id="prepared-structure-preview" style="display: none;">
|
| 458 |
+
<h3><i class="fas fa-eye"></i> Prepared Structure Preview</h3>
|
| 459 |
+
<div class="preview-content">
|
| 460 |
+
<div class="structure-info">
|
| 461 |
+
<p><strong>Original atoms</strong> <span style="font-size:0.9em; color:#6c757d;">(protein without H, before capping):</span> <span id="original-atoms"></span></p>
|
| 462 |
+
<p><strong>Prepared atoms</strong> <span style="font-size:0.9em; color:#6c757d;">(protein without H, after capping):</span> <span id="prepared-atoms"></span></p>
|
| 463 |
+
<p><strong>Removed components:</strong> <span id="removed-components"></span></p>
|
| 464 |
+
<p><strong>Added capping groups:</strong> <span id="added-capping"></span></p>
|
| 465 |
+
<p><strong>Ligands preserved:</strong> <span id="preserved-ligands"></span></p>
|
| 466 |
+
</div>
|
| 467 |
+
<div class="structure-visualization">
|
| 468 |
+
<div id="prepared-molecule-viewer" class="molecule-viewer">
|
| 469 |
+
<div id="prepared-ngl-viewer" style="width: 100%; height: 100%; min-height: 300px;"></div>
|
| 470 |
+
<div id="prepared-viewer-controls" class="viewer-controls" style="display: none;">
|
| 471 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.resetPreparedView()">
|
| 472 |
+
<i class="fas fa-home"></i> Reset View
|
| 473 |
+
</button>
|
| 474 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.togglePreparedRepresentation()">
|
| 475 |
+
<i class="fas fa-eye"></i> <span id="prepared-style-text">Mixed View</span>
|
| 476 |
+
</button>
|
| 477 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.togglePreparedSpin()">
|
| 478 |
+
<i class="fas fa-sync"></i> Spin
|
| 479 |
+
</button>
|
| 480 |
+
</div>
|
| 481 |
+
</div>
|
| 482 |
+
</div>
|
| 483 |
+
</div>
|
| 484 |
+
</div>
|
| 485 |
+
|
| 486 |
+
<!-- Docking Section (visible only when ligands are preserved and present) -->
|
| 487 |
+
<div class="card plumed-section-card" id="docking-section" style="display: none; margin-top: 20px;">
|
| 488 |
+
<h2 class="plumed-toggle-header" id="docking-toggle-header">
|
| 489 |
+
<i class="fas fa-vial"></i> Ligand Docking
|
| 490 |
+
<i class="fas fa-chevron-down toggle-icon" id="docking-toggle-icon"></i>
|
| 491 |
+
</h2>
|
| 492 |
+
<p class="section-description">
|
| 493 |
+
Configure docking for preserved ligands using AutoDock Vina and Meeko. Select which ligands to dock,
|
| 494 |
+
define the Vina bounding box with live visualization, then run docking and choose poses.
|
| 495 |
+
</p>
|
| 496 |
+
<div class="custom-plumed-section" id="docking-content-section">
|
| 497 |
+
<!-- Ligand Selection -->
|
| 498 |
+
<div class="form-group" style="margin-bottom: 20px;">
|
| 499 |
+
<label><i class="fas fa-pills"></i> Select Ligands to Dock</label>
|
| 500 |
+
<p class="option-description" style="margin-bottom: 10px;">
|
| 501 |
+
Choose which preserved ligands should be included in docking calculations.
|
| 502 |
+
</p>
|
| 503 |
+
<div id="docking-ligand-selection" class="multi-checkbox-group">
|
| 504 |
+
<!-- Ligand checkboxes will be rendered here -->
|
| 505 |
+
</div>
|
| 506 |
+
</div>
|
| 507 |
+
|
| 508 |
+
<!-- Collapsible: Docking Setup (Visualization + Box Dimensions) -->
|
| 509 |
+
<div class="docking-setup-collapsible" style="margin-top: 10px; border: 1px solid #dee2e6; border-radius: 8px; overflow: hidden;">
|
| 510 |
+
<div class="docking-setup-header" id="docking-setup-toggle" style="background: linear-gradient(135deg, #6f42c1 0%, #8e5dd4 100%); color: white; padding: 12px 15px; cursor: pointer; display: flex; justify-content: space-between; align-items: center;">
|
| 511 |
+
<span><i class="fas fa-cube"></i> Docking Search Space Setup</span>
|
| 512 |
+
<i class="fas fa-chevron-up" id="docking-setup-toggle-icon" style="transition: transform 0.3s ease;"></i>
|
| 513 |
+
</div>
|
| 514 |
+
<div class="docking-setup-content" id="docking-setup-content" style="padding: 15px; background: white;">
|
| 515 |
+
<!-- Docking Search Space Visualization -->
|
| 516 |
+
<div class="prepared-structure-preview" id="docking-structure-preview">
|
| 517 |
+
<h4 style="margin-top: 0;"><i class="fas fa-eye"></i> Search Space Visualization</h4>
|
| 518 |
+
<p class="card-description" style="margin-bottom: 10px;">
|
| 519 |
+
The protein–ligand system is shown below. For each selected ligand, a bounding box (10×10×10 Å by default)
|
| 520 |
+
represents the Vina search space. Adjust box dimensions below to update the visualization live.
|
| 521 |
+
</p>
|
| 522 |
+
<!-- NGL Viewer - Matching section 2 aspect ratio -->
|
| 523 |
+
<div id="docking-ngl-viewer" class="molecule-viewer" style="border: 2px solid #6f42c1; border-radius: 5px; width: 100%; max-width: 700px; height: 500px; position: relative; margin: 0 auto;">
|
| 524 |
+
<!-- Docking NGL visualization will be added here -->
|
| 525 |
+
</div>
|
| 526 |
+
</div>
|
| 527 |
+
|
| 528 |
+
<!-- Box Dimensions - Below Visualization -->
|
| 529 |
+
<div id="docking-box-controls" style="margin-top: 15px; background: #f8f9fa; padding: 15px; border-radius: 5px; border: 1px solid #dee2e6;">
|
| 530 |
+
<h5 style="margin-top: 0; margin-bottom: 15px;"><i class="fas fa-sliders-h"></i> Box Dimensions for Selected Ligands</h5>
|
| 531 |
+
<div id="docking-setup-list" style="display: flex; flex-wrap: wrap; gap: 15px;">
|
| 532 |
+
<!-- Per-ligand box controls will be rendered here in a horizontal layout -->
|
| 533 |
+
</div>
|
| 534 |
+
</div>
|
| 535 |
+
|
| 536 |
+
<div class="prep-actions" style="margin-top: 15px;">
|
| 537 |
+
<button class="btn btn-primary" id="run-docking">
|
| 538 |
+
<i class="fas fa-vial"></i> Run Docking with Above Settings
|
| 539 |
+
</button>
|
| 540 |
+
</div>
|
| 541 |
+
</div>
|
| 542 |
+
</div>
|
| 543 |
+
<div id="docking-status" class="status-message" style="display: none; margin-top: 10px;"></div>
|
| 544 |
+
|
| 545 |
+
<div id="docking-poses-container" style="display: none; margin-top: 20px;">
|
| 546 |
+
<h4><i class="fas fa-project-diagram"></i> Visualize Binding Poses</h4>
|
| 547 |
+
<p class="option-description">
|
| 548 |
+
Browse through docked poses for each ligand. Use the navigation arrows to view different binding modes.
|
| 549 |
+
Select your preferred pose for the simulation.
|
| 550 |
+
</p>
|
| 551 |
+
|
| 552 |
+
<!-- Ligand selector tabs -->
|
| 553 |
+
<div id="docking-ligand-tabs" class="docking-ligand-tabs">
|
| 554 |
+
<!-- Ligand tabs will be rendered here -->
|
| 555 |
+
</div>
|
| 556 |
+
|
| 557 |
+
<!-- 3D Viewer with pose navigation -->
|
| 558 |
+
<div class="docking-poses-viewer-wrapper">
|
| 559 |
+
<div id="docking-poses-viewer" class="docking-poses-viewer"></div>
|
| 560 |
+
|
| 561 |
+
<!-- Pose navigation controls overlay -->
|
| 562 |
+
<div class="pose-nav-controls">
|
| 563 |
+
<button type="button" class="pose-nav-btn pose-nav-prev" id="pose-prev-btn" title="Previous Pose">
|
| 564 |
+
<i class="fas fa-chevron-left"></i>
|
| 565 |
+
</button>
|
| 566 |
+
<div class="pose-info-display">
|
| 567 |
+
<div class="pose-mode-label" id="pose-mode-label">Original Ligand</div>
|
| 568 |
+
<div class="pose-energy-label" id="pose-energy-label"></div>
|
| 569 |
+
<div class="pose-color-legend">
|
| 570 |
+
<span><span class="legend-dot original"></span> Original (green)</span>
|
| 571 |
+
<span><span class="legend-dot docked"></span> Docked (coral)</span>
|
| 572 |
+
</div>
|
| 573 |
+
</div>
|
| 574 |
+
<button type="button" class="pose-nav-btn pose-nav-next" id="pose-next-btn" title="Next Pose">
|
| 575 |
+
<i class="fas fa-chevron-right"></i>
|
| 576 |
+
</button>
|
| 577 |
+
</div>
|
| 578 |
+
</div>
|
| 579 |
+
|
| 580 |
+
<!-- Pose selection summary -->
|
| 581 |
+
<div id="docking-poses-list" class="docking-poses-selection">
|
| 582 |
+
<!-- Radio buttons for final selection will be rendered here -->
|
| 583 |
+
</div>
|
| 584 |
+
|
| 585 |
+
<div class="prep-actions" style="margin-top: 15px;">
|
| 586 |
+
<button class="btn btn-success" id="apply-docking-poses">
|
| 587 |
+
<i class="fas fa-check"></i> Use Selected Pose
|
| 588 |
+
</button>
|
| 589 |
+
</div>
|
| 590 |
+
</div>
|
| 591 |
+
</div>
|
| 592 |
+
</div>
|
| 593 |
+
</div>
|
| 594 |
+
</div>
|
| 595 |
+
|
| 596 |
+
<!-- Simulation Parameters Tab -->
|
| 597 |
+
<div id="simulation-params" class="tab-content">
|
| 598 |
+
<div class="card">
|
| 599 |
+
<h2><i class="fas fa-sliders-h"></i> Simulation Parameters</h2>
|
| 600 |
+
|
| 601 |
+
<div class="params-grid">
|
| 602 |
+
<div class="param-section">
|
| 603 |
+
<h3><i class="fas fa-cube"></i> System Setup</h3>
|
| 604 |
+
<div class="form-group">
|
| 605 |
+
<label for="box-type">Box Type:</label>
|
| 606 |
+
<select id="box-type">
|
| 607 |
+
<option value="cuboid">Cuboid</option>
|
| 608 |
+
</select>
|
| 609 |
+
</div>
|
| 610 |
+
<div class="form-group">
|
| 611 |
+
<label for="box-size">
|
| 612 |
+
Distance (Å):
|
| 613 |
+
<i class="fas fa-info-circle"
|
| 614 |
+
style="color: #007bff; margin-left: 5px; cursor: help;"
|
| 615 |
+
data-toggle="tooltip"
|
| 616 |
+
data-placement="top"
|
| 617 |
+
data-html="true"
|
| 618 |
+
title="The minimum distance between any atom originally present in solute and the edge of the periodic box is given by the distance parameter.">
|
| 619 |
+
</i>
|
| 620 |
+
</label>
|
| 621 |
+
<input type="number" id="box-size" value="10" step="1" min="5">
|
| 622 |
+
</div>
|
| 623 |
+
</div>
|
| 624 |
+
|
| 625 |
+
<div class="param-section" id="ligand-forcefield-section" style="display: none;">
|
| 626 |
+
<h3><i class="fas fa-atom"></i> Ligand Force Field</h3>
|
| 627 |
+
<div class="form-group">
|
| 628 |
+
<label for="ligand-forcefield">Ligand Force Field:</label>
|
| 629 |
+
<select id="ligand-forcefield">
|
| 630 |
+
<option value="gaff2">gaff2</option>
|
| 631 |
+
<option value="gaff">gaff</option>
|
| 632 |
+
</select>
|
| 633 |
+
</div>
|
| 634 |
+
<div class="form-group">
|
| 635 |
+
<button type="button" class="btn btn-primary" onclick="mdPipeline.generateLigandFF(event)">
|
| 636 |
+
<i class="fas fa-cogs"></i> Generate FF for Ligand
|
| 637 |
+
</button>
|
| 638 |
+
</div>
|
| 639 |
+
</div>
|
| 640 |
+
|
| 641 |
+
<div class="param-section">
|
| 642 |
+
<h3><i class="fas fa-flask"></i> Force Field & Water Model</h3>
|
| 643 |
+
<div class="form-group">
|
| 644 |
+
<label for="force-field">Protein Force Field:</label>
|
| 645 |
+
<select id="force-field">
|
| 646 |
+
<option value="ff14SB">ff14SB</option>
|
| 647 |
+
<option value="ff19SB">ff19SB</option>
|
| 648 |
+
</select>
|
| 649 |
+
</div>
|
| 650 |
+
<div class="form-group">
|
| 651 |
+
<label for="water-model">Water Model:</label>
|
| 652 |
+
<select id="water-model">
|
| 653 |
+
<option value="tip3p">TIP3P</option>
|
| 654 |
+
<option value="spce">SPCE</option>
|
| 655 |
+
</select>
|
| 656 |
+
</div>
|
| 657 |
+
<div class="form-group">
|
| 658 |
+
<label for="add-ions">Add Ions:</label>
|
| 659 |
+
<div class="ion-controls">
|
| 660 |
+
<select id="add-ions">
|
| 661 |
+
<option value="None">None</option>
|
| 662 |
+
<option value="Na+">Na+</option>
|
| 663 |
+
<option value="Cl-">Cl-</option>
|
| 664 |
+
</select>
|
| 665 |
+
<button type="button" class="btn btn-sm btn-outline-primary" onclick="mdPipeline.calculateNetCharge(event)">
|
| 666 |
+
<i class="fas fa-calculator"></i> Net Charge
|
| 667 |
+
</button>
|
| 668 |
+
</div>
|
| 669 |
+
</div>
|
| 670 |
+
</div>
|
| 671 |
+
|
| 672 |
+
<div class="param-section">
|
| 673 |
+
<h3><i class="fas fa-thermometer-half"></i> Temperature & Pressure</h3>
|
| 674 |
+
<div class="form-group">
|
| 675 |
+
<label for="temperature">Temperature (K):</label>
|
| 676 |
+
<input type="number" id="temperature" value="300" step="5" min="200" max="400">
|
| 677 |
+
</div>
|
| 678 |
+
<div class="form-group">
|
| 679 |
+
<label for="pressure">Pressure (atm):</label>
|
| 680 |
+
<input type="number" id="pressure" value="1.0" step="0.1" min="0.1">
|
| 681 |
+
</div>
|
| 682 |
+
<div class="form-group">
|
| 683 |
+
<label for="coupling-type">Thermostat:</label>
|
| 684 |
+
<select id="coupling-type">
|
| 685 |
+
<option value="langevin">Langevin</option>
|
| 686 |
+
</select>
|
| 687 |
+
</div>
|
| 688 |
+
</div>
|
| 689 |
+
|
| 690 |
+
<div class="param-section">
|
| 691 |
+
<h3><i class="fas fa-clock"></i> Integration Parameters</h3>
|
| 692 |
+
<div class="form-group">
|
| 693 |
+
<label for="timestep">Time Step (ps):</label>
|
| 694 |
+
<input type="number" id="timestep" value="0.002" step="0.001" min="0.001" max="0.005">
|
| 695 |
+
</div>
|
| 696 |
+
<div class="form-group">
|
| 697 |
+
<label for="cutoff">Cutoff Distance (Ang):</label>
|
| 698 |
+
<input type="number" id="cutoff" value="10.0" step="1" min="8" max="20">
|
| 699 |
+
</div>
|
| 700 |
+
<div class="form-group">
|
| 701 |
+
<label for="electrostatic">Electrostatic:</label>
|
| 702 |
+
<select id="electrostatic">
|
| 703 |
+
<option value="pme">PME</option>
|
| 704 |
+
</select>
|
| 705 |
+
</div>
|
| 706 |
+
</div>
|
| 707 |
+
|
| 708 |
+
</div>
|
| 709 |
+
</div>
|
| 710 |
+
</div>
|
| 711 |
+
|
| 712 |
+
<!-- Simulation Steps Tab -->
|
| 713 |
+
<div id="simulation-steps" class="tab-content">
|
| 714 |
+
<div class="card">
|
| 715 |
+
<h2><i class="fas fa-list-ol"></i> Simulation Steps Configuration</h2>
|
| 716 |
+
|
| 717 |
+
<div class="steps-container">
|
| 718 |
+
<div class="step-item">
|
| 719 |
+
<div class="step-header">
|
| 720 |
+
<h3><i class="fas fa-lock"></i> Restrained Minimization</h3>
|
| 721 |
+
<label class="switch">
|
| 722 |
+
<input type="checkbox" id="enable-restrained-min" checked disabled>
|
| 723 |
+
<span class="slider"></span>
|
| 724 |
+
</label>
|
| 725 |
+
</div>
|
| 726 |
+
<div class="step-content" id="restrained-min-content">
|
| 727 |
+
<div class="form-row">
|
| 728 |
+
<div class="form-group">
|
| 729 |
+
<label for="restrained-steps">Steps:</label>
|
| 730 |
+
<input type="number" id="restrained-steps" value="10000" step="100" min="100">
|
| 731 |
+
</div>
|
| 732 |
+
<div class="form-group">
|
| 733 |
+
<label for="restrained-force">Force Constant (kJ/mol/Ų):</label>
|
| 734 |
+
<input type="number" id="restrained-force" value="10" step="1" min="1">
|
| 735 |
+
</div>
|
| 736 |
+
</div>
|
| 737 |
+
</div>
|
| 738 |
+
</div>
|
| 739 |
+
|
| 740 |
+
<div class="step-item">
|
| 741 |
+
<div class="step-header">
|
| 742 |
+
<h3><i class="fas fa-compress"></i> Minimization</h3>
|
| 743 |
+
<label class="switch">
|
| 744 |
+
<input type="checkbox" id="enable-minimization" checked disabled>
|
| 745 |
+
<span class="slider"></span>
|
| 746 |
+
</label>
|
| 747 |
+
</div>
|
| 748 |
+
<div class="step-content" id="minimization-content">
|
| 749 |
+
<div class="form-row">
|
| 750 |
+
<div class="form-group">
|
| 751 |
+
<label for="min-steps">Steps:</label>
|
| 752 |
+
<input type="number" id="min-steps" value="20000" step="500" min="1000">
|
| 753 |
+
</div>
|
| 754 |
+
<div class="form-group">
|
| 755 |
+
<label for="min-algorithm">Algorithm:</label>
|
| 756 |
+
<select id="min-algorithm">
|
| 757 |
+
<option value="cg">Conjugate Gradient</option>
|
| 758 |
+
</select>
|
| 759 |
+
</div>
|
| 760 |
+
</div>
|
| 761 |
+
</div>
|
| 762 |
+
</div>
|
| 763 |
+
|
| 764 |
+
<div class="step-item">
|
| 765 |
+
<div class="step-header">
|
| 766 |
+
<h3><i class="fas fa-fire"></i> NPT Heating</h3>
|
| 767 |
+
<label class="switch">
|
| 768 |
+
<input type="checkbox" id="enable-nvt" checked disabled>
|
| 769 |
+
<span class="slider"></span>
|
| 770 |
+
</label>
|
| 771 |
+
</div>
|
| 772 |
+
<div class="step-content" id="nvt-content">
|
| 773 |
+
<div class="form-row">
|
| 774 |
+
<div class="form-group">
|
| 775 |
+
<label for="nvt-steps">Steps:</label>
|
| 776 |
+
<input type="number" id="nvt-steps" value="50000" step="5000" min="10000">
|
| 777 |
+
</div>
|
| 778 |
+
<div class="form-group">
|
| 779 |
+
<label for="nvt-temp">Target Temperature (K):</label>
|
| 780 |
+
<input type="number" id="nvt-temp" value="300" step="5" min="200">
|
| 781 |
+
</div>
|
| 782 |
+
</div>
|
| 783 |
+
</div>
|
| 784 |
+
</div>
|
| 785 |
+
|
| 786 |
+
<div class="step-item">
|
| 787 |
+
<div class="step-header">
|
| 788 |
+
<h3><i class="fas fa-compress-arrows-alt"></i> NPT Equilibration</h3>
|
| 789 |
+
<label class="switch">
|
| 790 |
+
<input type="checkbox" id="enable-npt" checked disabled>
|
| 791 |
+
<span class="slider"></span>
|
| 792 |
+
</label>
|
| 793 |
+
</div>
|
| 794 |
+
<div class="step-content" id="npt-content">
|
| 795 |
+
<div class="form-row">
|
| 796 |
+
<div class="form-group">
|
| 797 |
+
<label for="npt-steps">Steps:</label>
|
| 798 |
+
<input type="number" id="npt-steps" value="100000" step="10000" min="20000">
|
| 799 |
+
</div>
|
| 800 |
+
<div class="form-group">
|
| 801 |
+
<label for="npt-temp">Temperature (K):</label>
|
| 802 |
+
<input type="number" id="npt-temp" value="300" step="5" min="200">
|
| 803 |
+
</div>
|
| 804 |
+
<div class="form-group">
|
| 805 |
+
<label for="npt-pressure">Pressure (atm):</label>
|
| 806 |
+
<input type="number" id="npt-pressure" value="1.0" step="0.1" min="0.1">
|
| 807 |
+
</div>
|
| 808 |
+
</div>
|
| 809 |
+
</div>
|
| 810 |
+
</div>
|
| 811 |
+
|
| 812 |
+
<div class="step-item">
|
| 813 |
+
<div class="step-header">
|
| 814 |
+
<h3><i class="fas fa-play"></i> Production Run(NPT)</h3>
|
| 815 |
+
<label class="switch">
|
| 816 |
+
<input type="checkbox" id="enable-production" checked disabled>
|
| 817 |
+
<span class="slider"></span>
|
| 818 |
+
</label>
|
| 819 |
+
</div>
|
| 820 |
+
<div class="step-content" id="production-content">
|
| 821 |
+
<div class="form-row">
|
| 822 |
+
<div class="form-group">
|
| 823 |
+
<label for="prod-steps">Steps:</label>
|
| 824 |
+
<input type="number" id="prod-steps" value="1000000" step="100000" min="100000">
|
| 825 |
+
</div>
|
| 826 |
+
<div class="form-group">
|
| 827 |
+
<label for="prod-temp">Temperature (K):</label>
|
| 828 |
+
<input type="number" id="prod-temp" value="300" step="5" min="200">
|
| 829 |
+
</div>
|
| 830 |
+
<div class="form-group">
|
| 831 |
+
<label for="prod-pressure">Pressure (atm):</label>
|
| 832 |
+
<input type="number" id="prod-pressure" value="1.0" step="0.1" min="0.1">
|
| 833 |
+
</div>
|
| 834 |
+
</div>
|
| 835 |
+
</div>
|
| 836 |
+
</div>
|
| 837 |
+
</div>
|
| 838 |
+
</div>
|
| 839 |
+
</div>
|
| 840 |
+
|
| 841 |
+
<!-- File Generation Tab -->
|
| 842 |
+
<div id="file-generation" class="tab-content">
|
| 843 |
+
<!-- Guidance Note Card -->
|
| 844 |
+
<div class="plumed-citation-note">
|
| 845 |
+
<i class="fas fa-info-circle"></i>
|
| 846 |
+
<div class="citation-content">
|
| 847 |
+
<p>Click on <strong>Generate All Files</strong>. If you want to run <strong>biased simulations</strong> using PLUMED, proceed to the <strong>next section (PLUMED)</strong> to configure collective variables. Otherwise, download all files here and you're all set!</p>
|
| 848 |
+
</div>
|
| 849 |
+
</div>
|
| 850 |
+
|
| 851 |
+
<div class="card">
|
| 852 |
+
<h2><i class="fas fa-file-download"></i> Generate Simulation Files</h2>
|
| 853 |
+
|
| 854 |
+
<div class="generation-controls">
|
| 855 |
+
<button class="btn btn-primary" id="generate-files">
|
| 856 |
+
<i class="fas fa-magic"></i> Generate All Files
|
| 857 |
+
</button>
|
| 858 |
+
<button class="btn btn-secondary" id="preview-files">
|
| 859 |
+
<i class="fas fa-eye"></i> Preview Files
|
| 860 |
+
</button>
|
| 861 |
+
<button class="btn btn-info" id="preview-solvated">
|
| 862 |
+
<i class="fas fa-tint"></i> Preview Solvated Protein
|
| 863 |
+
</button>
|
| 864 |
+
<button class="btn btn-success" id="download-solvated">
|
| 865 |
+
<i class="fas fa-download"></i> Download Solvated Protein
|
| 866 |
+
</button>
|
| 867 |
+
</div>
|
| 868 |
+
|
| 869 |
+
<div class="files-preview" id="files-preview" style="display: none;">
|
| 870 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
| 871 |
+
<h3 style="margin: 0;"><i class="fas fa-files"></i> Generated Files</h3>
|
| 872 |
+
<button class="btn btn-primary" id="add-simulation-file" style="margin-left: 10px;">
|
| 873 |
+
<i class="fas fa-plus"></i> Add Simulation File
|
| 874 |
+
</button>
|
| 875 |
+
</div>
|
| 876 |
+
<div class="files-list" id="files-list">
|
| 877 |
+
<!-- Generated files will be listed here -->
|
| 878 |
+
</div>
|
| 879 |
+
</div>
|
| 880 |
+
|
| 881 |
+
<div class="download-section" id="download-section" style="display: none;">
|
| 882 |
+
<h3><i class="fas fa-download"></i> Download Files</h3>
|
| 883 |
+
<div class="download-options">
|
| 884 |
+
<button class="btn btn-success" id="download-zip">
|
| 885 |
+
<i class="fas fa-file-archive"></i> Download All as ZIP
|
| 886 |
+
</button>
|
| 887 |
+
|
| 888 |
+
</div>
|
| 889 |
+
</div>
|
| 890 |
+
|
| 891 |
+
<div class="simulation-summary" id="simulation-summary" style="display: none;">
|
| 892 |
+
<h3><i class="fas fa-chart-line"></i> Simulation Summary</h3>
|
| 893 |
+
<div class="summary-content" id="summary-content">
|
| 894 |
+
<!-- Simulation summary will be displayed here -->
|
| 895 |
+
</div>
|
| 896 |
+
</div>
|
| 897 |
+
</div>
|
| 898 |
+
</div>
|
| 899 |
+
|
| 900 |
+
<!-- PLUMED Section -->
|
| 901 |
+
<div id="plumed" class="tab-content">
|
| 902 |
+
<!-- PLUMED Citation Note -->
|
| 903 |
+
<div class="plumed-citation-note">
|
| 904 |
+
<i class="fas fa-info-circle"></i>
|
| 905 |
+
<div class="citation-content">
|
| 906 |
+
<p><strong>Note:</strong> All CVs are taken from <a href="https://www.plumed.org/doc-v2.9/user-doc/html/index.html" target="_blank" rel="noopener noreferrer"><strong>PLUMED v2.9</strong> <i class="fas fa-external-link-alt"></i></a> documentation.</p>
|
| 907 |
+
<p>If you use PLUMED in your research, please cite it. <a href="https://www.plumed.org/cite" target="_blank" rel="noopener noreferrer">Citation information <i class="fas fa-external-link-alt"></i></a></p>
|
| 908 |
+
</div>
|
| 909 |
+
</div>
|
| 910 |
+
|
| 911 |
+
<!-- PLUMED Collective Variables Section -->
|
| 912 |
+
<div class="card plumed-section-card">
|
| 913 |
+
<h2>
|
| 914 |
+
<i class="fas fa-chart-line"></i> PLUMED Collective Variables
|
| 915 |
+
</h2>
|
| 916 |
+
<p class="section-description">
|
| 917 |
+
Configure Collective Variables (CVs) for biased MD simulations. Select a CV from the sidebar to view documentation and examples.
|
| 918 |
+
</p>
|
| 919 |
+
|
| 920 |
+
<div class="plumed-container" id="plumed-container">
|
| 921 |
+
<!-- Left Sidebar: CV List -->
|
| 922 |
+
<div class="plumed-sidebar">
|
| 923 |
+
<div class="sidebar-header">
|
| 924 |
+
<h3><i class="fas fa-list"></i> Collective Variables</h3>
|
| 925 |
+
<div class="search-box">
|
| 926 |
+
<input type="text" id="cv-search" placeholder="Search CVs..." class="search-input">
|
| 927 |
+
<i class="fas fa-search search-icon"></i>
|
| 928 |
+
</div>
|
| 929 |
+
</div>
|
| 930 |
+
<div class="cv-list" id="cv-list">
|
| 931 |
+
<!-- CV items will be populated by JavaScript -->
|
| 932 |
+
</div>
|
| 933 |
+
</div>
|
| 934 |
+
|
| 935 |
+
<!-- Right Panel: Documentation and Editor -->
|
| 936 |
+
<div class="plumed-content">
|
| 937 |
+
<div class="content-header" id="content-header" style="display: none;">
|
| 938 |
+
<h3 id="cv-title"></h3>
|
| 939 |
+
</div>
|
| 940 |
+
|
| 941 |
+
<div class="content-body" id="content-body">
|
| 942 |
+
<div class="welcome-message" id="welcome-message">
|
| 943 |
+
<i class="fas fa-hand-pointer fa-3x"></i>
|
| 944 |
+
<h3>Select a Collective Variable</h3>
|
| 945 |
+
<p>Choose a CV from the left sidebar to view its documentation and configure it for your simulation.</p>
|
| 946 |
+
</div>
|
| 947 |
+
|
| 948 |
+
<!-- Documentation Section -->
|
| 949 |
+
<div class="cv-documentation" id="cv-documentation" style="display: none;">
|
| 950 |
+
<div class="doc-section" id="cv-module-section" style="display: none;">
|
| 951 |
+
<h4><i class="fas fa-puzzle-piece"></i> Module</h4>
|
| 952 |
+
<div class="doc-content" id="cv-module"></div>
|
| 953 |
+
</div>
|
| 954 |
+
|
| 955 |
+
<div class="doc-section">
|
| 956 |
+
<h4><i class="fas fa-info-circle"></i> Description</h4>
|
| 957 |
+
<div class="doc-content" id="cv-description"></div>
|
| 958 |
+
</div>
|
| 959 |
+
|
| 960 |
+
<div class="doc-section">
|
| 961 |
+
<h4><i class="fas fa-code"></i> Syntax</h4>
|
| 962 |
+
<div class="doc-content">
|
| 963 |
+
<pre class="syntax-box" id="cv-syntax"></pre>
|
| 964 |
+
</div>
|
| 965 |
+
</div>
|
| 966 |
+
|
| 967 |
+
<div class="doc-section" id="cv-glossary-section" style="display: none;">
|
| 968 |
+
<h4><i class="fas fa-book"></i> Glossary of keywords and components</h4>
|
| 969 |
+
<div class="doc-content" id="cv-glossary"></div>
|
| 970 |
+
</div>
|
| 971 |
+
|
| 972 |
+
<div class="doc-section" id="cv-options-section" style="display: none;">
|
| 973 |
+
<h4><i class="fas fa-list-ul"></i> Options</h4>
|
| 974 |
+
<div class="doc-content" id="cv-options"></div>
|
| 975 |
+
</div>
|
| 976 |
+
|
| 977 |
+
<div class="doc-section" id="cv-components-section" style="display: none;">
|
| 978 |
+
<h4><i class="fas fa-cogs"></i> Components</h4>
|
| 979 |
+
<div class="doc-content" id="cv-components"></div>
|
| 980 |
+
</div>
|
| 981 |
+
|
| 982 |
+
<div class="doc-section">
|
| 983 |
+
<h4><i class="fas fa-book"></i> Examples</h4>
|
| 984 |
+
<div class="doc-content">
|
| 985 |
+
<pre class="example-box" id="cv-example"></pre>
|
| 986 |
+
</div>
|
| 987 |
+
</div>
|
| 988 |
+
|
| 989 |
+
<div class="doc-section" id="cv-notes-section" style="display: none;">
|
| 990 |
+
<h4><i class="fas fa-sticky-note"></i> Notes</h4>
|
| 991 |
+
<div class="doc-content" id="cv-notes"></div>
|
| 992 |
+
</div>
|
| 993 |
+
|
| 994 |
+
<div class="doc-section" id="cv-related-section" style="display: none;">
|
| 995 |
+
<h4><i class="fas fa-link"></i> Related Collective Variables</h4>
|
| 996 |
+
<div class="doc-content" id="cv-related"></div>
|
| 997 |
+
</div>
|
| 998 |
+
</div>
|
| 999 |
+
|
| 1000 |
+
<!-- Editable Editor Section -->
|
| 1001 |
+
<div class="cv-editor-section" id="cv-editor-section" style="display: none;">
|
| 1002 |
+
<div class="editor-header">
|
| 1003 |
+
<h4><i class="fas fa-edit"></i> Your Configuration</h4>
|
| 1004 |
+
<div class="editor-actions">
|
| 1005 |
+
<button class="btn btn-sm btn-info" id="copy-config">
|
| 1006 |
+
<i class="fas fa-copy"></i> Copy
|
| 1007 |
+
</button>
|
| 1008 |
+
<button class="btn btn-sm btn-primary" id="view-pdb">
|
| 1009 |
+
<i class="fas fa-eye"></i> View PDB
|
| 1010 |
+
</button>
|
| 1011 |
+
<button class="btn btn-sm btn-secondary" id="reset-cv">
|
| 1012 |
+
<i class="fas fa-redo"></i> Reset to Example
|
| 1013 |
+
</button>
|
| 1014 |
+
<button class="btn btn-sm btn-success" id="save-config">
|
| 1015 |
+
<i class="fas fa-save"></i> Save
|
| 1016 |
+
</button>
|
| 1017 |
+
</div>
|
| 1018 |
+
</div>
|
| 1019 |
+
<textarea id="cv-editor" class="cv-editor" placeholder="Enter your PLUMED configuration here..."></textarea>
|
| 1020 |
+
<div class="editor-footer">
|
| 1021 |
+
<span class="char-count"><span id="char-count">0</span> characters</span>
|
| 1022 |
+
<span class="line-count"><span id="line-count">0</span> lines</span>
|
| 1023 |
+
</div>
|
| 1024 |
+
</div>
|
| 1025 |
+
|
| 1026 |
+
<!-- Saved Configurations -->
|
| 1027 |
+
<div class="saved-configs" id="saved-configs" style="display: none;">
|
| 1028 |
+
<h4><i class="fas fa-bookmark"></i> Saved Configurations</h4>
|
| 1029 |
+
<div class="configs-list" id="configs-list">
|
| 1030 |
+
<!-- Saved configs will be shown here -->
|
| 1031 |
+
</div>
|
| 1032 |
+
</div>
|
| 1033 |
+
</div>
|
| 1034 |
+
</div>
|
| 1035 |
+
</div>
|
| 1036 |
+
</div>
|
| 1037 |
+
|
| 1038 |
+
<!-- Custom PLUMED File Section -->
|
| 1039 |
+
<div class="card plumed-section-card" id="custom-plumed-card">
|
| 1040 |
+
<h2 class="plumed-toggle-header" id="custom-plumed-toggle-header">
|
| 1041 |
+
<i class="fas fa-file-code"></i> Write Custom PLUMED File
|
| 1042 |
+
<i class="fas fa-chevron-down toggle-icon" id="custom-plumed-toggle-icon"></i>
|
| 1043 |
+
</h2>
|
| 1044 |
+
<p class="section-description">
|
| 1045 |
+
Write your custom PLUMED configuration from scratch. This editor allows you to create a complete PLUMED input file.
|
| 1046 |
+
</p>
|
| 1047 |
+
|
| 1048 |
+
<div class="custom-plumed-section" id="custom-plumed-section">
|
| 1049 |
+
<div class="custom-editor-header">
|
| 1050 |
+
<h4><i class="fas fa-edit"></i> Custom PLUMED Configuration</h4>
|
| 1051 |
+
<div class="editor-actions">
|
| 1052 |
+
<button class="btn btn-sm btn-info" id="copy-custom-plumed">
|
| 1053 |
+
<i class="fas fa-copy"></i> Copy
|
| 1054 |
+
</button>
|
| 1055 |
+
<button class="btn btn-sm btn-primary" id="view-pdb-custom">
|
| 1056 |
+
<i class="fas fa-eye"></i> View PDB
|
| 1057 |
+
</button>
|
| 1058 |
+
<button class="btn btn-sm btn-success" id="download-custom-plumed">
|
| 1059 |
+
<i class="fas fa-save"></i> Save
|
| 1060 |
+
</button>
|
| 1061 |
+
<button class="btn btn-sm btn-secondary" id="clear-custom-plumed">
|
| 1062 |
+
<i class="fas fa-trash"></i> Clear
|
| 1063 |
+
</button>
|
| 1064 |
+
</div>
|
| 1065 |
+
</div>
|
| 1066 |
+
<textarea id="custom-plumed-editor" class="custom-plumed-editor" placeholder="Write your PLUMED configuration here... 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 -->
|
| 1102 |
+
<div class="step-navigation">
|
| 1103 |
+
<button class="nav-btn prev-btn" id="prev-tab" disabled>
|
| 1104 |
+
<i class="fas fa-chevron-left"></i> Previous
|
| 1105 |
+
</button>
|
| 1106 |
+
<div class="step-indicator">
|
| 1107 |
+
<span id="current-step">1</span> of <span id="total-steps">7</span>
|
| 1108 |
+
</div>
|
| 1109 |
+
<button class="nav-btn next-btn" id="next-tab">
|
| 1110 |
+
Next <i class="fas fa-chevron-right"></i>
|
| 1111 |
+
</button>
|
| 1112 |
+
</div>
|
| 1113 |
+
|
| 1114 |
+
<!-- Footer -->
|
| 1115 |
+
<footer class="footer">
|
| 1116 |
+
<p>© 2025 MD Simulation Pipeline. Built for molecular dynamics simulations.</p>
|
| 1117 |
+
</footer>
|
| 1118 |
+
</div>
|
| 1119 |
+
|
| 1120 |
+
<!-- PDB Viewer Modal -->
|
| 1121 |
+
<div class="modal" id="pdb-viewer-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000; overflow: auto;">
|
| 1122 |
+
<div class="modal-content" style="max-width: 90%; max-height: 90vh; margin: 5vh auto; background: white; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
| 1123 |
+
<div class="modal-header" style="display: flex; justify-content: space-between; align-items: center; padding: 15px; border-bottom: 1px solid #ddd; background: #f8f9fa; border-radius: 8px 8px 0 0;">
|
| 1124 |
+
<h3 style="margin: 0;"><i class="fas fa-file-code"></i> Viewer PDB File</h3>
|
| 1125 |
+
<button class="btn btn-sm btn-secondary" id="close-pdb-modal" style="margin: 0;">
|
| 1126 |
+
<i class="fas fa-times"></i> Close
|
| 1127 |
+
</button>
|
| 1128 |
+
</div>
|
| 1129 |
+
<div class="modal-body" style="padding: 15px; overflow: auto; max-height: calc(90vh - 100px);">
|
| 1130 |
+
<div id="pdb-content-loading" style="text-align: center; padding: 20px;">
|
| 1131 |
+
<i class="fas fa-spinner fa-spin"></i> Loading PDB file...
|
| 1132 |
+
</div>
|
| 1133 |
+
<div id="pdb-content-error" style="display: none; color: #dc3545; padding: 20px; text-align: center;">
|
| 1134 |
+
<i class="fas fa-exclamation-triangle"></i> <span id="pdb-error-message"></span>
|
| 1135 |
+
</div>
|
| 1136 |
+
<pre id="pdb-content" style="display: none; font-family: 'Courier New', monospace; font-size: 12px; line-height: 1.4; background: #f8f9fa; padding: 15px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;"></pre>
|
| 1137 |
+
</div>
|
| 1138 |
+
</div>
|
| 1139 |
+
</div>
|
| 1140 |
+
|
| 1141 |
+
<script src="../js/script.js"></script>
|
| 1142 |
+
<script src="../js/plumed_cv_docs.js"></script>
|
| 1143 |
+
<script src="../js/plumed.js"></script>
|
| 1144 |
+
</body>
|
| 1145 |
+
</html>
|
ambermdflow/html/plumed.html
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!-- PLUMED Collective Variables Section -->
|
| 2 |
+
<div id="plumed" class="tab-content">
|
| 3 |
+
<div class="card">
|
| 4 |
+
<h2><i class="fas fa-chart-line"></i> PLUMED Collective Variables</h2>
|
| 5 |
+
<p class="section-description">
|
| 6 |
+
Configure Collective Variables (CVs) for biased MD simulations. Select a CV from the sidebar to view documentation and examples.
|
| 7 |
+
</p>
|
| 8 |
+
|
| 9 |
+
<div class="plumed-container">
|
| 10 |
+
<!-- Left Sidebar: CV List -->
|
| 11 |
+
<div class="plumed-sidebar">
|
| 12 |
+
<div class="sidebar-header">
|
| 13 |
+
<h3><i class="fas fa-list"></i> Collective Variables</h3>
|
| 14 |
+
<div class="search-box">
|
| 15 |
+
<input type="text" id="cv-search" placeholder="Search CVs..." class="search-input">
|
| 16 |
+
<i class="fas fa-search search-icon"></i>
|
| 17 |
+
</div>
|
| 18 |
+
</div>
|
| 19 |
+
<div class="cv-list" id="cv-list">
|
| 20 |
+
<!-- CV items will be populated by JavaScript -->
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
|
| 24 |
+
<!-- Right Panel: Documentation and Editor -->
|
| 25 |
+
<div class="plumed-content">
|
| 26 |
+
<div class="content-header" id="content-header" style="display: none;">
|
| 27 |
+
<h3 id="cv-title"></h3>
|
| 28 |
+
<button class="btn btn-sm btn-secondary" id="reset-cv">
|
| 29 |
+
<i class="fas fa-redo"></i> Reset to Example
|
| 30 |
+
</button>
|
| 31 |
+
</div>
|
| 32 |
+
|
| 33 |
+
<div class="content-body" id="content-body">
|
| 34 |
+
<div class="welcome-message" id="welcome-message">
|
| 35 |
+
<i class="fas fa-hand-pointer fa-3x"></i>
|
| 36 |
+
<h3>Select a Collective Variable</h3>
|
| 37 |
+
<p>Choose a CV from the left sidebar to view its documentation and configure it for your simulation.</p>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
<!-- Documentation Section -->
|
| 41 |
+
<div class="cv-documentation" id="cv-documentation" style="display: none;">
|
| 42 |
+
<div class="doc-section">
|
| 43 |
+
<h4><i class="fas fa-info-circle"></i> Description</h4>
|
| 44 |
+
<div class="doc-content" id="cv-description"></div>
|
| 45 |
+
</div>
|
| 46 |
+
|
| 47 |
+
<div class="doc-section">
|
| 48 |
+
<h4><i class="fas fa-code"></i> Syntax</h4>
|
| 49 |
+
<div class="doc-content">
|
| 50 |
+
<pre class="syntax-box" id="cv-syntax"></pre>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<div class="doc-section">
|
| 55 |
+
<h4><i class="fas fa-book"></i> Example</h4>
|
| 56 |
+
<div class="doc-content">
|
| 57 |
+
<pre class="example-box" id="cv-example"></pre>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<div class="doc-section" id="cv-components-section" style="display: none;">
|
| 62 |
+
<h4><i class="fas fa-cogs"></i> Components</h4>
|
| 63 |
+
<div class="doc-content" id="cv-components"></div>
|
| 64 |
+
</div>
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
+
<!-- Editable Editor Section -->
|
| 68 |
+
<div class="cv-editor-section" id="cv-editor-section" style="display: none;">
|
| 69 |
+
<div class="editor-header">
|
| 70 |
+
<h4><i class="fas fa-edit"></i> Your Configuration</h4>
|
| 71 |
+
<div class="editor-actions">
|
| 72 |
+
<button class="btn btn-sm btn-info" id="copy-config">
|
| 73 |
+
<i class="fas fa-copy"></i> Copy
|
| 74 |
+
</button>
|
| 75 |
+
<button class="btn btn-sm btn-success" id="save-config">
|
| 76 |
+
<i class="fas fa-save"></i> Save
|
| 77 |
+
</button>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
<textarea id="cv-editor" class="cv-editor" placeholder="Enter your PLUMED configuration here..."></textarea>
|
| 81 |
+
<div class="editor-footer">
|
| 82 |
+
<span class="char-count"><span id="char-count">0</span> characters</span>
|
| 83 |
+
<span class="line-count"><span id="line-count">0</span> lines</span>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
<!-- Saved Configurations -->
|
| 88 |
+
<div class="saved-configs" id="saved-configs" style="display: none;">
|
| 89 |
+
<h4><i class="fas fa-bookmark"></i> Saved Configurations</h4>
|
| 90 |
+
<div class="configs-list" id="configs-list">
|
| 91 |
+
<!-- Saved configs will be shown here -->
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
</div>
|
| 99 |
+
|
ambermdflow/js/plumed.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
ambermdflow/js/plumed_cv_docs.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
ambermdflow/js/script.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
ambermdflow/structure_preparation.py
ADDED
|
@@ -0,0 +1,1194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AMBER Structure Preparation Script using MDAnalysis
|
| 4 |
+
Complete pipeline: extract protein, add caps, handle ligands
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import glob
|
| 8 |
+
import os
|
| 9 |
+
import re
|
| 10 |
+
import subprocess
|
| 11 |
+
import sys
|
| 12 |
+
import shutil
|
| 13 |
+
import logging
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
logger = logging.getLogger(__name__)
|
| 17 |
+
|
| 18 |
+
def run_command(cmd, description=""):
|
| 19 |
+
"""Run a command and return success status"""
|
| 20 |
+
try:
|
| 21 |
+
print(f"Running: {description}")
|
| 22 |
+
print(f"Command: {cmd}")
|
| 23 |
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=120)
|
| 24 |
+
print(f"Return code: {result.returncode}")
|
| 25 |
+
if result.stdout:
|
| 26 |
+
print(f"STDOUT: {result.stdout}")
|
| 27 |
+
if result.stderr:
|
| 28 |
+
print(f"STDERR: {result.stderr}")
|
| 29 |
+
if result.returncode != 0:
|
| 30 |
+
print(f"Error: {result.stderr}")
|
| 31 |
+
return False
|
| 32 |
+
return True
|
| 33 |
+
except subprocess.TimeoutExpired:
|
| 34 |
+
print(f"Timeout: {description}")
|
| 35 |
+
return False
|
| 36 |
+
except Exception as e:
|
| 37 |
+
print(f"Error running {description}: {str(e)}")
|
| 38 |
+
return False
|
| 39 |
+
|
| 40 |
+
def extract_protein_only(pdb_content, output_file, selected_chains=None):
|
| 41 |
+
"""Extract protein without hydrogens using MDAnalysis. Optionally restrict to selected chains."""
|
| 42 |
+
# Write input content to output file first
|
| 43 |
+
with open(output_file, 'w') as f:
|
| 44 |
+
f.write(pdb_content)
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
# Run MDAnalysis command with the output file as input
|
| 48 |
+
chain_sel = ''
|
| 49 |
+
if selected_chains:
|
| 50 |
+
chain_filters = ' or '.join([f'chain {c}' for c in selected_chains])
|
| 51 |
+
chain_sel = f' and ({chain_filters})'
|
| 52 |
+
selection = f"protein{chain_sel} and not name H* 1H* 2H* 3H*"
|
| 53 |
+
abspath = os.path.abspath(output_file)
|
| 54 |
+
cmd = f'python -c "import MDAnalysis as mda; u=mda.Universe(\'{abspath}\'); u.select_atoms(\'{selection}\').write(\'{abspath}\')"'
|
| 55 |
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
|
| 56 |
+
|
| 57 |
+
if result.returncode != 0:
|
| 58 |
+
raise Exception(f"MDAnalysis error: {result.stderr}")
|
| 59 |
+
|
| 60 |
+
return True
|
| 61 |
+
except Exception as e:
|
| 62 |
+
print(f"Error in extract_protein_only: {e}")
|
| 63 |
+
return False
|
| 64 |
+
|
| 65 |
+
def add_capping_groups(input_file, output_file):
|
| 66 |
+
"""Add ACE and NME capping groups using add_caps.py"""
|
| 67 |
+
add_caps_script = (Path(__file__).resolve().parent / "add_caps.py")
|
| 68 |
+
# First add caps
|
| 69 |
+
temp_capped = output_file.replace('.pdb', '_temp.pdb')
|
| 70 |
+
cmd = f"python {add_caps_script} -i {input_file} -o {temp_capped}"
|
| 71 |
+
if not run_command(cmd, f"Adding capping groups to {input_file}"):
|
| 72 |
+
return False
|
| 73 |
+
|
| 74 |
+
# Then add TER cards using awk
|
| 75 |
+
cmd = f"awk '/NME/{{nme=NR}} /ACE/ && nme && NR > nme {{print \"TER\"; nme=0}} {{print}}' {temp_capped} > {output_file}"
|
| 76 |
+
if not run_command(cmd, f"Adding TER cards to {temp_capped}"):
|
| 77 |
+
return False
|
| 78 |
+
|
| 79 |
+
# Clean up temp file
|
| 80 |
+
if os.path.exists(temp_capped):
|
| 81 |
+
os.remove(temp_capped)
|
| 82 |
+
|
| 83 |
+
return True
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def replace_chain_in_pdb(target_pdb, chain_id, source_pdb):
|
| 87 |
+
"""
|
| 88 |
+
Replace a specific chain in target_pdb with the chain from source_pdb.
|
| 89 |
+
Only performs replacement if the target actually contains the chain_id.
|
| 90 |
+
Used to merge ESMFold-minimized chains into 1_protein_no_hydrogens.pdb.
|
| 91 |
+
If the source has no ATOM lines (or none matching the chain), we do NOT
|
| 92 |
+
modify the target, to avoid wiping the protein when the minimized file is
|
| 93 |
+
empty or has an unexpected format.
|
| 94 |
+
"""
|
| 95 |
+
with open(target_pdb, 'r') as f:
|
| 96 |
+
target_lines = f.readlines()
|
| 97 |
+
if not any(
|
| 98 |
+
ln.startswith(('ATOM', 'HETATM')) and len(ln) >= 22 and ln[21] == chain_id
|
| 99 |
+
for ln in target_lines
|
| 100 |
+
):
|
| 101 |
+
return
|
| 102 |
+
with open(source_pdb, 'r') as f:
|
| 103 |
+
source_lines = f.readlines()
|
| 104 |
+
source_chain_lines = []
|
| 105 |
+
for ln in source_lines:
|
| 106 |
+
if ln.startswith(('ATOM', 'HETATM')) and len(ln) >= 22:
|
| 107 |
+
ch = ln[21]
|
| 108 |
+
if ch == 'A' or ch == chain_id:
|
| 109 |
+
source_chain_lines.append(ln[:21] + chain_id + ln[22:])
|
| 110 |
+
if not source_chain_lines:
|
| 111 |
+
# Fallback: minimized PDB may use chain ' ' or other; take all ATOM/HETATM.
|
| 112 |
+
for ln in source_lines:
|
| 113 |
+
if ln.startswith(('ATOM', 'HETATM')) and len(ln) >= 22:
|
| 114 |
+
source_chain_lines.append(ln[:21] + chain_id + ln[22:])
|
| 115 |
+
if not source_chain_lines:
|
| 116 |
+
return # Do not modify target: we have nothing to add; avoid wiping the protein.
|
| 117 |
+
filtered_target = [
|
| 118 |
+
ln for ln in target_lines
|
| 119 |
+
if not (ln.startswith(('ATOM', 'HETATM')) and len(ln) >= 22 and ln[21] == chain_id)
|
| 120 |
+
]
|
| 121 |
+
combined = []
|
| 122 |
+
for ln in filtered_target:
|
| 123 |
+
if ln.startswith('END'):
|
| 124 |
+
combined.extend(source_chain_lines)
|
| 125 |
+
combined.append("TER\n")
|
| 126 |
+
combined.append(ln)
|
| 127 |
+
with open(target_pdb, 'w') as f:
|
| 128 |
+
f.writelines(combined)
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def extract_selected_chains(pdb_content, output_file, selected_chains):
|
| 132 |
+
"""Extract selected chains using PyMOL commands"""
|
| 133 |
+
try:
|
| 134 |
+
# Write input content to temp file
|
| 135 |
+
temp_input = output_file.replace('.pdb', '_temp_input.pdb')
|
| 136 |
+
with open(temp_input, 'w') as f:
|
| 137 |
+
f.write(pdb_content)
|
| 138 |
+
|
| 139 |
+
# Build chain selection string
|
| 140 |
+
chain_filters = ' or '.join([f'chain {c}' for c in selected_chains])
|
| 141 |
+
selection = f"({chain_filters}) and polymer.protein"
|
| 142 |
+
|
| 143 |
+
# Use PyMOL to extract chains
|
| 144 |
+
cmd = f'''python -c "
|
| 145 |
+
import pymol
|
| 146 |
+
pymol.finish_launching(['pymol', '-c'])
|
| 147 |
+
pymol.cmd.load('{temp_input}')
|
| 148 |
+
pymol.cmd.save('{output_file}', '{selection}')
|
| 149 |
+
pymol.cmd.quit()
|
| 150 |
+
"'''
|
| 151 |
+
|
| 152 |
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
|
| 153 |
+
|
| 154 |
+
# Clean up temp file
|
| 155 |
+
if os.path.exists(temp_input):
|
| 156 |
+
os.remove(temp_input)
|
| 157 |
+
|
| 158 |
+
if result.returncode != 0:
|
| 159 |
+
print(f"PyMOL chain extraction error: {result.stderr}")
|
| 160 |
+
return False
|
| 161 |
+
|
| 162 |
+
return True
|
| 163 |
+
except Exception as e:
|
| 164 |
+
print(f"Error extracting selected chains: {e}")
|
| 165 |
+
return False
|
| 166 |
+
|
| 167 |
+
def extract_selected_ligands(pdb_content, output_file, selected_ligands):
|
| 168 |
+
"""Extract selected ligands using PyMOL commands.
|
| 169 |
+
selected_ligands: list of dicts with resn, chain, and optionally resi.
|
| 170 |
+
When resi is provided, use (resn X and chain Y and resi Z) to uniquely pick
|
| 171 |
+
one instance when the same ligand (resn) appears multiple times in the same chain.
|
| 172 |
+
"""
|
| 173 |
+
try:
|
| 174 |
+
# Write input content to temp file
|
| 175 |
+
temp_input = output_file.replace('.pdb', '_temp_input.pdb')
|
| 176 |
+
with open(temp_input, 'w') as f:
|
| 177 |
+
f.write(pdb_content)
|
| 178 |
+
|
| 179 |
+
# Build ligand selection string (include resi when present to disambiguate duplicates)
|
| 180 |
+
parts = []
|
| 181 |
+
for lig in selected_ligands:
|
| 182 |
+
resn = lig.get('resn', '').strip()
|
| 183 |
+
chain = lig.get('chain', '').strip()
|
| 184 |
+
resi = lig.get('resi') if lig.get('resi') is not None else ''
|
| 185 |
+
resi = str(resi).strip() if resi else ''
|
| 186 |
+
if resn and chain:
|
| 187 |
+
if resi:
|
| 188 |
+
parts.append(f"(resn {resn} and chain {chain} and resi {resi})")
|
| 189 |
+
else:
|
| 190 |
+
parts.append(f"(resn {resn} and chain {chain})")
|
| 191 |
+
elif resn:
|
| 192 |
+
parts.append(f"resn {resn}")
|
| 193 |
+
|
| 194 |
+
if not parts:
|
| 195 |
+
# No ligands to extract
|
| 196 |
+
with open(output_file, 'w') as f:
|
| 197 |
+
f.write('\n')
|
| 198 |
+
return True
|
| 199 |
+
|
| 200 |
+
selection = ' or '.join(parts)
|
| 201 |
+
|
| 202 |
+
# Use PyMOL to extract ligands
|
| 203 |
+
cmd = f'''python -c "
|
| 204 |
+
import pymol
|
| 205 |
+
pymol.finish_launching(['pymol', '-c'])
|
| 206 |
+
pymol.cmd.load('{temp_input}')
|
| 207 |
+
pymol.cmd.save('{output_file}', '{selection}')
|
| 208 |
+
pymol.cmd.quit()
|
| 209 |
+
"'''
|
| 210 |
+
|
| 211 |
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
|
| 212 |
+
|
| 213 |
+
# Clean up temp file
|
| 214 |
+
if os.path.exists(temp_input):
|
| 215 |
+
os.remove(temp_input)
|
| 216 |
+
|
| 217 |
+
if result.returncode != 0:
|
| 218 |
+
print(f"PyMOL ligand extraction error: {result.stderr}")
|
| 219 |
+
return False
|
| 220 |
+
|
| 221 |
+
return True
|
| 222 |
+
except Exception as e:
|
| 223 |
+
print(f"Error extracting selected ligands: {e}")
|
| 224 |
+
return False
|
| 225 |
+
|
| 226 |
+
def extract_ligands(pdb_content, output_file, ligand_residue_name=None, selected_ligands=None):
|
| 227 |
+
"""Extract ligands using MDAnalysis. Optionally restrict to selected ligands (list of dicts with resn, chain, resi)."""
|
| 228 |
+
# Write input content to output file first
|
| 229 |
+
with open(output_file, 'w') as f:
|
| 230 |
+
f.write(pdb_content)
|
| 231 |
+
|
| 232 |
+
try:
|
| 233 |
+
# Run MDAnalysis command with the output file as input
|
| 234 |
+
if selected_ligands:
|
| 235 |
+
# Build selection from provided ligand list; include resid when present to disambiguate
|
| 236 |
+
# when the same ligand (resn) appears multiple times in the same chain (GOL-A-1, GOL-A-2)
|
| 237 |
+
parts = []
|
| 238 |
+
for lig in selected_ligands:
|
| 239 |
+
resn = lig.get('resn', '').strip()
|
| 240 |
+
chain = lig.get('chain', '').strip()
|
| 241 |
+
resi = lig.get('resi') if lig.get('resi') is not None else ''
|
| 242 |
+
resi = str(resi).strip() if resi else ''
|
| 243 |
+
if resn and chain:
|
| 244 |
+
if resi:
|
| 245 |
+
# Extract leading digits for resid in case of insertion codes (e.g. 100A -> 100)
|
| 246 |
+
m = re.search(r'^(-?\d+)', resi)
|
| 247 |
+
resid_val = m.group(1) if m else resi
|
| 248 |
+
parts.append(f"(resname {resn} and segid {chain} and resid {resid_val})")
|
| 249 |
+
else:
|
| 250 |
+
parts.append(f"(resname {resn} and segid {chain})")
|
| 251 |
+
elif resn:
|
| 252 |
+
parts.append(f"resname {resn}")
|
| 253 |
+
if parts:
|
| 254 |
+
selection = ' or '.join(parts)
|
| 255 |
+
cmd = f'''python -c "
|
| 256 |
+
import MDAnalysis as mda
|
| 257 |
+
u = mda.Universe('{output_file}')
|
| 258 |
+
u.select_atoms('{selection}').write('{output_file}')
|
| 259 |
+
"'''
|
| 260 |
+
else:
|
| 261 |
+
cmd = f"python -c \"open('{output_file}','w').write('\\n')\""
|
| 262 |
+
elif ligand_residue_name:
|
| 263 |
+
# Use specified ligand residue name - extract from both ATOM and HETATM records
|
| 264 |
+
cmd = f'''python -c "
|
| 265 |
+
import MDAnalysis as mda
|
| 266 |
+
u = mda.Universe('{output_file}')
|
| 267 |
+
# Extract specific ligand residue from both ATOM and HETATM records
|
| 268 |
+
u.select_atoms('resname {ligand_residue_name}').write('{output_file}')
|
| 269 |
+
"'''
|
| 270 |
+
else:
|
| 271 |
+
# Auto-detect ligand residues
|
| 272 |
+
cmd = f'''python -c "
|
| 273 |
+
import MDAnalysis as mda
|
| 274 |
+
u = mda.Universe('{output_file}')
|
| 275 |
+
# Get all unique residue names from HETATM records
|
| 276 |
+
hetatm_residues = set()
|
| 277 |
+
for atom in u.atoms:
|
| 278 |
+
if atom.record_type == 'HETATM':
|
| 279 |
+
hetatm_residues.add(atom.resname)
|
| 280 |
+
# Remove water and ions
|
| 281 |
+
ligand_residues = hetatm_residues - {{'HOH', 'WAT', 'TIP3', 'TIP4', 'SPC', 'SPCE', 'NA', 'CL', 'K', 'MG', 'CA', 'ZN', 'FE', 'MN', 'CU', 'NI', 'CO', 'CD', 'HG', 'PB', 'SR', 'BA', 'RB', 'CS', 'LI', 'F', 'BR', 'I', 'PO4', 'PO3', 'H2PO4', 'HPO4', 'H3PO4', 'SO4'}}
|
| 282 |
+
if ligand_residues:
|
| 283 |
+
resname_sel = ' or '.join([f'resname {{res}}' for res in ligand_residues])
|
| 284 |
+
u.select_atoms(resname_sel).write('{output_file}')
|
| 285 |
+
else:
|
| 286 |
+
# No ligands found, create empty file
|
| 287 |
+
with open('{output_file}', 'w') as f:
|
| 288 |
+
f.write('\\n')
|
| 289 |
+
"'''
|
| 290 |
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
|
| 291 |
+
|
| 292 |
+
if result.returncode != 0:
|
| 293 |
+
raise Exception(f"MDAnalysis error: {result.stderr}")
|
| 294 |
+
|
| 295 |
+
# If specific ligand residue name was provided, convert ATOM to HETATM
|
| 296 |
+
if ligand_residue_name:
|
| 297 |
+
convert_atom_to_hetatm(output_file)
|
| 298 |
+
|
| 299 |
+
return True
|
| 300 |
+
except Exception as e:
|
| 301 |
+
print(f"Error in extract_ligands: {e}")
|
| 302 |
+
return False
|
| 303 |
+
|
| 304 |
+
def convert_atom_to_hetatm(pdb_file):
|
| 305 |
+
"""Convert ATOM records to HETATM in PDB file"""
|
| 306 |
+
try:
|
| 307 |
+
with open(pdb_file, 'r') as f:
|
| 308 |
+
lines = f.readlines()
|
| 309 |
+
|
| 310 |
+
# Convert ATOM to HETATM
|
| 311 |
+
converted_lines = []
|
| 312 |
+
for line in lines:
|
| 313 |
+
if line.startswith('ATOM'):
|
| 314 |
+
# Replace ATOM with HETATM
|
| 315 |
+
converted_line = 'HETATM' + line[6:]
|
| 316 |
+
converted_lines.append(converted_line)
|
| 317 |
+
else:
|
| 318 |
+
converted_lines.append(line)
|
| 319 |
+
|
| 320 |
+
# Write back to file
|
| 321 |
+
with open(pdb_file, 'w') as f:
|
| 322 |
+
f.writelines(converted_lines)
|
| 323 |
+
|
| 324 |
+
print(f"Converted ATOM records to HETATM in {pdb_file}")
|
| 325 |
+
return True
|
| 326 |
+
except Exception as e:
|
| 327 |
+
print(f"Error converting ATOM to HETATM: {e}")
|
| 328 |
+
return False
|
| 329 |
+
|
| 330 |
+
def extract_original_residue_info(ligand_file):
|
| 331 |
+
"""Extract original residue name, chain ID, and residue number from ligand PDB file"""
|
| 332 |
+
residue_info = {}
|
| 333 |
+
try:
|
| 334 |
+
with open(ligand_file, 'r') as f:
|
| 335 |
+
for line in f:
|
| 336 |
+
if line.startswith(('ATOM', 'HETATM')):
|
| 337 |
+
resname = line[17:20].strip()
|
| 338 |
+
chain_id = line[21:22].strip()
|
| 339 |
+
resnum = line[22:26].strip()
|
| 340 |
+
# Store the first residue info we find (assuming single residue per file)
|
| 341 |
+
if resname and resname not in residue_info:
|
| 342 |
+
residue_info = {
|
| 343 |
+
'resname': resname,
|
| 344 |
+
'chain_id': chain_id,
|
| 345 |
+
'resnum': resnum
|
| 346 |
+
}
|
| 347 |
+
break # We only need the first residue info
|
| 348 |
+
return residue_info
|
| 349 |
+
except Exception as e:
|
| 350 |
+
print(f"Error extracting residue info: {e}")
|
| 351 |
+
return {}
|
| 352 |
+
|
| 353 |
+
def restore_residue_info_in_pdb(pdb_file, original_resname, original_chain_id, original_resnum):
|
| 354 |
+
"""Restore original residue name, chain ID, and residue number in PDB file"""
|
| 355 |
+
try:
|
| 356 |
+
with open(pdb_file, 'r') as f:
|
| 357 |
+
lines = f.readlines()
|
| 358 |
+
|
| 359 |
+
restored_lines = []
|
| 360 |
+
for line in lines:
|
| 361 |
+
if line.startswith(('ATOM', 'HETATM')):
|
| 362 |
+
# Restore residue name (columns 17-20)
|
| 363 |
+
restored_line = line[:17] + f"{original_resname:>3}" + line[20:]
|
| 364 |
+
# Restore chain ID (column 21)
|
| 365 |
+
if original_chain_id:
|
| 366 |
+
restored_line = restored_line[:21] + original_chain_id + restored_line[22:]
|
| 367 |
+
# Restore residue number (columns 22-26)
|
| 368 |
+
if original_resnum:
|
| 369 |
+
restored_line = restored_line[:22] + f"{original_resnum:>4}" + restored_line[26:]
|
| 370 |
+
restored_lines.append(restored_line)
|
| 371 |
+
elif line.startswith('MASTER'):
|
| 372 |
+
# Skip MASTER records
|
| 373 |
+
continue
|
| 374 |
+
else:
|
| 375 |
+
restored_lines.append(line)
|
| 376 |
+
|
| 377 |
+
with open(pdb_file, 'w') as f:
|
| 378 |
+
f.writelines(restored_lines)
|
| 379 |
+
|
| 380 |
+
print(f"Restored residue info: {original_resname} {original_chain_id} {original_resnum} in {pdb_file}")
|
| 381 |
+
return True
|
| 382 |
+
except Exception as e:
|
| 383 |
+
print(f"Error restoring residue info: {e}")
|
| 384 |
+
return False
|
| 385 |
+
|
| 386 |
+
def correct_ligand_with_openbabel(ligand_file, corrected_file):
|
| 387 |
+
"""Correct ligand using OpenBabel (add hydrogens at pH 7.4) and preserve original residue info"""
|
| 388 |
+
ligand_path = os.path.abspath(ligand_file)
|
| 389 |
+
corrected_path = os.path.abspath(corrected_file)
|
| 390 |
+
if not os.path.isfile(ligand_path) or os.path.getsize(ligand_path) == 0:
|
| 391 |
+
print("Ligand file missing or empty:", ligand_path)
|
| 392 |
+
return False
|
| 393 |
+
|
| 394 |
+
# Extract original residue info before OpenBabel processing
|
| 395 |
+
residue_info = extract_original_residue_info(ligand_path)
|
| 396 |
+
original_resname = residue_info.get('resname', 'UNL')
|
| 397 |
+
original_chain_id = residue_info.get('chain_id', '')
|
| 398 |
+
original_resnum = residue_info.get('resnum', '1')
|
| 399 |
+
|
| 400 |
+
print(f"Original residue info: {original_resname} {original_chain_id} {original_resnum}")
|
| 401 |
+
|
| 402 |
+
# Use OpenBabel to add hydrogens at pH 7.4
|
| 403 |
+
cmd = f'obabel -i pdb {ligand_path} -o pdb -O {corrected_path} -p 7.4'
|
| 404 |
+
success = run_command(cmd, f"Correcting ligand with OpenBabel")
|
| 405 |
+
|
| 406 |
+
if not success:
|
| 407 |
+
return False
|
| 408 |
+
|
| 409 |
+
# Restore original residue name, chain ID, and residue number
|
| 410 |
+
if residue_info:
|
| 411 |
+
restore_residue_info_in_pdb(corrected_path, original_resname, original_chain_id, original_resnum)
|
| 412 |
+
|
| 413 |
+
return True
|
| 414 |
+
|
| 415 |
+
def split_ligands_by_residue(ligand_file, output_dir):
|
| 416 |
+
"""Split multi-ligand PDB file into individual ligand files using MDAnalysis (one file per residue)
|
| 417 |
+
This is more robust than splitting by TER records as it properly handles residue-based splitting.
|
| 418 |
+
"""
|
| 419 |
+
ligand_files = []
|
| 420 |
+
try:
|
| 421 |
+
ligand_path = os.path.abspath(ligand_file)
|
| 422 |
+
output_dir_abs = os.path.abspath(output_dir)
|
| 423 |
+
|
| 424 |
+
# Use MDAnalysis to split ligands by residue - this is the robust method
|
| 425 |
+
# Command: python -c "import MDAnalysis as mda; u=mda.Universe('3_ligands_extracted.pdb'); [res.atoms.write(f'3_ligand_extracted_{i}.pdb') for i,res in enumerate(u.residues,1)]"
|
| 426 |
+
cmd = f'''python -c "import MDAnalysis as mda; import os; u=mda.Universe('{ligand_path}'); os.chdir('{output_dir_abs}'); [res.atoms.write(f'3_ligand_extracted_{{i}}.pdb') for i,res in enumerate(u.residues,1)]"'''
|
| 427 |
+
|
| 428 |
+
print(f"Running MDAnalysis command to split ligands by residue...")
|
| 429 |
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, cwd=output_dir_abs)
|
| 430 |
+
|
| 431 |
+
if result.returncode != 0:
|
| 432 |
+
print(f"Error running MDAnalysis command: {result.stderr}")
|
| 433 |
+
print(f"Command output: {result.stdout}")
|
| 434 |
+
return []
|
| 435 |
+
|
| 436 |
+
# Collect all generated ligand files
|
| 437 |
+
ligand_files = []
|
| 438 |
+
for f in os.listdir(output_dir):
|
| 439 |
+
if f.startswith('3_ligand_extracted_') and f.endswith('.pdb'):
|
| 440 |
+
ligand_files.append(os.path.join(output_dir, f))
|
| 441 |
+
|
| 442 |
+
# Sort by number in filename (e.g., 3_ligand_extracted_1.pdb, 3_ligand_extracted_2.pdb, ...)
|
| 443 |
+
ligand_files.sort(key=lambda x: int(os.path.basename(x).split('_')[-1].split('.')[0]))
|
| 444 |
+
|
| 445 |
+
print(f"Split {len(ligand_files)} ligand(s) from {ligand_file}")
|
| 446 |
+
return ligand_files
|
| 447 |
+
except Exception as e:
|
| 448 |
+
print(f"Error splitting ligands: {e}")
|
| 449 |
+
import traceback
|
| 450 |
+
traceback.print_exc()
|
| 451 |
+
return []
|
| 452 |
+
|
| 453 |
+
def remove_connect_records(pdb_file):
|
| 454 |
+
"""Remove CONNECT and MASTER records from PDB file"""
|
| 455 |
+
try:
|
| 456 |
+
with open(pdb_file, 'r') as f:
|
| 457 |
+
lines = f.readlines()
|
| 458 |
+
|
| 459 |
+
# Filter out CONNECT and MASTER records
|
| 460 |
+
filtered_lines = [line for line in lines if not line.startswith(('CONECT', 'MASTER'))]
|
| 461 |
+
|
| 462 |
+
with open(pdb_file, 'w') as f:
|
| 463 |
+
f.writelines(filtered_lines)
|
| 464 |
+
|
| 465 |
+
print(f"Removed CONNECT and MASTER records from {pdb_file}")
|
| 466 |
+
return True
|
| 467 |
+
except Exception as e:
|
| 468 |
+
print(f"Error removing CONNECT/MASTER records: {e}")
|
| 469 |
+
return False
|
| 470 |
+
|
| 471 |
+
def convert_atom_to_hetatm_in_ligand(pdb_file):
|
| 472 |
+
"""Convert ATOM records to HETATM in ligand PDB file for consistency"""
|
| 473 |
+
try:
|
| 474 |
+
with open(pdb_file, 'r') as f:
|
| 475 |
+
lines = f.readlines()
|
| 476 |
+
|
| 477 |
+
converted_lines = []
|
| 478 |
+
converted_count = 0
|
| 479 |
+
for line in lines:
|
| 480 |
+
if line.startswith('ATOM'):
|
| 481 |
+
# Replace ATOM with HETATM, preserving the rest of the line
|
| 482 |
+
converted_line = 'HETATM' + line[6:]
|
| 483 |
+
converted_lines.append(converted_line)
|
| 484 |
+
converted_count += 1
|
| 485 |
+
else:
|
| 486 |
+
converted_lines.append(line)
|
| 487 |
+
|
| 488 |
+
with open(pdb_file, 'w') as f:
|
| 489 |
+
f.writelines(converted_lines)
|
| 490 |
+
|
| 491 |
+
if converted_count > 0:
|
| 492 |
+
print(f"Converted {converted_count} ATOM record(s) to HETATM in {pdb_file}")
|
| 493 |
+
|
| 494 |
+
return True
|
| 495 |
+
except Exception as e:
|
| 496 |
+
print(f"Error converting ATOM to HETATM: {e}")
|
| 497 |
+
return False
|
| 498 |
+
|
| 499 |
+
def make_atom_names_distinct(pdb_file):
|
| 500 |
+
"""Make all atom names distinct (C1, C2, O1, O2, H1, H2, etc.) for antechamber compatibility
|
| 501 |
+
Antechamber requires each atom to have a unique name.
|
| 502 |
+
"""
|
| 503 |
+
try:
|
| 504 |
+
from collections import defaultdict
|
| 505 |
+
|
| 506 |
+
with open(pdb_file, 'r') as f:
|
| 507 |
+
lines = f.readlines()
|
| 508 |
+
|
| 509 |
+
# Track counts for each element type
|
| 510 |
+
element_counts = defaultdict(int)
|
| 511 |
+
modified_lines = []
|
| 512 |
+
modified_count = 0
|
| 513 |
+
|
| 514 |
+
for line in lines:
|
| 515 |
+
if line.startswith(('ATOM', 'HETATM')):
|
| 516 |
+
# Extract element from the last field (column 76-78) or from atom name (columns 12-16)
|
| 517 |
+
# Try to get element from the last field first (more reliable)
|
| 518 |
+
element = line[76:78].strip()
|
| 519 |
+
|
| 520 |
+
# If element not found in last field, try to extract from atom name
|
| 521 |
+
if not element:
|
| 522 |
+
atom_name = line[12:16].strip()
|
| 523 |
+
# Extract element symbol (first letter, or first two letters for two-letter elements)
|
| 524 |
+
if len(atom_name) >= 1:
|
| 525 |
+
# Check for two-letter elements (common ones: Cl, Br, etc.)
|
| 526 |
+
if len(atom_name) >= 2 and atom_name[:2].upper() in ['CL', 'BR', 'MG', 'CA', 'ZN', 'FE', 'MN', 'CU', 'NI', 'CO', 'CD', 'HG', 'PB', 'SR', 'BA', 'RB', 'CS', 'LI']:
|
| 527 |
+
element = atom_name[:2].upper()
|
| 528 |
+
else:
|
| 529 |
+
element = atom_name[0].upper()
|
| 530 |
+
|
| 531 |
+
# Increment count for this element
|
| 532 |
+
element_counts[element] += 1
|
| 533 |
+
count = element_counts[element]
|
| 534 |
+
|
| 535 |
+
# Create distinct atom name: Element + number (e.g., C1, C2, O1, O2, H1, H2)
|
| 536 |
+
# Atom name is in columns 12-16 (4 characters, right-aligned)
|
| 537 |
+
distinct_name = f"{element}{count}"
|
| 538 |
+
|
| 539 |
+
# Ensure the name fits in 4 characters (right-aligned)
|
| 540 |
+
if len(distinct_name) > 4:
|
| 541 |
+
# For long element names, use abbreviation or truncate
|
| 542 |
+
if element == 'CL':
|
| 543 |
+
distinct_name = f"Cl{count}"[:4]
|
| 544 |
+
elif element == 'BR':
|
| 545 |
+
distinct_name = f"Br{count}"[:4]
|
| 546 |
+
else:
|
| 547 |
+
distinct_name = distinct_name[:4]
|
| 548 |
+
|
| 549 |
+
# Replace atom name (columns 12-16, right-aligned)
|
| 550 |
+
modified_line = line[:12] + f"{distinct_name:>4}" + line[16:]
|
| 551 |
+
modified_lines.append(modified_line)
|
| 552 |
+
modified_count += 1
|
| 553 |
+
else:
|
| 554 |
+
modified_lines.append(line)
|
| 555 |
+
|
| 556 |
+
with open(pdb_file, 'w') as f:
|
| 557 |
+
f.writelines(modified_lines)
|
| 558 |
+
|
| 559 |
+
if modified_count > 0:
|
| 560 |
+
print(f"Made {modified_count} atom name(s) distinct in {pdb_file}")
|
| 561 |
+
print(f"Element counts: {dict(element_counts)}")
|
| 562 |
+
|
| 563 |
+
return True
|
| 564 |
+
except Exception as e:
|
| 565 |
+
print(f"Error making atom names distinct: {e}")
|
| 566 |
+
import traceback
|
| 567 |
+
traceback.print_exc()
|
| 568 |
+
return False
|
| 569 |
+
|
| 570 |
+
def sanity_check_ligand_pdb(pdb_file):
|
| 571 |
+
"""Perform sanity checks on ligand PDB file after OpenBabel processing:
|
| 572 |
+
1. Remove CONECT and MASTER records
|
| 573 |
+
2. Convert ATOM records to HETATM for consistency
|
| 574 |
+
3. Make all atom names distinct (C1, C2, O1, O2, H1, H2, etc.) for antechamber compatibility
|
| 575 |
+
"""
|
| 576 |
+
try:
|
| 577 |
+
# Step 1: Remove CONECT and MASTER records
|
| 578 |
+
if not remove_connect_records(pdb_file):
|
| 579 |
+
return False
|
| 580 |
+
|
| 581 |
+
# Step 2: Convert ATOM to HETATM for consistency
|
| 582 |
+
if not convert_atom_to_hetatm_in_ligand(pdb_file):
|
| 583 |
+
return False
|
| 584 |
+
|
| 585 |
+
# Step 3: Make atom names distinct (required by antechamber)
|
| 586 |
+
if not make_atom_names_distinct(pdb_file):
|
| 587 |
+
return False
|
| 588 |
+
|
| 589 |
+
print(f"Sanity check completed for {pdb_file}")
|
| 590 |
+
return True
|
| 591 |
+
except Exception as e:
|
| 592 |
+
print(f"Error in sanity check: {e}")
|
| 593 |
+
return False
|
| 594 |
+
|
| 595 |
+
def merge_protein_and_ligand(protein_file, ligand_file, output_file, ligand_lines_list=None, ligand_groups=None):
|
| 596 |
+
"""Merge capped protein and corrected ligand(s) with proper PDB formatting
|
| 597 |
+
|
| 598 |
+
Args:
|
| 599 |
+
protein_file: Path to protein PDB file
|
| 600 |
+
ligand_file: Path to ligand PDB file (optional, if ligand_lines_list or ligand_groups is provided)
|
| 601 |
+
output_file: Path to output merged PDB file
|
| 602 |
+
ligand_lines_list: List of ligand lines (optional, for backward compatibility - single ligand)
|
| 603 |
+
ligand_groups: List of ligand line groups, where each group is a list of lines for one ligand (for multiple ligands with TER separation)
|
| 604 |
+
"""
|
| 605 |
+
try:
|
| 606 |
+
# Read protein file
|
| 607 |
+
with open(protein_file, 'r') as f:
|
| 608 |
+
protein_lines = f.readlines()
|
| 609 |
+
|
| 610 |
+
# Get ligand lines - prioritize ligand_groups for multiple ligands
|
| 611 |
+
if ligand_groups is not None:
|
| 612 |
+
# Multiple ligands: each group will be separated by TER
|
| 613 |
+
ligand_groups_processed = ligand_groups
|
| 614 |
+
elif ligand_lines_list is not None:
|
| 615 |
+
# Single ligand: wrap in a list for consistent processing
|
| 616 |
+
ligand_groups_processed = [ligand_lines_list] if ligand_lines_list else []
|
| 617 |
+
elif ligand_file:
|
| 618 |
+
# Read ligand file
|
| 619 |
+
with open(ligand_file, 'r') as f:
|
| 620 |
+
ligand_lines = f.readlines()
|
| 621 |
+
# Process ligand file: remove header info (CRYST, REMARK, etc.) and keep only ATOM/HETATM
|
| 622 |
+
ligand_processed = []
|
| 623 |
+
for line in ligand_lines:
|
| 624 |
+
if line.startswith(('ATOM', 'HETATM')):
|
| 625 |
+
ligand_processed.append(line)
|
| 626 |
+
ligand_groups_processed = [ligand_processed] if ligand_processed else []
|
| 627 |
+
else:
|
| 628 |
+
ligand_groups_processed = []
|
| 629 |
+
|
| 630 |
+
# Process protein file: remove 'END' and add properly formatted 'TER'
|
| 631 |
+
protein_processed = []
|
| 632 |
+
last_atom_line = None
|
| 633 |
+
for line in protein_lines:
|
| 634 |
+
if line.strip() == 'END':
|
| 635 |
+
# Create properly formatted TER card using the last atom's info
|
| 636 |
+
if last_atom_line and last_atom_line.startswith('ATOM'):
|
| 637 |
+
# Extract atom number and residue info from last atom
|
| 638 |
+
atom_num = last_atom_line[6:11].strip()
|
| 639 |
+
res_name = last_atom_line[17:20].strip()
|
| 640 |
+
chain_id = last_atom_line[21:22].strip()
|
| 641 |
+
res_num = last_atom_line[22:26].strip()
|
| 642 |
+
ter_line = f"TER {atom_num:>5} {res_name} {chain_id}{res_num}\n"
|
| 643 |
+
protein_processed.append(ter_line)
|
| 644 |
+
else:
|
| 645 |
+
protein_processed.append('TER\n')
|
| 646 |
+
else:
|
| 647 |
+
protein_processed.append(line)
|
| 648 |
+
if line.startswith('ATOM'):
|
| 649 |
+
last_atom_line = line
|
| 650 |
+
|
| 651 |
+
# Combine ligands with TER records between each ligand
|
| 652 |
+
ligand_content = []
|
| 653 |
+
for i, ligand_group in enumerate(ligand_groups_processed):
|
| 654 |
+
if ligand_group: # Only process non-empty groups
|
| 655 |
+
# Add ligand atoms
|
| 656 |
+
ligand_content.extend(ligand_group)
|
| 657 |
+
# Add TER record after each ligand (except the last one, which will be followed by END)
|
| 658 |
+
if i < len(ligand_groups_processed) - 1:
|
| 659 |
+
# Get last atom info from current ligand group to create TER
|
| 660 |
+
if ligand_group:
|
| 661 |
+
last_ligand_atom = ligand_group[-1]
|
| 662 |
+
if last_ligand_atom.startswith(('ATOM', 'HETATM')):
|
| 663 |
+
atom_num = last_ligand_atom[6:11].strip()
|
| 664 |
+
res_name = last_ligand_atom[17:20].strip()
|
| 665 |
+
chain_id = last_ligand_atom[21:22].strip()
|
| 666 |
+
res_num = last_ligand_atom[22:26].strip()
|
| 667 |
+
ter_line = f"TER {atom_num:>5} {res_name} {chain_id}{res_num}\n"
|
| 668 |
+
ligand_content.append(ter_line)
|
| 669 |
+
else:
|
| 670 |
+
ligand_content.append('TER\n')
|
| 671 |
+
|
| 672 |
+
# Combine: protein + TER + ligand(s) with TER between ligands + END
|
| 673 |
+
merged_content = ''.join(protein_processed) + ''.join(ligand_content) + 'END\n'
|
| 674 |
+
|
| 675 |
+
with open(output_file, 'w') as f:
|
| 676 |
+
f.write(merged_content)
|
| 677 |
+
|
| 678 |
+
return True
|
| 679 |
+
except Exception as e:
|
| 680 |
+
print(f"Error merging files: {str(e)}")
|
| 681 |
+
import traceback
|
| 682 |
+
traceback.print_exc()
|
| 683 |
+
return False
|
| 684 |
+
|
| 685 |
+
def prepare_structure(pdb_content, options, output_dir="output"):
|
| 686 |
+
"""Main function to prepare structure for AMBER simulation"""
|
| 687 |
+
try:
|
| 688 |
+
# Create output directory if it doesn't exist
|
| 689 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 690 |
+
|
| 691 |
+
# Define all file paths in output directory
|
| 692 |
+
# Prefer the superimposed completed structure (0_complete_structure.pdb) when it
|
| 693 |
+
# exists: it has ESMFold/minimized chains aligned to the original frame so that
|
| 694 |
+
# ligands stay in the same coordinate frame throughout the pipeline.
|
| 695 |
+
complete_structure_file = os.path.join(output_dir, "0_complete_structure.pdb")
|
| 696 |
+
original_input_file = os.path.join(output_dir, "0_original_input.pdb")
|
| 697 |
+
|
| 698 |
+
if os.path.exists(complete_structure_file):
|
| 699 |
+
input_file = complete_structure_file
|
| 700 |
+
logger.info("Using superimposed completed structure (0_complete_structure.pdb) as input for coordinate-frame consistency with ligands")
|
| 701 |
+
else:
|
| 702 |
+
input_file = original_input_file
|
| 703 |
+
logger.info("Using original input (0_original_input.pdb) as input")
|
| 704 |
+
|
| 705 |
+
user_chain_file = os.path.join(output_dir, "0_user_chain_selected.pdb")
|
| 706 |
+
protein_file = os.path.join(output_dir, "1_protein_no_hydrogens.pdb")
|
| 707 |
+
protein_capped_file = os.path.join(output_dir, "2_protein_with_caps.pdb")
|
| 708 |
+
ligand_file = os.path.join(output_dir, "3_ligands_extracted.pdb")
|
| 709 |
+
ligand_corrected_file = os.path.join(output_dir, "4_ligands_corrected.pdb")
|
| 710 |
+
tleap_ready_file = os.path.join(output_dir, "tleap_ready.pdb")
|
| 711 |
+
|
| 712 |
+
# Step 0: Save original input for reference (only if using original input)
|
| 713 |
+
# If using completed structure, we don't overwrite it
|
| 714 |
+
if input_file == original_input_file:
|
| 715 |
+
print("Step 0: Saving original input...")
|
| 716 |
+
with open(input_file, 'w') as f:
|
| 717 |
+
f.write(pdb_content)
|
| 718 |
+
else:
|
| 719 |
+
# If using completed structure, read it instead of using pdb_content
|
| 720 |
+
print("Step 0: Using completed structure as input...")
|
| 721 |
+
with open(input_file, 'r') as f:
|
| 722 |
+
pdb_content = f.read()
|
| 723 |
+
# Also save a reference to original input if it doesn't exist
|
| 724 |
+
if not os.path.exists(original_input_file):
|
| 725 |
+
print("Step 0: Saving reference to original input...")
|
| 726 |
+
with open(original_input_file, 'w') as f:
|
| 727 |
+
f.write(pdb_content)
|
| 728 |
+
|
| 729 |
+
# Step 0.5: Extract user-selected chains and ligands
|
| 730 |
+
selected_chains = options.get('selected_chains', [])
|
| 731 |
+
selected_ligands = options.get('selected_ligands', [])
|
| 732 |
+
|
| 733 |
+
if selected_chains:
|
| 734 |
+
print(f"Step 0.5a: Extracting selected chains: {', '.join(selected_chains)}")
|
| 735 |
+
if not extract_selected_chains(pdb_content, user_chain_file, selected_chains):
|
| 736 |
+
raise Exception("Failed to extract selected chains")
|
| 737 |
+
else:
|
| 738 |
+
# No chains selected - raise an error instead of using all chains
|
| 739 |
+
raise Exception("No chains selected. Please select at least one chain for structure preparation.")
|
| 740 |
+
|
| 741 |
+
if selected_ligands:
|
| 742 |
+
ligand_names = []
|
| 743 |
+
for l in selected_ligands:
|
| 744 |
+
s = f"{l.get('resn', '')}-{l.get('chain', '')}"
|
| 745 |
+
if l.get('resi'):
|
| 746 |
+
s += f" (resi {l.get('resi')})"
|
| 747 |
+
ligand_names.append(s)
|
| 748 |
+
print(f"Step 0.5b: Extracting selected ligands: {ligand_names}")
|
| 749 |
+
if not extract_selected_ligands(pdb_content, ligand_file, selected_ligands):
|
| 750 |
+
raise Exception("Failed to extract selected ligands")
|
| 751 |
+
else:
|
| 752 |
+
print("Step 0.5b: No ligands selected, creating empty ligand file")
|
| 753 |
+
with open(ligand_file, 'w') as f:
|
| 754 |
+
f.write('\n')
|
| 755 |
+
|
| 756 |
+
# Step 1: Extract protein only (remove hydrogens) from user-selected chains
|
| 757 |
+
print("Step 1: Extracting protein without hydrogens from selected chains...")
|
| 758 |
+
# Read the user-selected chain file
|
| 759 |
+
with open(user_chain_file, 'r') as f:
|
| 760 |
+
chain_content = f.read()
|
| 761 |
+
|
| 762 |
+
if not extract_protein_only(chain_content, protein_file):
|
| 763 |
+
raise Exception("Failed to extract protein")
|
| 764 |
+
|
| 765 |
+
# Step 1b: Merge minimized chains into 1_protein_no_hydrogens.pdb only when the
|
| 766 |
+
# input is NOT 0_complete_structure. When we use 0_complete_structure, it was
|
| 767 |
+
# built by rebuild_pdb_with_esmfold, which already incorporates and superimposes
|
| 768 |
+
# the minimized chains; the raw *_esmfold_minimized_noH.pdb files are in the
|
| 769 |
+
# minimization frame, so merging them here would break the coordinate frame.
|
| 770 |
+
if input_file != complete_structure_file:
|
| 771 |
+
for path in glob.glob(os.path.join(output_dir, "*_chain_*_esmfold_minimized_noH.pdb")):
|
| 772 |
+
name = os.path.basename(path).replace(".pdb", "")
|
| 773 |
+
parts = name.split("_chain_")
|
| 774 |
+
if len(parts) == 2:
|
| 775 |
+
chain_id = parts[1].split("_")[0]
|
| 776 |
+
replace_chain_in_pdb(protein_file, chain_id, path)
|
| 777 |
+
logger.info("Merged minimized chain %s into 1_protein_no_hydrogens.pdb", chain_id)
|
| 778 |
+
|
| 779 |
+
# Step 2: Add capping groups (only if add_ace or add_nme is True)
|
| 780 |
+
add_ace = options.get('add_ace', True)
|
| 781 |
+
add_nme = options.get('add_nme', True)
|
| 782 |
+
|
| 783 |
+
if add_ace or add_nme:
|
| 784 |
+
print("Step 2: Adding ACE and NME capping groups...")
|
| 785 |
+
if not add_capping_groups(protein_file, protein_capped_file):
|
| 786 |
+
raise Exception("Failed to add capping groups")
|
| 787 |
+
else:
|
| 788 |
+
print("Step 2: Skipping capping groups (add_ace=False, add_nme=False)")
|
| 789 |
+
print("Using protein without capping - copying to capped file")
|
| 790 |
+
# Copy protein file to capped file (no capping)
|
| 791 |
+
shutil.copy2(protein_file, protein_capped_file)
|
| 792 |
+
|
| 793 |
+
# Step 3: Handle ligands (use pre-extracted ligand file)
|
| 794 |
+
preserve_ligands = options.get('preserve_ligands', True)
|
| 795 |
+
ligand_present = False
|
| 796 |
+
ligand_count = 0
|
| 797 |
+
selected_ligand_count = 0 # Store count from selected_ligands separately
|
| 798 |
+
|
| 799 |
+
# Count selected ligands if provided (before processing)
|
| 800 |
+
if selected_ligands:
|
| 801 |
+
# Count unique ligand entities (by residue name, chain, and residue number)
|
| 802 |
+
unique_ligands = set()
|
| 803 |
+
for lig in selected_ligands:
|
| 804 |
+
resn = str(lig.get('resn') or '')
|
| 805 |
+
chain = str(lig.get('chain') or '')
|
| 806 |
+
resi = str(lig.get('resi') or '')
|
| 807 |
+
# Create unique identifier (resi disambiguates when same resn+chain appears multiple times)
|
| 808 |
+
unique_id = f"{resn}_{chain}_{resi}"
|
| 809 |
+
unique_ligands.add(unique_id)
|
| 810 |
+
selected_ligand_count = len(unique_ligands)
|
| 811 |
+
ligand_count = selected_ligand_count # Initialize with selected count
|
| 812 |
+
print(f"Found {selected_ligand_count} unique selected ligand(s)")
|
| 813 |
+
|
| 814 |
+
if preserve_ligands:
|
| 815 |
+
print("Step 3: Processing pre-extracted ligands...")
|
| 816 |
+
|
| 817 |
+
# Check if ligand file has content (not just empty or newline)
|
| 818 |
+
with open(ligand_file, 'r') as f:
|
| 819 |
+
ligand_content = f.read().strip()
|
| 820 |
+
|
| 821 |
+
if ligand_content and len(ligand_content) > 1:
|
| 822 |
+
ligand_present = True
|
| 823 |
+
print("Found pre-extracted ligands")
|
| 824 |
+
|
| 825 |
+
# Split ligands into individual files using MDAnalysis (by residue)
|
| 826 |
+
individual_ligand_files = split_ligands_by_residue(ligand_file, output_dir)
|
| 827 |
+
# Update ligand_count based on actual split results if not already set from selected_ligands
|
| 828 |
+
if not selected_ligands or len(individual_ligand_files) != ligand_count:
|
| 829 |
+
ligand_count = len(individual_ligand_files)
|
| 830 |
+
print(f"Split into {ligand_count} individual ligand file(s)")
|
| 831 |
+
|
| 832 |
+
if ligand_count == 0:
|
| 833 |
+
print("Warning: No ligands could be extracted from file")
|
| 834 |
+
shutil.copy2(protein_capped_file, tleap_ready_file)
|
| 835 |
+
else:
|
| 836 |
+
print(f"Processing {ligand_count} ligand(s) individually...")
|
| 837 |
+
|
| 838 |
+
# Process each ligand: OpenBabel -> sanity check -> final corrected file
|
| 839 |
+
corrected_ligand_files = []
|
| 840 |
+
for i, individual_file in enumerate(individual_ligand_files, 1):
|
| 841 |
+
# OpenBabel output file (intermediate, kept for reference)
|
| 842 |
+
obabel_file = os.path.join(output_dir, f"4_ligands_corrected_obabel_{i}.pdb")
|
| 843 |
+
# Final corrected file (after sanity checks)
|
| 844 |
+
corrected_file = os.path.join(output_dir, f"4_ligands_corrected_{i}.pdb")
|
| 845 |
+
|
| 846 |
+
# Use OpenBabel to add hydrogens (write to obabel_file)
|
| 847 |
+
if not correct_ligand_with_openbabel(individual_file, obabel_file):
|
| 848 |
+
print(f"Error: Failed to process ligand {i} with OpenBabel")
|
| 849 |
+
continue
|
| 850 |
+
|
| 851 |
+
# Copy obabel file to corrected file before sanity check
|
| 852 |
+
shutil.copy2(obabel_file, corrected_file)
|
| 853 |
+
|
| 854 |
+
# Perform sanity check on corrected_file: remove CONECT/MASTER, convert ATOM to HETATM, make names distinct
|
| 855 |
+
if not sanity_check_ligand_pdb(corrected_file):
|
| 856 |
+
print(f"Warning: Sanity check failed for ligand {i}, but continuing...")
|
| 857 |
+
|
| 858 |
+
corrected_ligand_files.append(corrected_file)
|
| 859 |
+
|
| 860 |
+
if not corrected_ligand_files:
|
| 861 |
+
print("Error: Failed to process any ligands")
|
| 862 |
+
return {
|
| 863 |
+
'error': 'Failed to process ligands with OpenBabel',
|
| 864 |
+
'prepared_structure': '',
|
| 865 |
+
'original_atoms': 0,
|
| 866 |
+
'prepared_atoms': 0,
|
| 867 |
+
'removed_components': {},
|
| 868 |
+
'added_capping': {},
|
| 869 |
+
'preserved_ligands': 0,
|
| 870 |
+
'ligand_present': False
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
# Merge all corrected ligands into a single file for tleap_ready
|
| 874 |
+
# Read all corrected ligand files and group them by ligand (for TER separation)
|
| 875 |
+
all_ligand_groups = []
|
| 876 |
+
for corrected_lig_file in corrected_ligand_files:
|
| 877 |
+
with open(corrected_lig_file, 'r') as f:
|
| 878 |
+
lig_lines = [line for line in f if line.startswith(('ATOM', 'HETATM'))]
|
| 879 |
+
if lig_lines: # Only add non-empty ligand groups
|
| 880 |
+
all_ligand_groups.append(lig_lines)
|
| 881 |
+
|
| 882 |
+
# Create combined ligand file (4_ligands_corrected.pdb) for separate download
|
| 883 |
+
with open(ligand_corrected_file, 'w') as f:
|
| 884 |
+
for i, lig_group in enumerate(all_ligand_groups):
|
| 885 |
+
for line in lig_group:
|
| 886 |
+
f.write(line if line.endswith('\n') else line + '\n')
|
| 887 |
+
if i < len(all_ligand_groups) - 1:
|
| 888 |
+
f.write('TER\n')
|
| 889 |
+
f.write('END\n')
|
| 890 |
+
print(f"Created combined ligand file: {ligand_corrected_file}")
|
| 891 |
+
|
| 892 |
+
# Merge protein and all ligands (with TER records between ligands)
|
| 893 |
+
if not merge_protein_and_ligand(protein_capped_file, None, tleap_ready_file, ligand_groups=all_ligand_groups):
|
| 894 |
+
raise Exception("Failed to merge protein and ligands")
|
| 895 |
+
elif selected_ligands and ligand_count > 0:
|
| 896 |
+
# If ligands were selected but file is empty, still mark as present if we have a count
|
| 897 |
+
ligand_present = True
|
| 898 |
+
print(f"Ligands were selected ({ligand_count} unique), but ligand file appears empty")
|
| 899 |
+
# Use protein only since no ligand content found
|
| 900 |
+
shutil.copy2(protein_capped_file, tleap_ready_file)
|
| 901 |
+
else:
|
| 902 |
+
print("No ligands found in pre-extracted file, using protein only")
|
| 903 |
+
# Copy protein file to tleap_ready
|
| 904 |
+
shutil.copy2(protein_capped_file, tleap_ready_file)
|
| 905 |
+
else:
|
| 906 |
+
print("Step 3: Skipping ligand processing (preserve_ligands=False)")
|
| 907 |
+
print("Using protein only - copying capped protein to tleap_ready")
|
| 908 |
+
# Copy protein file to tleap_ready (protein only, no ligands)
|
| 909 |
+
shutil.copy2(protein_capped_file, tleap_ready_file)
|
| 910 |
+
|
| 911 |
+
# Ensure tleap_ready.pdb exists before proceeding
|
| 912 |
+
if not os.path.exists(tleap_ready_file):
|
| 913 |
+
print(f"Error: tleap_ready.pdb was not created. Checking what went wrong...")
|
| 914 |
+
# Try to create it from protein_capped_file as fallback
|
| 915 |
+
if os.path.exists(protein_capped_file):
|
| 916 |
+
print("Creating tleap_ready.pdb from protein_capped_file as fallback...")
|
| 917 |
+
shutil.copy2(protein_capped_file, tleap_ready_file)
|
| 918 |
+
else:
|
| 919 |
+
raise Exception(f"tleap_ready.pdb was not created and protein_capped_file also doesn't exist")
|
| 920 |
+
|
| 921 |
+
# Remove CONNECT records from tleap_ready.pdb (PyMOL adds them)
|
| 922 |
+
print("Removing CONNECT records from tleap_ready.pdb...")
|
| 923 |
+
if not remove_connect_records(tleap_ready_file):
|
| 924 |
+
print("Warning: Failed to remove CONNECT records, but continuing...")
|
| 925 |
+
|
| 926 |
+
# Read the final prepared structure
|
| 927 |
+
if not os.path.exists(tleap_ready_file):
|
| 928 |
+
raise Exception("tleap_ready.pdb does not exist after processing")
|
| 929 |
+
|
| 930 |
+
with open(tleap_ready_file, 'r') as f:
|
| 931 |
+
prepared_content = f.read()
|
| 932 |
+
|
| 933 |
+
# Calculate statistics
|
| 934 |
+
original_atoms = len([line for line in pdb_content.split('\n') if line.startswith('ATOM')])
|
| 935 |
+
prepared_atoms = len([line for line in prepared_content.split('\n') if line.startswith('ATOM')])
|
| 936 |
+
|
| 937 |
+
# Calculate removed components
|
| 938 |
+
water_count = len([line for line in pdb_content.split('\n') if line.startswith('HETATM') and line[17:20].strip() in ['HOH', 'WAT', 'TIP3', 'TIP4', 'TIP5', 'SPC', 'SPCE']])
|
| 939 |
+
ion_count = len([line for line in pdb_content.split('\n') if line.startswith('HETATM') and line[17:20].strip() in ['NA', 'CL', 'K', 'MG', 'CA', 'ZN', 'FE', 'MN', 'CU', 'NI', 'CO', 'CD', 'HG', 'PB', 'SR', 'BA', 'RB', 'CS', 'LI', 'F', 'BR', 'I', 'PO4', 'PO3', 'H2PO4', 'HPO4', 'H3PO4']])
|
| 940 |
+
hydrogen_count = len([line for line in pdb_content.split('\n') if line.startswith('ATOM') and line[76:78].strip() == 'H'])
|
| 941 |
+
|
| 942 |
+
# If not preserving ligands, count them as removed
|
| 943 |
+
ligand_count = 0
|
| 944 |
+
if not preserve_ligands and ligand_present:
|
| 945 |
+
# Count ligands from the pre-extracted file
|
| 946 |
+
with open(ligand_file, 'r') as f:
|
| 947 |
+
ligand_lines = [line for line in f if line.startswith('HETATM')]
|
| 948 |
+
ligand_count = len(set(line[17:20].strip() for line in ligand_lines))
|
| 949 |
+
|
| 950 |
+
removed_components = {
|
| 951 |
+
'water': water_count,
|
| 952 |
+
'ions': ion_count,
|
| 953 |
+
'hydrogens': hydrogen_count,
|
| 954 |
+
'ligands': ligand_count
|
| 955 |
+
}
|
| 956 |
+
|
| 957 |
+
# Calculate added capping groups (only if capping was performed)
|
| 958 |
+
if add_ace or add_nme:
|
| 959 |
+
# Count unique ACE and NME residues, not individual atoms
|
| 960 |
+
ace_residues = set()
|
| 961 |
+
nme_residues = set()
|
| 962 |
+
|
| 963 |
+
for line in prepared_content.split('\n'):
|
| 964 |
+
if line.startswith('ATOM') and 'ACE' in line:
|
| 965 |
+
# Extract residue number to count unique ACE groups
|
| 966 |
+
res_num = line[22:26].strip()
|
| 967 |
+
ace_residues.add(res_num)
|
| 968 |
+
elif line.startswith('ATOM') and 'NME' in line:
|
| 969 |
+
# Extract residue number to count unique NME groups
|
| 970 |
+
res_num = line[22:26].strip()
|
| 971 |
+
nme_residues.add(res_num)
|
| 972 |
+
|
| 973 |
+
added_capping = {
|
| 974 |
+
'ace_groups': len(ace_residues),
|
| 975 |
+
'nme_groups': len(nme_residues)
|
| 976 |
+
}
|
| 977 |
+
else:
|
| 978 |
+
added_capping = {
|
| 979 |
+
'ace_groups': 0,
|
| 980 |
+
'nme_groups': 0
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
# Count preserved ligands
|
| 984 |
+
# Priority: 1) selected_ligands count, 2) processed ligand_count, 3) 0
|
| 985 |
+
if preserve_ligands:
|
| 986 |
+
if selected_ligand_count > 0:
|
| 987 |
+
# Use count from selected_ligands (most reliable)
|
| 988 |
+
preserved_ligands = selected_ligand_count
|
| 989 |
+
print(f"Using selected ligand count: {preserved_ligands}")
|
| 990 |
+
elif ligand_present and ligand_count > 0:
|
| 991 |
+
# Use count from processing
|
| 992 |
+
preserved_ligands = ligand_count
|
| 993 |
+
print(f"Using processed ligand count: {preserved_ligands}")
|
| 994 |
+
elif ligand_present:
|
| 995 |
+
# Ligands were present but count is 0, try to count from tleap_ready
|
| 996 |
+
# Count unique ligand residue names in tleap_ready.pdb
|
| 997 |
+
ligand_resnames = set()
|
| 998 |
+
for line in prepared_content.split('\n'):
|
| 999 |
+
if line.startswith('HETATM'):
|
| 1000 |
+
resname = line[17:20].strip()
|
| 1001 |
+
if resname and resname not in ['HOH', 'WAT', 'TIP', 'SPC', 'NA', 'CL', 'ACE', 'NME']:
|
| 1002 |
+
ligand_resnames.add(resname)
|
| 1003 |
+
preserved_ligands = len(ligand_resnames)
|
| 1004 |
+
print(f"Counted {preserved_ligands} unique ligand residue name(s) from tleap_ready.pdb")
|
| 1005 |
+
else:
|
| 1006 |
+
preserved_ligands = 0
|
| 1007 |
+
else:
|
| 1008 |
+
preserved_ligands = 0
|
| 1009 |
+
|
| 1010 |
+
result = {
|
| 1011 |
+
'prepared_structure': prepared_content,
|
| 1012 |
+
'original_atoms': original_atoms,
|
| 1013 |
+
'prepared_atoms': prepared_atoms,
|
| 1014 |
+
'removed_components': removed_components,
|
| 1015 |
+
'added_capping': added_capping,
|
| 1016 |
+
'preserved_ligands': preserved_ligands,
|
| 1017 |
+
'ligand_present': ligand_present,
|
| 1018 |
+
'separate_ligands': options.get('separate_ligands', False)
|
| 1019 |
+
}
|
| 1020 |
+
|
| 1021 |
+
# If separate ligands is enabled and ligands are present, include ligand content
|
| 1022 |
+
if ligand_present and options.get('separate_ligands', False):
|
| 1023 |
+
with open(ligand_corrected_file, 'r') as f:
|
| 1024 |
+
result['ligand_content'] = f.read()
|
| 1025 |
+
|
| 1026 |
+
return result
|
| 1027 |
+
|
| 1028 |
+
except Exception as e:
|
| 1029 |
+
return {
|
| 1030 |
+
'error': str(e),
|
| 1031 |
+
'prepared_structure': '',
|
| 1032 |
+
'original_atoms': 0,
|
| 1033 |
+
'prepared_atoms': 0,
|
| 1034 |
+
'removed_components': {},
|
| 1035 |
+
'added_capping': {},
|
| 1036 |
+
'preserved_ligands': 0,
|
| 1037 |
+
'ligand_present': False
|
| 1038 |
+
}
|
| 1039 |
+
|
| 1040 |
+
def parse_structure_info(pdb_content):
|
| 1041 |
+
"""Parse structure information for display"""
|
| 1042 |
+
lines = pdb_content.split('\n')
|
| 1043 |
+
atom_count = 0
|
| 1044 |
+
chains = set()
|
| 1045 |
+
residues = set()
|
| 1046 |
+
water_molecules = 0
|
| 1047 |
+
ions = 0
|
| 1048 |
+
ligands = set()
|
| 1049 |
+
hetatoms = 0
|
| 1050 |
+
|
| 1051 |
+
# Common water molecule names
|
| 1052 |
+
water_names = {'HOH', 'WAT', 'TIP3', 'TIP4', 'SPC', 'SPCE'}
|
| 1053 |
+
|
| 1054 |
+
# Common ion names
|
| 1055 |
+
ion_names = {'NA', 'CL', 'K', 'MG', 'CA', 'ZN', 'FE', 'MN', 'CU', 'NI', 'CO', 'CD', 'HG', 'PB', 'SR', 'BA', 'RB', 'CS', 'LI', 'F', 'BR', 'I', 'PO4', 'PO3', 'H2PO4', 'HPO4', 'H3PO4','SO4'}
|
| 1056 |
+
|
| 1057 |
+
# Common ligand indicators
|
| 1058 |
+
ligand_indicators = {'ATP', 'ADP', 'AMP', 'GDP', 'GTP', 'NAD', 'FAD', 'HEM', 'HEME', 'COA', 'SAM', 'PLP', 'THF', 'FMN', 'FAD', 'NADP', 'UDP', 'CDP', 'TDP', 'GDP', 'ADP', 'ATP'}
|
| 1059 |
+
|
| 1060 |
+
for line in lines:
|
| 1061 |
+
if line.startswith('ATOM'):
|
| 1062 |
+
atom_count += 1
|
| 1063 |
+
chain_id = line[21:22].strip()
|
| 1064 |
+
if chain_id:
|
| 1065 |
+
chains.add(chain_id)
|
| 1066 |
+
|
| 1067 |
+
res_name = line[17:20].strip()
|
| 1068 |
+
res_num = line[22:26].strip()
|
| 1069 |
+
residues.add(f"{res_name}{res_num}")
|
| 1070 |
+
elif line.startswith('HETATM'):
|
| 1071 |
+
hetatoms += 1
|
| 1072 |
+
res_name = line[17:20].strip()
|
| 1073 |
+
|
| 1074 |
+
if res_name in water_names:
|
| 1075 |
+
water_molecules += 1
|
| 1076 |
+
elif res_name in ion_names:
|
| 1077 |
+
ions += 1
|
| 1078 |
+
elif res_name in ligand_indicators:
|
| 1079 |
+
ligands.add(res_name)
|
| 1080 |
+
|
| 1081 |
+
# Count unique water molecules
|
| 1082 |
+
unique_water_residues = set()
|
| 1083 |
+
for line in lines:
|
| 1084 |
+
if line.startswith('HETATM'):
|
| 1085 |
+
res_name = line[17:20].strip()
|
| 1086 |
+
res_num = line[22:26].strip()
|
| 1087 |
+
if res_name in water_names:
|
| 1088 |
+
unique_water_residues.add(f"{res_name}{res_num}")
|
| 1089 |
+
|
| 1090 |
+
return {
|
| 1091 |
+
'atom_count': atom_count,
|
| 1092 |
+
'chains': list(chains),
|
| 1093 |
+
'residue_count': len(residues),
|
| 1094 |
+
'water_molecules': len(unique_water_residues),
|
| 1095 |
+
'ions': ions,
|
| 1096 |
+
'ligands': list(ligands),
|
| 1097 |
+
'hetatoms': hetatoms
|
| 1098 |
+
}
|
| 1099 |
+
|
| 1100 |
+
def test_structure_preparation():
|
| 1101 |
+
"""Test function to verify structure preparation works correctly"""
|
| 1102 |
+
# Create a simple test PDB content
|
| 1103 |
+
test_pdb = """HEADER TEST PROTEIN
|
| 1104 |
+
ATOM 1 N MET A 1 16.347 37.019 21.335 1.00 50.73 N
|
| 1105 |
+
ATOM 2 CA MET A 1 15.737 37.120 20.027 1.00 45.30 C
|
| 1106 |
+
ATOM 3 C MET A 1 15.955 35.698 19.546 1.00 41.78 C
|
| 1107 |
+
ATOM 4 O MET A 1 16.847 35.123 20.123 1.00 40.15 O
|
| 1108 |
+
ATOM 5 CB MET A 1 14.234 37.456 19.789 1.00 44.12 C
|
| 1109 |
+
ATOM 6 CG MET A 1 13.456 36.123 19.234 1.00 43.45 C
|
| 1110 |
+
ATOM 7 SD MET A 1 12.123 35.456 18.123 1.00 42.78 S
|
| 1111 |
+
ATOM 8 CE MET A 1 11.456 34.123 17.456 1.00 42.11 C
|
| 1112 |
+
ATOM 9 N ALA A 2 15.123 35.456 18.789 1.00 40.44 N
|
| 1113 |
+
ATOM 10 CA ALA A 2 14.456 34.123 18.123 1.00 39.77 C
|
| 1114 |
+
ATOM 11 C ALA A 2 13.123 33.456 17.456 1.00 39.10 C
|
| 1115 |
+
ATOM 12 O ALA A 2 12.456 32.123 16.789 1.00 38.43 O
|
| 1116 |
+
ATOM 13 CB ALA A 2 13.789 33.123 17.123 1.00 38.76 C
|
| 1117 |
+
ATOM 14 N ALA A 3 12.789 32.456 16.123 1.00 38.09 N
|
| 1118 |
+
ATOM 15 CA ALA A 3 11.456 31.789 15.456 1.00 37.42 C
|
| 1119 |
+
ATOM 16 C ALA A 3 10.123 30.456 14.789 1.00 36.75 C
|
| 1120 |
+
ATOM 17 O ALA A 3 9.456 29.123 14.123 1.00 36.08 O
|
| 1121 |
+
ATOM 18 CB ALA A 3 9.789 29.456 13.456 1.00 35.41 C
|
| 1122 |
+
ATOM 19 OXT ALA A 3 8.123 28.789 13.456 1.00 35.74 O
|
| 1123 |
+
HETATM 20 O HOH A 4 20.000 20.000 20.000 1.00 20.00 O
|
| 1124 |
+
HETATM 21 H1 HOH A 4 20.500 20.500 20.500 1.00 20.00 H
|
| 1125 |
+
HETATM 22 H2 HOH A 4 19.500 19.500 19.500 1.00 20.00 H
|
| 1126 |
+
HETATM 23 NA NA A 5 25.000 25.000 25.000 1.00 25.00 NA
|
| 1127 |
+
HETATM 24 CL CL A 6 30.000 30.000 30.000 1.00 30.00 CL
|
| 1128 |
+
HETATM 1 PG GTP A 180 29.710 30.132 -5.989 1.00 52.48 A P
|
| 1129 |
+
HETATM 2 O1G GTP A 180 29.197 28.937 -5.265 1.00 43.51 A O
|
| 1130 |
+
HETATM 3 O2G GTP A 180 30.881 29.816 -6.827 1.00 63.11 A O
|
| 1131 |
+
HETATM 4 O3G GTP A 180 30.013 31.278 -5.117 1.00 29.97 A O
|
| 1132 |
+
HETATM 5 O3B GTP A 180 28.517 30.631 -6.995 1.00 23.23 A O
|
| 1133 |
+
HETATM 6 PB GTP A 180 27.017 31.171 -6.766 1.00 29.58 A P
|
| 1134 |
+
HETATM 7 O1B GTP A 180 26.072 30.050 -6.958 1.00 17.62 A O
|
| 1135 |
+
HETATM 8 O2B GTP A 180 26.960 31.913 -5.483 1.00 38.76 A O
|
| 1136 |
+
HETATM 9 O3A GTP A 180 26.807 32.212 -7.961 1.00 13.12 A O
|
| 1137 |
+
HETATM 10 PA GTP A 180 26.277 33.726 -8.045 1.00 25.06 A P
|
| 1138 |
+
HETATM 11 O1A GTP A 180 25.089 33.867 -7.187 1.00 44.06 A O
|
| 1139 |
+
HETATM 12 O2A GTP A 180 27.427 34.635 -7.843 1.00 23.47 A O
|
| 1140 |
+
HETATM 13 O5' GTP A 180 25.804 33.834 -9.555 1.00 42.05 A O
|
| 1141 |
+
HETATM 14 C5' GTP A 180 26.615 33.475 -10.679 1.00 19.97 A C
|
| 1142 |
+
HETATM 15 C4' GTP A 180 26.219 34.288 -11.894 1.00 14.90 A C
|
| 1143 |
+
HETATM 16 O4' GTP A 180 24.826 34.017 -12.143 1.00 19.00 A O
|
| 1144 |
+
HETATM 17 C3' GTP A 180 26.372 35.802 -11.724 1.00 4.96 A C
|
| 1145 |
+
HETATM 18 O3' GTP A 180 26.880 36.347 -12.936 1.00 44.49 A O
|
| 1146 |
+
HETATM 19 C2' GTP A 180 24.932 36.243 -11.481 1.00 17.12 A C
|
| 1147 |
+
HETATM 20 O2' GTP A 180 24.719 37.581 -11.901 1.00 32.45 A O
|
| 1148 |
+
HETATM 21 C1' GTP A 180 24.069 35.240 -12.240 1.00 16.17 A C
|
| 1149 |
+
HETATM 22 N9 GTP A 180 22.724 35.005 -11.630 1.00 28.10 A N
|
| 1150 |
+
HETATM 23 C8 GTP A 180 22.443 34.655 -10.325 1.00 27.05 A C
|
| 1151 |
+
HETATM 24 N7 GTP A 180 21.168 34.483 -10.079 1.00 33.25 A N
|
| 1152 |
+
HETATM 25 C5 GTP A 180 20.554 34.737 -11.307 1.00 26.23 A C
|
| 1153 |
+
HETATM 26 C6 GTP A 180 19.183 34.712 -11.659 1.00 29.31 A C
|
| 1154 |
+
HETATM 27 O6 GTP A 180 18.205 34.448 -10.957 1.00 40.80 A O
|
| 1155 |
+
HETATM 28 N1 GTP A 180 19.000 35.036 -13.013 1.00 26.85 A N
|
| 1156 |
+
HETATM 29 C2 GTP A 180 20.022 35.339 -13.903 1.00 28.70 A C
|
| 1157 |
+
HETATM 30 N2 GTP A 180 19.627 35.619 -15.147 1.00 44.24 A N
|
| 1158 |
+
HETATM 31 N3 GTP A 180 21.301 35.367 -13.569 1.00 21.67 A N
|
| 1159 |
+
HETATM 32 C4 GTP A 180 21.489 35.054 -12.257 1.00 41.91 A C
|
| 1160 |
+
END
|
| 1161 |
+
"""
|
| 1162 |
+
|
| 1163 |
+
options = {
|
| 1164 |
+
'remove_water': True,
|
| 1165 |
+
'remove_ions': True,
|
| 1166 |
+
'remove_hydrogens': True,
|
| 1167 |
+
'add_ace': True,
|
| 1168 |
+
'add_nme': True,
|
| 1169 |
+
'preserve_ligands': True,
|
| 1170 |
+
'separate_ligands': False,
|
| 1171 |
+
'fix_missing_atoms': False,
|
| 1172 |
+
'standardize_residues': False
|
| 1173 |
+
}
|
| 1174 |
+
|
| 1175 |
+
print("Testing structure preparation...")
|
| 1176 |
+
result = prepare_structure(test_pdb, options, "output")
|
| 1177 |
+
|
| 1178 |
+
print("\n=== STATISTICS ===")
|
| 1179 |
+
print(f"Original atoms: {result['original_atoms']}")
|
| 1180 |
+
print(f"Prepared atoms: {result['prepared_atoms']}")
|
| 1181 |
+
print(f"Removed: {result['removed_components']}")
|
| 1182 |
+
print(f"Added: {result['added_capping']}")
|
| 1183 |
+
print(f"Ligands: {result['preserved_ligands']}")
|
| 1184 |
+
print(f"Ligand present: {result['ligand_present']}")
|
| 1185 |
+
|
| 1186 |
+
print(f"\nTest completed! Check 'output' folder for results:")
|
| 1187 |
+
print("- 1_protein_no_hydrogens.pdb (protein without hydrogens)")
|
| 1188 |
+
print("- 2_protein_with_caps.pdb (protein with ACE/NME caps)")
|
| 1189 |
+
print("- 3_ligands_extracted.pdb (extracted ligands, if any)")
|
| 1190 |
+
print("- 4_ligands_corrected.pdb (corrected ligands, if any)")
|
| 1191 |
+
print("- tleap_ready.pdb (final structure ready for tleap)")
|
| 1192 |
+
|
| 1193 |
+
if __name__ == "__main__":
|
| 1194 |
+
test_structure_preparation()
|
pyproject.toml
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[build-system]
|
| 2 |
+
requires = ["setuptools>=61.0", "wheel"]
|
| 3 |
+
build-backend = "setuptools.build_meta"
|
| 4 |
+
|
| 5 |
+
[project]
|
| 6 |
+
name = "ambermdflow"
|
| 7 |
+
version = "0.0.1"
|
| 8 |
+
description = "Web-based MD simulation pipeline with AMBER, ESMFold, docking, and PLUMED"
|
| 9 |
+
readme = "README.md"
|
| 10 |
+
license = {text = "MIT"}
|
| 11 |
+
authors = [
|
| 12 |
+
{name = "Hemant Nagar", email = "hn533621@ohio.edu"}
|
| 13 |
+
]
|
| 14 |
+
keywords = [
|
| 15 |
+
"molecular-dynamics",
|
| 16 |
+
"amber",
|
| 17 |
+
"md-simulation",
|
| 18 |
+
"protein-structure",
|
| 19 |
+
"esmfold",
|
| 20 |
+
"docking",
|
| 21 |
+
"autodock-vina",
|
| 22 |
+
"plumed",
|
| 23 |
+
"computational-chemistry",
|
| 24 |
+
"bioinformatics"
|
| 25 |
+
]
|
| 26 |
+
classifiers = [
|
| 27 |
+
"Development Status :: 4 - Beta",
|
| 28 |
+
"Environment :: Web Environment",
|
| 29 |
+
"Framework :: Flask",
|
| 30 |
+
"Intended Audience :: Science/Research",
|
| 31 |
+
"License :: OSI Approved :: MIT License",
|
| 32 |
+
"Operating System :: OS Independent",
|
| 33 |
+
"Programming Language :: Python :: 3",
|
| 34 |
+
"Programming Language :: Python :: 3.10",
|
| 35 |
+
"Programming Language :: Python :: 3.11",
|
| 36 |
+
"Programming Language :: Python :: 3.12",
|
| 37 |
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
| 38 |
+
"Topic :: Scientific/Engineering :: Chemistry",
|
| 39 |
+
]
|
| 40 |
+
requires-python = ">=3.10"
|
| 41 |
+
|
| 42 |
+
# Core dependencies installable via pip
|
| 43 |
+
# Note: Users MUST install these via conda FIRST (not available on PyPI):
|
| 44 |
+
# conda install -c conda-forge ambertools pymol-open-source vina openbabel rdkit gemmi
|
| 45 |
+
# These conda packages also provide: numpy, pandas, matplotlib (don't override them)
|
| 46 |
+
dependencies = [
|
| 47 |
+
"flask==2.3.3",
|
| 48 |
+
"flask-cors==4.0.0",
|
| 49 |
+
"biopython",
|
| 50 |
+
"seaborn",
|
| 51 |
+
"mdanalysis",
|
| 52 |
+
"gunicorn==21.2.0",
|
| 53 |
+
"requests",
|
| 54 |
+
"meeko>=0.7.0",
|
| 55 |
+
"prody",
|
| 56 |
+
"numpy<2.0",
|
| 57 |
+
]
|
| 58 |
+
|
| 59 |
+
[project.optional-dependencies]
|
| 60 |
+
dev = [
|
| 61 |
+
"pytest>=7.0.0",
|
| 62 |
+
"pytest-cov>=4.0.0",
|
| 63 |
+
"black>=23.0.0",
|
| 64 |
+
"isort>=5.12.0",
|
| 65 |
+
"flake8>=6.0.0",
|
| 66 |
+
]
|
| 67 |
+
|
| 68 |
+
[project.urls]
|
| 69 |
+
Homepage = "https://huggingface.co/spaces/hemantn/AmberMDFlow"
|
| 70 |
+
Documentation = "https://github.com/nagarh/AmberMDFlow"
|
| 71 |
+
Repository = "https://github.com/nagarh/AmberMDFlow"
|
| 72 |
+
Issues = "https://github.com/nagarh/AmberMDFlow/issues"
|
| 73 |
+
|
| 74 |
+
[project.scripts]
|
| 75 |
+
ambermdflow = "ambermdflow.__main__:main"
|
| 76 |
+
|
| 77 |
+
[tool.setuptools]
|
| 78 |
+
include-package-data = true
|
| 79 |
+
|
| 80 |
+
[tool.setuptools.packages.find]
|
| 81 |
+
where = ["."]
|
| 82 |
+
include = ["ambermdflow*"]
|
| 83 |
+
|
| 84 |
+
[tool.setuptools.package-data]
|
| 85 |
+
ambermdflow = [
|
| 86 |
+
"html/*.html",
|
| 87 |
+
"css/*.css",
|
| 88 |
+
"js/*.js",
|
| 89 |
+
]
|
| 90 |
+
|
| 91 |
+
[tool.black]
|
| 92 |
+
line-length = 100
|
| 93 |
+
target-version = ["py310", "py311", "py312"]
|
| 94 |
+
|
| 95 |
+
[tool.isort]
|
| 96 |
+
profile = "black"
|
| 97 |
+
line_length = 100
|
start_web_server.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AmberMDFlow - Entry point when running from project root.
|
| 4 |
+
Uses the ambermdflow package. For installed package: use `ambermdflow` or `python -m ambermdflow`.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from ambermdflow.app import app
|
| 8 |
+
|
| 9 |
+
if __name__ == "__main__":
|
| 10 |
+
app.run(debug=False, host="0.0.0.0", port=7860)
|