Deploy AmberFlow to Hugging Face Spaces
Browse files- Dockerfile +56 -0
- README.md +119 -5
- add_caps.py +176 -0
- css/styles.css +1113 -0
- html/index.html +587 -0
- js/script.js +2216 -0
- python/__pycache__/app.cpython-310.pyc +0 -0
- python/__pycache__/app.cpython-312.pyc +0 -0
- python/__pycache__/structure_preparation.cpython-310.pyc +0 -0
- python/app.py +1549 -0
- python/structure_preparation.py +693 -0
- start_web_server.py +20 -0
Dockerfile
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 14 |
+
|
| 15 |
+
# Install conda
|
| 16 |
+
RUN wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && \
|
| 17 |
+
bash Miniconda3-latest-Linux-x86_64.sh -b -p /opt/conda && \
|
| 18 |
+
rm Miniconda3-latest-Linux-x86_64.sh
|
| 19 |
+
|
| 20 |
+
ENV PATH="/opt/conda/bin:${PATH}"
|
| 21 |
+
|
| 22 |
+
# Install AMBER tools and PyMOL via conda
|
| 23 |
+
RUN conda install -c conda-forge -c bioconda \
|
| 24 |
+
ambertools \
|
| 25 |
+
pymol-open-source \
|
| 26 |
+
-y
|
| 27 |
+
|
| 28 |
+
# Install Python packages via pip
|
| 29 |
+
RUN pip install --no-cache-dir \
|
| 30 |
+
flask==2.3.3 \
|
| 31 |
+
flask-cors==4.0.0 \
|
| 32 |
+
biopython==1.81 \
|
| 33 |
+
numpy==1.24.3 \
|
| 34 |
+
pandas==2.0.3 \
|
| 35 |
+
matplotlib==3.7.2 \
|
| 36 |
+
seaborn==0.12.2 \
|
| 37 |
+
mdanalysis==2.5.0 \
|
| 38 |
+
gunicorn==21.2.0 \
|
| 39 |
+
requests==2.31.0 \
|
| 40 |
+
rdkit==2023.3.1 \
|
| 41 |
+
scipy==1.11.1
|
| 42 |
+
|
| 43 |
+
# Set working directory
|
| 44 |
+
WORKDIR /AmberFlow
|
| 45 |
+
|
| 46 |
+
# Copy the entire project
|
| 47 |
+
COPY . .
|
| 48 |
+
|
| 49 |
+
# Make sure the python directory is in the Python path
|
| 50 |
+
ENV PYTHONPATH="${PYTHONPATH}:/AmberFlow/python"
|
| 51 |
+
|
| 52 |
+
# Expose the port
|
| 53 |
+
EXPOSE 7860
|
| 54 |
+
|
| 55 |
+
# Run the application
|
| 56 |
+
CMD ["python", "start_web_server.py"]
|
README.md
CHANGED
|
@@ -1,12 +1,126 @@
|
|
|
|
|
| 1 |
---
|
| 2 |
-
title: AmberFlow
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
-
|
| 10 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
+
<!--
|
| 2 |
---
|
| 3 |
+
title: AmberFlow - MD Simulation Pipeline
|
| 4 |
+
emoji: 🧬
|
| 5 |
+
colorFrom: blue
|
| 6 |
colorTo: purple
|
| 7 |
sdk: docker
|
| 8 |
pinned: false
|
| 9 |
license: mit
|
| 10 |
+
app_port: 7860
|
| 11 |
---
|
| 12 |
+
-->
|
| 13 |
+
|
| 14 |
+
# AmberFlow - Molecular Dynamics Simulation Pipeline
|
| 15 |
+
|
| 16 |
+
🧬 **AmberFlow** is a comprehensive web-based pipeline for preparing and setting up molecular dynamics (MD) simulations using the AMBER force field. This tool provides an intuitive interface for protein structure preparation, parameter generation, and simulation file creation.
|
| 17 |
+
|
| 18 |
+
## Features
|
| 19 |
+
|
| 20 |
+
### 🔬 Structure Preparation
|
| 21 |
+
- **Protein Loading**: Upload PDB files or fetch from RCSB PDB database
|
| 22 |
+
- **Structure Cleaning**: Remove water molecules, ions, and hydrogen atoms
|
| 23 |
+
- **Capping Groups**: Add ACE (N-terminal) and NME (C-terminal) capping groups
|
| 24 |
+
- **Ligand Handling**: Preserve and process ligands with automatic force field parameter generation
|
| 25 |
+
- **3D Visualization**: Interactive molecular viewer using NGL
|
| 26 |
+
|
| 27 |
+
### ⚙️ Simulation Parameters
|
| 28 |
+
- **Force Fields**: Support for ff14SB and ff19SB protein force fields
|
| 29 |
+
- **Water Models**: TIP3P and SPCE water models
|
| 30 |
+
- **System Setup**: Configurable box size and ion addition
|
| 31 |
+
- **Thermodynamics**: Temperature and pressure control
|
| 32 |
+
|
| 33 |
+
### 📋 Simulation Steps
|
| 34 |
+
- **Restrained Minimization**: Position-restrained energy minimization
|
| 35 |
+
- **Minimization**: Full system energy minimization
|
| 36 |
+
- **NPT Heating**: Temperature equilibration
|
| 37 |
+
- **NPT Equilibration**: Pressure and temperature equilibration
|
| 38 |
+
- **Production Run**: Configurable production MD simulation
|
| 39 |
+
|
| 40 |
+
### 📁 File Generation
|
| 41 |
+
- **AMBER Input Files**: Complete set of .in files for all simulation steps
|
| 42 |
+
- **Force Field Parameters**: Generated .prmtop and .inpcrd files
|
| 43 |
+
- **PBS Scripts**: HPC submission scripts
|
| 44 |
+
- **Analysis Scripts**: Post-simulation analysis tools
|
| 45 |
+
|
| 46 |
+
## Usage
|
| 47 |
+
|
| 48 |
+
1. **Load Protein Structure**
|
| 49 |
+
- Upload a PDB file or enter a PDB ID to fetch from RCSB
|
| 50 |
+
- View 3D structure and basic information
|
| 51 |
+
|
| 52 |
+
2. **Prepare Structure**
|
| 53 |
+
- Configure structure preparation options
|
| 54 |
+
- Remove unwanted components (water, ions, hydrogens)
|
| 55 |
+
- Add capping groups for termini
|
| 56 |
+
- Handle ligands if present
|
| 57 |
+
|
| 58 |
+
3. **Set Simulation Parameters**
|
| 59 |
+
- Choose force field and water model
|
| 60 |
+
- Configure system parameters
|
| 61 |
+
- Set temperature and pressure
|
| 62 |
+
|
| 63 |
+
4. **Configure Simulation Steps**
|
| 64 |
+
- Enable/disable simulation steps
|
| 65 |
+
- Set step-specific parameters
|
| 66 |
+
- Configure production run duration
|
| 67 |
+
|
| 68 |
+
5. **Generate Files**
|
| 69 |
+
- Generate all simulation input files
|
| 70 |
+
- Download files as ZIP archive
|
| 71 |
+
- Preview generated files
|
| 72 |
+
|
| 73 |
+
## Technical Details
|
| 74 |
+
|
| 75 |
+
### Dependencies
|
| 76 |
+
- **MDAnalysis**: Structure manipulation and analysis
|
| 77 |
+
- **BioPython**: PDB file parsing
|
| 78 |
+
- **Flask**: Web framework
|
| 79 |
+
- **NGL Viewer**: 3D molecular visualization
|
| 80 |
+
- **AMBER Tools**: Force field parameter generation
|
| 81 |
+
|
| 82 |
+
### File Structure
|
| 83 |
+
```
|
| 84 |
+
AmberFlow/
|
| 85 |
+
├── app.py # Hugging Face Spaces entry point
|
| 86 |
+
├── requirements.txt # Python dependencies
|
| 87 |
+
├── python/
|
| 88 |
+
│ ├── app.py # Main Flask application
|
| 89 |
+
│ ├── structure_preparation.py
|
| 90 |
+
│ └── requirements.txt
|
| 91 |
+
├── html/
|
| 92 |
+
│ └── index.html # Web interface
|
| 93 |
+
├── css/
|
| 94 |
+
│ └── styles.css # Styling
|
| 95 |
+
├── js/
|
| 96 |
+
│ └── script.js # Frontend logic
|
| 97 |
+
├── templates/ # AMBER input file templates
|
| 98 |
+
└── add_caps.py # Capping group addition script
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
## Citation
|
| 102 |
+
|
| 103 |
+
If you use AmberFlow in your research, please cite:
|
| 104 |
+
|
| 105 |
+
```bibtex
|
| 106 |
+
@software{Amberflow2025,
|
| 107 |
+
title={AmberFlow: Molecular Dynamics Simulation Pipeline},
|
| 108 |
+
author={Hemant Nagar},
|
| 109 |
+
year={2025},
|
| 110 |
+
url={https://huggingface.co/spaces/hemantn/AmberFlow}
|
| 111 |
+
}
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
## Acknowledgments
|
| 115 |
+
|
| 116 |
+
- **Mohd Ibrahim** (Technical University of Munich) for the protein capping functionality (`add_caps.py`)
|
| 117 |
+
|
| 118 |
+
## License
|
| 119 |
+
|
| 120 |
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
| 121 |
+
|
| 122 |
+
## Contact
|
| 123 |
+
|
| 124 |
+
- **Author**: Hemant Nagar
|
| 125 |
+
- **Email**: hn533621@ohio.edu
|
| 126 |
|
|
|
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)
|
css/styles.css
ADDED
|
@@ -0,0 +1,1113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
/* Protein Loading Styles */
|
| 235 |
+
.input-methods {
|
| 236 |
+
display: grid;
|
| 237 |
+
grid-template-columns: 1fr auto 1fr;
|
| 238 |
+
gap: 2rem;
|
| 239 |
+
align-items: center;
|
| 240 |
+
margin-bottom: 2rem;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.method-option {
|
| 244 |
+
background: #f8f9fa;
|
| 245 |
+
padding: 1.5rem;
|
| 246 |
+
border-radius: 8px;
|
| 247 |
+
border: 2px dashed #dee2e6;
|
| 248 |
+
transition: all 0.3s ease;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.method-option:hover {
|
| 252 |
+
border-color: #3498db;
|
| 253 |
+
background: #f0f8ff;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
.method-option h3 {
|
| 257 |
+
color: #2c3e50;
|
| 258 |
+
margin-bottom: 1rem;
|
| 259 |
+
font-size: 1.2rem;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.method-option h3 i {
|
| 263 |
+
margin-right: 0.5rem;
|
| 264 |
+
color: #3498db;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.divider {
|
| 268 |
+
text-align: center;
|
| 269 |
+
font-weight: bold;
|
| 270 |
+
color: #7f8c8d;
|
| 271 |
+
font-size: 1.1rem;
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
.divider::before,
|
| 275 |
+
.divider::after {
|
| 276 |
+
content: '';
|
| 277 |
+
display: inline-block;
|
| 278 |
+
width: 50px;
|
| 279 |
+
height: 2px;
|
| 280 |
+
background: #bdc3c7;
|
| 281 |
+
vertical-align: middle;
|
| 282 |
+
margin: 0 1rem;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
/* File Upload Area */
|
| 286 |
+
.file-upload-area {
|
| 287 |
+
border: 2px dashed #3498db;
|
| 288 |
+
border-radius: 8px;
|
| 289 |
+
padding: 2rem;
|
| 290 |
+
text-align: center;
|
| 291 |
+
cursor: pointer;
|
| 292 |
+
transition: all 0.3s ease;
|
| 293 |
+
background: #f8f9fa;
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
+
.file-upload-area:hover {
|
| 297 |
+
background: #e3f2fd;
|
| 298 |
+
border-color: #2980b9;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
.file-upload-area i {
|
| 302 |
+
font-size: 3rem;
|
| 303 |
+
color: #3498db;
|
| 304 |
+
margin-bottom: 1rem;
|
| 305 |
+
display: block;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.file-upload-area p {
|
| 309 |
+
margin-bottom: 1rem;
|
| 310 |
+
color: #7f8c8d;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.file-info {
|
| 314 |
+
background: #d4edda;
|
| 315 |
+
border: 1px solid #c3e6cb;
|
| 316 |
+
border-radius: 4px;
|
| 317 |
+
padding: 1rem;
|
| 318 |
+
margin-top: 1rem;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.file-info p {
|
| 322 |
+
margin: 0.25rem 0;
|
| 323 |
+
color: #155724;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
/* PDB Fetch */
|
| 327 |
+
.pdb-fetch {
|
| 328 |
+
margin-top: 1rem;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.input-group {
|
| 332 |
+
display: flex;
|
| 333 |
+
gap: 1rem;
|
| 334 |
+
align-items: end;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.input-group label {
|
| 338 |
+
font-weight: 600;
|
| 339 |
+
color: #2c3e50;
|
| 340 |
+
margin-bottom: 0.5rem;
|
| 341 |
+
display: block;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.input-group input {
|
| 345 |
+
flex: 1;
|
| 346 |
+
padding: 0.75rem;
|
| 347 |
+
border: 2px solid #dee2e6;
|
| 348 |
+
border-radius: 4px;
|
| 349 |
+
font-size: 1rem;
|
| 350 |
+
transition: border-color 0.3s ease;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
.input-group input:focus {
|
| 354 |
+
outline: none;
|
| 355 |
+
border-color: #3498db;
|
| 356 |
+
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
/* Status Messages */
|
| 360 |
+
.status-message {
|
| 361 |
+
margin-top: 1rem;
|
| 362 |
+
padding: 0.75rem;
|
| 363 |
+
border-radius: 4px;
|
| 364 |
+
font-weight: 500;
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.status-message.success {
|
| 368 |
+
background: #d4edda;
|
| 369 |
+
color: #155724;
|
| 370 |
+
border: 1px solid #c3e6cb;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
.status-message.error {
|
| 374 |
+
background: #f8d7da;
|
| 375 |
+
color: #721c24;
|
| 376 |
+
border: 1px solid #f5c6cb;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.status-message.info {
|
| 380 |
+
background: #d1ecf1;
|
| 381 |
+
color: #0c5460;
|
| 382 |
+
border: 1px solid #bee5eb;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
/* Protein Preview */
|
| 386 |
+
.protein-preview {
|
| 387 |
+
margin-top: 2rem;
|
| 388 |
+
background: #f8f9fa;
|
| 389 |
+
border-radius: 8px;
|
| 390 |
+
padding: 1.5rem;
|
| 391 |
+
border: 1px solid #dee2e6;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.preview-content {
|
| 395 |
+
display: grid;
|
| 396 |
+
grid-template-columns: 1fr 1fr;
|
| 397 |
+
gap: 2rem;
|
| 398 |
+
margin-top: 1rem;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.protein-info p {
|
| 402 |
+
margin: 0.5rem 0;
|
| 403 |
+
font-size: 1rem;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.protein-visualization {
|
| 407 |
+
background: white;
|
| 408 |
+
border-radius: 4px;
|
| 409 |
+
padding: 1rem;
|
| 410 |
+
border: 1px solid #dee2e6;
|
| 411 |
+
min-height: 300px;
|
| 412 |
+
position: relative;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
/* Ensure NGL viewer controls overlay within both original and prepared viewers */
|
| 416 |
+
.molecule-viewer {
|
| 417 |
+
position: relative;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
#ngl-viewer {
|
| 421 |
+
border-radius: 4px;
|
| 422 |
+
background: #f8f9fa;
|
| 423 |
+
border: 1px solid #dee2e6;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.viewer-controls {
|
| 427 |
+
position: absolute;
|
| 428 |
+
top: 10px;
|
| 429 |
+
right: 10px;
|
| 430 |
+
display: flex;
|
| 431 |
+
gap: 0.5rem;
|
| 432 |
+
z-index: 10;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.viewer-controls .btn {
|
| 436 |
+
padding: 0.25rem 0.5rem;
|
| 437 |
+
font-size: 0.8rem;
|
| 438 |
+
border-radius: 3px;
|
| 439 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.viewer-controls .btn:hover {
|
| 443 |
+
transform: translateY(-1px);
|
| 444 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
/* Simulation Parameters */
|
| 448 |
+
.params-grid {
|
| 449 |
+
display: grid;
|
| 450 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 451 |
+
gap: 2rem;
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
.param-section {
|
| 455 |
+
background: #f8f9fa;
|
| 456 |
+
padding: 1.5rem;
|
| 457 |
+
border-radius: 8px;
|
| 458 |
+
border: 1px solid #dee2e6;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
.param-section h3 {
|
| 462 |
+
color: #2c3e50;
|
| 463 |
+
margin-bottom: 1rem;
|
| 464 |
+
font-size: 1.2rem;
|
| 465 |
+
border-bottom: 1px solid #bdc3c7;
|
| 466 |
+
padding-bottom: 0.5rem;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
.param-section h3 i {
|
| 470 |
+
margin-right: 0.5rem;
|
| 471 |
+
color: #3498db;
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
.form-group {
|
| 475 |
+
margin-bottom: 1rem;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.form-group label {
|
| 479 |
+
display: block;
|
| 480 |
+
margin-bottom: 0.5rem;
|
| 481 |
+
font-weight: 600;
|
| 482 |
+
color: #2c3e50;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
.form-group input,
|
| 486 |
+
.form-group select {
|
| 487 |
+
width: 100%;
|
| 488 |
+
padding: 0.75rem;
|
| 489 |
+
border: 2px solid #dee2e6;
|
| 490 |
+
border-radius: 4px;
|
| 491 |
+
font-size: 1rem;
|
| 492 |
+
transition: border-color 0.3s ease;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.form-group input:focus,
|
| 496 |
+
.form-group select:focus {
|
| 497 |
+
outline: none;
|
| 498 |
+
border-color: #3498db;
|
| 499 |
+
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
/* Simulation Steps */
|
| 503 |
+
.steps-container {
|
| 504 |
+
display: flex;
|
| 505 |
+
flex-direction: column;
|
| 506 |
+
gap: 1.5rem;
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
.step-item {
|
| 510 |
+
background: #f8f9fa;
|
| 511 |
+
border-radius: 8px;
|
| 512 |
+
border: 1px solid #dee2e6;
|
| 513 |
+
overflow: hidden;
|
| 514 |
+
transition: all 0.3s ease;
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
.step-item:hover {
|
| 518 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
| 519 |
+
transform: translateY(-2px);
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
.step-header {
|
| 523 |
+
display: flex;
|
| 524 |
+
justify-content: space-between;
|
| 525 |
+
align-items: center;
|
| 526 |
+
padding: 1.5rem;
|
| 527 |
+
background: #34495e;
|
| 528 |
+
color: white;
|
| 529 |
+
cursor: pointer;
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
.step-header h3 {
|
| 533 |
+
margin: 0;
|
| 534 |
+
font-size: 1.2rem;
|
| 535 |
+
font-weight: 600;
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
.step-header h3 i {
|
| 539 |
+
margin-right: 0.5rem;
|
| 540 |
+
color: #3498db;
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
.step-content {
|
| 544 |
+
padding: 1.5rem;
|
| 545 |
+
background: white;
|
| 546 |
+
display: none;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.step-content.active {
|
| 550 |
+
display: block;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
.form-row {
|
| 554 |
+
display: grid;
|
| 555 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 556 |
+
gap: 1rem;
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
/* Toggle Switch */
|
| 560 |
+
.switch {
|
| 561 |
+
position: relative;
|
| 562 |
+
display: inline-block;
|
| 563 |
+
width: 60px;
|
| 564 |
+
height: 34px;
|
| 565 |
+
}
|
| 566 |
+
|
| 567 |
+
.switch input {
|
| 568 |
+
opacity: 0;
|
| 569 |
+
width: 0;
|
| 570 |
+
height: 0;
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
.slider {
|
| 574 |
+
position: absolute;
|
| 575 |
+
cursor: pointer;
|
| 576 |
+
top: 0;
|
| 577 |
+
left: 0;
|
| 578 |
+
right: 0;
|
| 579 |
+
bottom: 0;
|
| 580 |
+
background-color: #ccc;
|
| 581 |
+
transition: .4s;
|
| 582 |
+
border-radius: 34px;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
.slider:before {
|
| 586 |
+
position: absolute;
|
| 587 |
+
content: "";
|
| 588 |
+
height: 26px;
|
| 589 |
+
width: 26px;
|
| 590 |
+
left: 4px;
|
| 591 |
+
bottom: 4px;
|
| 592 |
+
background-color: white;
|
| 593 |
+
transition: .4s;
|
| 594 |
+
border-radius: 50%;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
input:checked + .slider {
|
| 598 |
+
background-color: #3498db;
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
input:checked + .slider:before {
|
| 602 |
+
transform: translateX(26px);
|
| 603 |
+
}
|
| 604 |
+
|
| 605 |
+
/* File Generation */
|
| 606 |
+
.generation-controls {
|
| 607 |
+
display: flex;
|
| 608 |
+
gap: 1rem;
|
| 609 |
+
margin-bottom: 2rem;
|
| 610 |
+
flex-wrap: wrap;
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
.files-preview {
|
| 614 |
+
margin-bottom: 2rem;
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
.files-list {
|
| 618 |
+
display: grid;
|
| 619 |
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
| 620 |
+
gap: 1rem;
|
| 621 |
+
margin-top: 1rem;
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
.file-item {
|
| 625 |
+
background: #f8f9fa;
|
| 626 |
+
border: 1px solid #dee2e6;
|
| 627 |
+
border-radius: 8px;
|
| 628 |
+
padding: 1rem;
|
| 629 |
+
transition: all 0.3s ease;
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
.file-item:hover {
|
| 633 |
+
background: #e3f2fd;
|
| 634 |
+
border-color: #3498db;
|
| 635 |
+
transform: translateY(-2px);
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
.file-item h4 {
|
| 639 |
+
color: #2c3e50;
|
| 640 |
+
margin-bottom: 0.5rem;
|
| 641 |
+
font-size: 1.1rem;
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
.file-item p {
|
| 645 |
+
color: #7f8c8d;
|
| 646 |
+
font-size: 0.9rem;
|
| 647 |
+
margin: 0.25rem 0;
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
.download-section {
|
| 651 |
+
margin-bottom: 2rem;
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
.download-options {
|
| 655 |
+
display: flex;
|
| 656 |
+
gap: 1rem;
|
| 657 |
+
flex-wrap: wrap;
|
| 658 |
+
margin-top: 1rem;
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
.simulation-summary {
|
| 662 |
+
background: #f8f9fa;
|
| 663 |
+
border-radius: 8px;
|
| 664 |
+
padding: 1.5rem;
|
| 665 |
+
border: 1px solid #dee2e6;
|
| 666 |
+
}
|
| 667 |
+
|
| 668 |
+
.summary-content {
|
| 669 |
+
display: grid;
|
| 670 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 671 |
+
gap: 1rem;
|
| 672 |
+
margin-top: 1rem;
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
.summary-item {
|
| 676 |
+
background: white;
|
| 677 |
+
padding: 1rem;
|
| 678 |
+
border-radius: 4px;
|
| 679 |
+
border: 1px solid #dee2e6;
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
.summary-item h4 {
|
| 683 |
+
color: #2c3e50;
|
| 684 |
+
margin-bottom: 0.5rem;
|
| 685 |
+
font-size: 1rem;
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.summary-item p {
|
| 689 |
+
color: #7f8c8d;
|
| 690 |
+
margin: 0.25rem 0;
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
/* Checkbox Group Styles */
|
| 694 |
+
.checkbox-group {
|
| 695 |
+
display: flex;
|
| 696 |
+
gap: 1rem;
|
| 697 |
+
margin-top: 0.5rem;
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
.checkbox-container {
|
| 701 |
+
display: flex;
|
| 702 |
+
align-items: center;
|
| 703 |
+
cursor: pointer;
|
| 704 |
+
font-size: 0.9rem;
|
| 705 |
+
color: #495057;
|
| 706 |
+
}
|
| 707 |
+
|
| 708 |
+
.checkbox-container input[type="checkbox"] {
|
| 709 |
+
margin-right: 0.5rem;
|
| 710 |
+
transform: scale(1.2);
|
| 711 |
+
}
|
| 712 |
+
|
| 713 |
+
.checkbox-container:hover {
|
| 714 |
+
color: #007bff;
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
/* Ion Controls */
|
| 718 |
+
.ion-controls {
|
| 719 |
+
display: flex;
|
| 720 |
+
gap: 0.5rem;
|
| 721 |
+
align-items: center;
|
| 722 |
+
}
|
| 723 |
+
|
| 724 |
+
.ion-controls select {
|
| 725 |
+
flex: 1;
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
|
| 729 |
+
|
| 730 |
+
|
| 731 |
+
#ligand-forcefield-section {
|
| 732 |
+
transition: all 0.3s ease;
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
#ligand-forcefield-section.disabled {
|
| 736 |
+
opacity: 0.5;
|
| 737 |
+
pointer-events: none;
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
/* Tooltip styling for better text wrapping */
|
| 741 |
+
.tooltip {
|
| 742 |
+
max-width: 300px;
|
| 743 |
+
}
|
| 744 |
+
|
| 745 |
+
.tooltip-inner {
|
| 746 |
+
text-align: left;
|
| 747 |
+
white-space: normal;
|
| 748 |
+
word-wrap: break-word;
|
| 749 |
+
}
|
| 750 |
+
|
| 751 |
+
/* Button Styles */
|
| 752 |
+
.btn {
|
| 753 |
+
display: inline-flex;
|
| 754 |
+
align-items: center;
|
| 755 |
+
padding: 0.75rem 1.5rem;
|
| 756 |
+
border: none;
|
| 757 |
+
border-radius: 6px;
|
| 758 |
+
font-size: 1rem;
|
| 759 |
+
font-weight: 600;
|
| 760 |
+
cursor: pointer;
|
| 761 |
+
transition: all 0.3s ease;
|
| 762 |
+
text-decoration: none;
|
| 763 |
+
gap: 0.5rem;
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
.btn:hover {
|
| 767 |
+
transform: translateY(-2px);
|
| 768 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
.btn:active {
|
| 772 |
+
transform: translateY(0);
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
.btn-primary {
|
| 776 |
+
background: linear-gradient(135deg, #3498db, #2980b9);
|
| 777 |
+
color: white;
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
.btn-primary:hover {
|
| 781 |
+
background: linear-gradient(135deg, #2980b9, #1f618d);
|
| 782 |
+
}
|
| 783 |
+
|
| 784 |
+
.btn-secondary {
|
| 785 |
+
background: linear-gradient(135deg, #95a5a6, #7f8c8d);
|
| 786 |
+
color: white;
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
.btn-secondary:hover {
|
| 790 |
+
background: linear-gradient(135deg, #7f8c8d, #6c7b7d);
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
.btn-success {
|
| 794 |
+
background: linear-gradient(135deg, #27ae60, #229954);
|
| 795 |
+
color: white;
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
.btn-success:hover {
|
| 799 |
+
background: linear-gradient(135deg, #229954, #1e8449);
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
.btn-info {
|
| 803 |
+
background: linear-gradient(135deg, #17a2b8, #138496);
|
| 804 |
+
color: white;
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
.btn-info:hover {
|
| 808 |
+
background: linear-gradient(135deg, #138496, #117a8b);
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
.btn i {
|
| 812 |
+
font-size: 1rem;
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
/* Footer */
|
| 816 |
+
.footer {
|
| 817 |
+
background: #2c3e50;
|
| 818 |
+
color: white;
|
| 819 |
+
text-align: center;
|
| 820 |
+
padding: 2rem;
|
| 821 |
+
margin-top: 2rem;
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
.footer p {
|
| 825 |
+
margin: 0;
|
| 826 |
+
opacity: 0.8;
|
| 827 |
+
}
|
| 828 |
+
|
| 829 |
+
/* Responsive Design */
|
| 830 |
+
@media (max-width: 768px) {
|
| 831 |
+
.container {
|
| 832 |
+
margin: 0;
|
| 833 |
+
box-shadow: none;
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
.header-content h1 {
|
| 837 |
+
font-size: 2rem;
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
.tab-navigation {
|
| 841 |
+
flex-direction: column;
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
.tab-button {
|
| 845 |
+
min-width: auto;
|
| 846 |
+
border-bottom: 1px solid #2c3e50;
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
.main-content {
|
| 850 |
+
padding: 1rem;
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
.input-methods {
|
| 854 |
+
grid-template-columns: 1fr;
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
.divider {
|
| 858 |
+
order: 2;
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
.preview-content {
|
| 862 |
+
grid-template-columns: 1fr;
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
.params-grid {
|
| 866 |
+
grid-template-columns: 1fr;
|
| 867 |
+
}
|
| 868 |
+
|
| 869 |
+
.form-row {
|
| 870 |
+
grid-template-columns: 1fr;
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
+
.generation-controls {
|
| 874 |
+
flex-direction: column;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
.download-options {
|
| 878 |
+
flex-direction: column;
|
| 879 |
+
}
|
| 880 |
+
|
| 881 |
+
.summary-content {
|
| 882 |
+
grid-template-columns: 1fr;
|
| 883 |
+
}
|
| 884 |
+
}
|
| 885 |
+
|
| 886 |
+
@media (max-width: 480px) {
|
| 887 |
+
.header-content h1 {
|
| 888 |
+
font-size: 1.5rem;
|
| 889 |
+
}
|
| 890 |
+
|
| 891 |
+
.card {
|
| 892 |
+
padding: 1rem;
|
| 893 |
+
}
|
| 894 |
+
|
| 895 |
+
.card h2 {
|
| 896 |
+
font-size: 1.5rem;
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
.btn {
|
| 900 |
+
padding: 0.5rem 1rem;
|
| 901 |
+
font-size: 0.9rem;
|
| 902 |
+
}
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
/* Loading Animation */
|
| 906 |
+
.loading {
|
| 907 |
+
display: inline-block;
|
| 908 |
+
width: 20px;
|
| 909 |
+
height: 20px;
|
| 910 |
+
border: 3px solid #f3f3f3;
|
| 911 |
+
border-top: 3px solid #3498db;
|
| 912 |
+
border-radius: 50%;
|
| 913 |
+
animation: spin 1s linear infinite;
|
| 914 |
+
}
|
| 915 |
+
|
| 916 |
+
@keyframes spin {
|
| 917 |
+
0% { transform: rotate(0deg); }
|
| 918 |
+
100% { transform: rotate(360deg); }
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
/* Structure Preparation Styles */
|
| 922 |
+
.card-description {
|
| 923 |
+
color: #7f8c8d;
|
| 924 |
+
margin-bottom: 2rem;
|
| 925 |
+
font-style: italic;
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
.prep-sections {
|
| 929 |
+
display: grid;
|
| 930 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 931 |
+
gap: 2rem;
|
| 932 |
+
margin-bottom: 2rem;
|
| 933 |
+
}
|
| 934 |
+
|
| 935 |
+
.prep-section {
|
| 936 |
+
background: #f8f9fa;
|
| 937 |
+
border-radius: 8px;
|
| 938 |
+
padding: 1.5rem;
|
| 939 |
+
border: 1px solid #dee2e6;
|
| 940 |
+
}
|
| 941 |
+
|
| 942 |
+
.prep-section h3 {
|
| 943 |
+
color: #2c3e50;
|
| 944 |
+
margin-bottom: 1rem;
|
| 945 |
+
font-size: 1.2rem;
|
| 946 |
+
border-bottom: 1px solid #bdc3c7;
|
| 947 |
+
padding-bottom: 0.5rem;
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
+
.prep-section h3 i {
|
| 951 |
+
margin-right: 0.5rem;
|
| 952 |
+
color: #3498db;
|
| 953 |
+
}
|
| 954 |
+
|
| 955 |
+
.prep-options {
|
| 956 |
+
display: flex;
|
| 957 |
+
flex-direction: column;
|
| 958 |
+
gap: 1rem;
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
.prep-option {
|
| 962 |
+
background: white;
|
| 963 |
+
border-radius: 6px;
|
| 964 |
+
padding: 1rem;
|
| 965 |
+
border: 1px solid #e1e8ed;
|
| 966 |
+
transition: all 0.3s ease;
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
.prep-option:hover {
|
| 970 |
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
| 971 |
+
transform: translateY(-1px);
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
.checkbox-container {
|
| 975 |
+
display: flex;
|
| 976 |
+
align-items: center;
|
| 977 |
+
cursor: pointer;
|
| 978 |
+
font-weight: 600;
|
| 979 |
+
color: #2c3e50;
|
| 980 |
+
margin-bottom: 0.5rem;
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
.checkbox-container input[type="checkbox"] {
|
| 984 |
+
margin-right: 0.75rem;
|
| 985 |
+
transform: scale(1.2);
|
| 986 |
+
}
|
| 987 |
+
|
| 988 |
+
.option-description {
|
| 989 |
+
color: #7f8c8d;
|
| 990 |
+
font-size: 0.9rem;
|
| 991 |
+
margin: 0;
|
| 992 |
+
margin-left: 1.5rem;
|
| 993 |
+
}
|
| 994 |
+
|
| 995 |
+
.prep-actions {
|
| 996 |
+
display: flex;
|
| 997 |
+
gap: 1rem;
|
| 998 |
+
margin-bottom: 2rem;
|
| 999 |
+
flex-wrap: wrap;
|
| 1000 |
+
}
|
| 1001 |
+
|
| 1002 |
+
.prep-status {
|
| 1003 |
+
background: #e8f5e8;
|
| 1004 |
+
border: 1px solid #c3e6cb;
|
| 1005 |
+
border-radius: 8px;
|
| 1006 |
+
padding: 1.5rem;
|
| 1007 |
+
margin-bottom: 2rem;
|
| 1008 |
+
}
|
| 1009 |
+
|
| 1010 |
+
.prep-status h3 {
|
| 1011 |
+
color: #155724;
|
| 1012 |
+
margin-bottom: 1rem;
|
| 1013 |
+
}
|
| 1014 |
+
|
| 1015 |
+
.status-content {
|
| 1016 |
+
color: #155724;
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
.prepared-structure-preview {
|
| 1020 |
+
background: #f8f9fa;
|
| 1021 |
+
border-radius: 8px;
|
| 1022 |
+
padding: 1.5rem;
|
| 1023 |
+
border: 1px solid #dee2e6;
|
| 1024 |
+
margin-top: 2rem;
|
| 1025 |
+
}
|
| 1026 |
+
|
| 1027 |
+
.prepared-structure-preview h3 {
|
| 1028 |
+
color: #2c3e50;
|
| 1029 |
+
margin-bottom: 1rem;
|
| 1030 |
+
font-size: 1.2rem;
|
| 1031 |
+
}
|
| 1032 |
+
|
| 1033 |
+
.structure-info p {
|
| 1034 |
+
margin: 0.5rem 0;
|
| 1035 |
+
font-size: 1rem;
|
| 1036 |
+
}
|
| 1037 |
+
|
| 1038 |
+
.structure-visualization {
|
| 1039 |
+
margin-top: 1rem;
|
| 1040 |
+
}
|
| 1041 |
+
|
| 1042 |
+
#prepared-ngl-viewer {
|
| 1043 |
+
border-radius: 4px;
|
| 1044 |
+
background: #f8f9fa;
|
| 1045 |
+
border: 1px solid #dee2e6;
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
/* Custom Checkbox Styles */
|
| 1049 |
+
.checkbox-container input[type="checkbox"] {
|
| 1050 |
+
appearance: none;
|
| 1051 |
+
width: 20px;
|
| 1052 |
+
height: 20px;
|
| 1053 |
+
border: 2px solid #bdc3c7;
|
| 1054 |
+
border-radius: 4px;
|
| 1055 |
+
background: white;
|
| 1056 |
+
position: relative;
|
| 1057 |
+
cursor: pointer;
|
| 1058 |
+
transition: all 0.3s ease;
|
| 1059 |
+
}
|
| 1060 |
+
|
| 1061 |
+
.checkbox-container input[type="checkbox"]:checked {
|
| 1062 |
+
background: #3498db;
|
| 1063 |
+
border-color: #3498db;
|
| 1064 |
+
}
|
| 1065 |
+
|
| 1066 |
+
.checkbox-container input[type="checkbox"]:checked::after {
|
| 1067 |
+
content: '✓';
|
| 1068 |
+
position: absolute;
|
| 1069 |
+
top: 50%;
|
| 1070 |
+
left: 50%;
|
| 1071 |
+
transform: translate(-50%, -50%);
|
| 1072 |
+
color: white;
|
| 1073 |
+
font-weight: bold;
|
| 1074 |
+
font-size: 14px;
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
.checkbox-container input[type="checkbox"]:hover {
|
| 1078 |
+
border-color: #3498db;
|
| 1079 |
+
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
| 1080 |
+
}
|
| 1081 |
+
|
| 1082 |
+
/* Responsive Design for Structure Prep */
|
| 1083 |
+
@media (max-width: 768px) {
|
| 1084 |
+
.prep-sections {
|
| 1085 |
+
grid-template-columns: 1fr;
|
| 1086 |
+
}
|
| 1087 |
+
|
| 1088 |
+
.prep-actions {
|
| 1089 |
+
flex-direction: column;
|
| 1090 |
+
}
|
| 1091 |
+
|
| 1092 |
+
.prep-option {
|
| 1093 |
+
padding: 0.75rem;
|
| 1094 |
+
}
|
| 1095 |
+
|
| 1096 |
+
.option-description {
|
| 1097 |
+
margin-left: 1.25rem;
|
| 1098 |
+
}
|
| 1099 |
+
}
|
| 1100 |
+
|
| 1101 |
+
/* Utility Classes */
|
| 1102 |
+
.text-center { text-align: center; }
|
| 1103 |
+
.text-left { text-align: left; }
|
| 1104 |
+
.text-right { text-align: right; }
|
| 1105 |
+
.mt-1 { margin-top: 0.5rem; }
|
| 1106 |
+
.mt-2 { margin-top: 1rem; }
|
| 1107 |
+
.mt-3 { margin-top: 1.5rem; }
|
| 1108 |
+
.mb-1 { margin-bottom: 0.5rem; }
|
| 1109 |
+
.mb-2 { margin-bottom: 1rem; }
|
| 1110 |
+
.mb-3 { margin-bottom: 1.5rem; }
|
| 1111 |
+
.p-1 { padding: 0.5rem; }
|
| 1112 |
+
.p-2 { padding: 1rem; }
|
| 1113 |
+
.p-3 { padding: 1.5rem; }
|
html/index.html
ADDED
|
@@ -0,0 +1,587 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
| 9 |
+
<!-- NGL 3D Molecular Viewer -->
|
| 10 |
+
<script src="https://unpkg.com/ngl@2.0.0-dev.35/dist/ngl.js"></script>
|
| 11 |
+
</head>
|
| 12 |
+
<body>
|
| 13 |
+
<div class="container">
|
| 14 |
+
<!-- Header -->
|
| 15 |
+
<header class="header">
|
| 16 |
+
<div class="header-content">
|
| 17 |
+
<h1><i class="fas fa-atom"></i> MD Simulation Pipeline</h1>
|
| 18 |
+
<p>Molecular Dynamics Simulation Setup and File Generation</p>
|
| 19 |
+
</div>
|
| 20 |
+
</header>
|
| 21 |
+
|
| 22 |
+
<!-- Navigation Tabs -->
|
| 23 |
+
<nav class="tab-navigation">
|
| 24 |
+
<button class="tab-button active" data-tab="protein-loading">
|
| 25 |
+
<i class="fas fa-upload"></i> Protein Loading
|
| 26 |
+
</button>
|
| 27 |
+
<button class="tab-button" data-tab="structure-prep">
|
| 28 |
+
<i class="fas fa-tools"></i> Structure Preparation
|
| 29 |
+
</button>
|
| 30 |
+
<button class="tab-button" data-tab="simulation-params">
|
| 31 |
+
<i class="fas fa-cogs"></i> Simulation Parameters
|
| 32 |
+
</button>
|
| 33 |
+
<button class="tab-button" data-tab="simulation-steps">
|
| 34 |
+
<i class="fas fa-list-ol"></i> Simulation Steps
|
| 35 |
+
</button>
|
| 36 |
+
<button class="tab-button" data-tab="file-generation">
|
| 37 |
+
<i class="fas fa-file-download"></i> Generate Files
|
| 38 |
+
</button>
|
| 39 |
+
</nav>
|
| 40 |
+
|
| 41 |
+
<!-- Main Content -->
|
| 42 |
+
<main class="main-content">
|
| 43 |
+
<!-- Protein Loading Tab -->
|
| 44 |
+
<div id="protein-loading" class="tab-content active">
|
| 45 |
+
<div class="card">
|
| 46 |
+
<h2><i class="fas fa-dna"></i> Protein Structure Input</h2>
|
| 47 |
+
|
| 48 |
+
<div class="input-methods">
|
| 49 |
+
<div class="method-option">
|
| 50 |
+
<h3><i class="fas fa-file-upload"></i> Upload PDB File</h3>
|
| 51 |
+
<div class="file-upload-area" id="file-upload-area">
|
| 52 |
+
<i class="fas fa-cloud-upload-alt"></i>
|
| 53 |
+
<p>Drag and drop your PDB file here or click to browse</p>
|
| 54 |
+
<input type="file" id="pdb-file" accept=".pdb,.ent" style="display: none;">
|
| 55 |
+
<button type="button" class="btn btn-secondary" id="choose-file-btn">
|
| 56 |
+
Choose File
|
| 57 |
+
</button>
|
| 58 |
+
</div>
|
| 59 |
+
<div id="file-info" class="file-info" style="display: none;">
|
| 60 |
+
<p><strong>Selected file:</strong> <span id="file-name"></span></p>
|
| 61 |
+
<p><strong>Size:</strong> <span id="file-size"></span></p>
|
| 62 |
+
</div>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
<div class="divider">
|
| 66 |
+
<span>OR</span>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<div class="method-option">
|
| 70 |
+
<h3><i class="fas fa-database"></i> Fetch from PDB</h3>
|
| 71 |
+
<div class="pdb-fetch">
|
| 72 |
+
<div class="input-group">
|
| 73 |
+
<label for="pdb-id">PDB ID:</label>
|
| 74 |
+
<input type="text" id="pdb-id" placeholder="e.g., 1CRN, 1HTM" maxlength="4">
|
| 75 |
+
<button type="button" class="btn btn-primary" id="fetch-pdb">
|
| 76 |
+
<i class="fas fa-download"></i> Fetch
|
| 77 |
+
</button>
|
| 78 |
+
</div>
|
| 79 |
+
<div id="pdb-status" class="status-message"></div>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
<div class="protein-preview" id="protein-preview" style="display: none;">
|
| 85 |
+
<h3><i class="fas fa-eye"></i> Protein Preview</h3>
|
| 86 |
+
<div class="preview-content">
|
| 87 |
+
<div class="protein-info">
|
| 88 |
+
<p><strong>Structure ID:</strong> <span id="structure-id"></span></p>
|
| 89 |
+
<p><strong>Number of atoms:</strong> <span id="atom-count"></span></p>
|
| 90 |
+
<p><strong>Chains:</strong> <span id="chain-info"></span></p>
|
| 91 |
+
<p><strong>Residues:</strong> <span id="residue-count"></span></p>
|
| 92 |
+
<p><strong>Water molecules:</strong> <span id="water-count"></span></p>
|
| 93 |
+
<p><strong>Ions:</strong> <span id="ion-count"></span></p>
|
| 94 |
+
<p><strong>Ligands:</strong> <span id="ligand-info"></span></p>
|
| 95 |
+
<p><strong>HETATM entries:</strong> <span id="hetatm-count"></span></p>
|
| 96 |
+
</div>
|
| 97 |
+
<div class="protein-visualization">
|
| 98 |
+
<div id="molecule-viewer" class="molecule-viewer">
|
| 99 |
+
<!-- 3D visualization will be added here -->
|
| 100 |
+
<div id="ngl-viewer" style="width: 100%; height: 100%; min-height: 300px;"></div>
|
| 101 |
+
<div id="viewer-controls" class="viewer-controls" style="display: none;">
|
| 102 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.resetView()">
|
| 103 |
+
<i class="fas fa-home"></i> Reset View
|
| 104 |
+
</button>
|
| 105 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleRepresentation()">
|
| 106 |
+
<i class="fas fa-eye"></i> <span id="style-text">Mixed View</span>
|
| 107 |
+
</button>
|
| 108 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.toggleSpin()">
|
| 109 |
+
<i class="fas fa-sync"></i> Spin
|
| 110 |
+
</button>
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
</div>
|
| 118 |
+
|
| 119 |
+
<!-- Structure Preparation Tab -->
|
| 120 |
+
<div id="structure-prep" class="tab-content">
|
| 121 |
+
<div class="card">
|
| 122 |
+
<h2><i class="fas fa-tools"></i> Structure Preparation for AMBER</h2>
|
| 123 |
+
<p class="card-description">Prepare the protein structure for AMBER force field generation by cleaning and modifying the PDB file.</p>
|
| 124 |
+
|
| 125 |
+
<div class="prep-sections">
|
| 126 |
+
<div class="prep-section">
|
| 127 |
+
<h3><i class="fas fa-trash"></i> Remove Components</h3>
|
| 128 |
+
<div class="prep-options">
|
| 129 |
+
<div class="prep-option">
|
| 130 |
+
<label class="checkbox-container">
|
| 131 |
+
<input type="checkbox" id="remove-water" checked>
|
| 132 |
+
<span class="checkmark"></span>
|
| 133 |
+
Remove water molecules
|
| 134 |
+
</label>
|
| 135 |
+
<p class="option-description">Remove all water molecules (HOH, WAT, TIP3, etc.) from the structure</p>
|
| 136 |
+
</div>
|
| 137 |
+
|
| 138 |
+
<div class="prep-option">
|
| 139 |
+
<label class="checkbox-container">
|
| 140 |
+
<input type="checkbox" id="remove-ions" checked>
|
| 141 |
+
<span class="checkmark"></span>
|
| 142 |
+
Remove ions
|
| 143 |
+
</label>
|
| 144 |
+
<p class="option-description">Remove all ions (Na+, Cl-, K+, Mg2+, etc.) from the structure</p>
|
| 145 |
+
</div>
|
| 146 |
+
|
| 147 |
+
<div class="prep-option">
|
| 148 |
+
<label class="checkbox-container">
|
| 149 |
+
<input type="checkbox" id="remove-hydrogens" checked>
|
| 150 |
+
<span class="checkmark"></span>
|
| 151 |
+
Remove hydrogen atoms
|
| 152 |
+
</label>
|
| 153 |
+
<p class="option-description">Remove all hydrogen atoms from the protein structure</p>
|
| 154 |
+
</div>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
|
| 158 |
+
<div class="prep-section">
|
| 159 |
+
<h3><i class="fas fa-plus-circle"></i> Add Capping Groups and Select Protein Chains</h3>
|
| 160 |
+
<div class="prep-options">
|
| 161 |
+
<div class="prep-option">
|
| 162 |
+
<label class="checkbox-container">
|
| 163 |
+
<input type="checkbox" id="add-nme" checked>
|
| 164 |
+
<span class="checkmark"></span>
|
| 165 |
+
Add NME group (C-terminal)
|
| 166 |
+
</label>
|
| 167 |
+
<p class="option-description">Add N-methyl amide (NME) group to C-terminal residues</p>
|
| 168 |
+
</div>
|
| 169 |
+
|
| 170 |
+
<div class="prep-option">
|
| 171 |
+
<label class="checkbox-container">
|
| 172 |
+
<input type="checkbox" id="add-ace" checked>
|
| 173 |
+
<span class="checkmark"></span>
|
| 174 |
+
Add ACE group (N-terminal)
|
| 175 |
+
</label>
|
| 176 |
+
<p class="option-description">Add acetyl (ACE) group to N-terminal residues</p>
|
| 177 |
+
</div>
|
| 178 |
+
|
| 179 |
+
<div class="form-group">
|
| 180 |
+
<label>Preserve Chains for FF Generation:</label>
|
| 181 |
+
<div id="chain-selection" class="multi-checkbox-group">
|
| 182 |
+
<!-- Chain checkboxes will be rendered here -->
|
| 183 |
+
</div>
|
| 184 |
+
<small class="form-help">Select one or more protein chains to include in preparation</small>
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<div class="prep-section">
|
| 190 |
+
<h3><i class="fas fa-pills"></i> Ligand Handling</h3>
|
| 191 |
+
<div class="prep-options">
|
| 192 |
+
<div class="prep-option">
|
| 193 |
+
<label class="checkbox-container">
|
| 194 |
+
<input type="checkbox" id="preserve-ligands">
|
| 195 |
+
<span class="checkmark"></span>
|
| 196 |
+
Preserve ligands
|
| 197 |
+
</label>
|
| 198 |
+
<p class="option-description">Keep ligands in the structure and append them at the end</p>
|
| 199 |
+
</div>
|
| 200 |
+
|
| 201 |
+
<div class="prep-option">
|
| 202 |
+
<div class="checkbox-with-button">
|
| 203 |
+
<label class="checkbox-container">
|
| 204 |
+
<input type="checkbox" id="separate-ligands">
|
| 205 |
+
<span class="checkmark"></span>
|
| 206 |
+
Create separate ligand file
|
| 207 |
+
</label>
|
| 208 |
+
<button class="btn btn-sm btn-outline-primary" id="download-ligand" disabled>
|
| 209 |
+
<i class="fas fa-download"></i>
|
| 210 |
+
</button>
|
| 211 |
+
</div>
|
| 212 |
+
<p class="option-description">Extract ligands to a separate PDB file for individual processing</p>
|
| 213 |
+
</div>
|
| 214 |
+
|
| 215 |
+
<div class="form-group">
|
| 216 |
+
<label>Select Ligands to Preserve</label>
|
| 217 |
+
<div id="ligand-selection" class="multi-checkbox-group">
|
| 218 |
+
<!-- Ligand checkboxes will be rendered here -->
|
| 219 |
+
</div>
|
| 220 |
+
<small class="form-help">Tick ligands to include. Unselected ligands will be ignored.</small>
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
</div>
|
| 224 |
+
|
| 225 |
+
</div>
|
| 226 |
+
|
| 227 |
+
<div class="prep-actions">
|
| 228 |
+
<button class="btn btn-primary" id="prepare-structure">
|
| 229 |
+
<i class="fas fa-magic"></i> Prepare Structure
|
| 230 |
+
</button>
|
| 231 |
+
<button class="btn btn-secondary" id="preview-prepared">
|
| 232 |
+
<i class="fas fa-eye"></i> Preview Prepared Structure
|
| 233 |
+
</button>
|
| 234 |
+
<button class="btn btn-info" id="download-prepared">
|
| 235 |
+
<i class="fas fa-download"></i> Download Prepared PDB
|
| 236 |
+
</button>
|
| 237 |
+
</div>
|
| 238 |
+
|
| 239 |
+
<div class="prep-status" id="prep-status" style="display: none;">
|
| 240 |
+
<h3><i class="fas fa-info-circle"></i> Preparation Status</h3>
|
| 241 |
+
<div class="status-content" id="prep-status-content">
|
| 242 |
+
<!-- Status information will be displayed here -->
|
| 243 |
+
</div>
|
| 244 |
+
</div>
|
| 245 |
+
|
| 246 |
+
<div class="prepared-structure-preview" id="prepared-structure-preview" style="display: none;">
|
| 247 |
+
<h3><i class="fas fa-eye"></i> Prepared Structure Preview</h3>
|
| 248 |
+
<div class="preview-content">
|
| 249 |
+
<div class="structure-info">
|
| 250 |
+
<p><strong>Original atoms:</strong> <span id="original-atoms"></span></p>
|
| 251 |
+
<p><strong>Prepared atoms:</strong> <span id="prepared-atoms"></span></p>
|
| 252 |
+
<p><strong>Removed components:</strong> <span id="removed-components"></span></p>
|
| 253 |
+
<p><strong>Added capping groups:</strong> <span id="added-capping"></span></p>
|
| 254 |
+
<p><strong>Ligands preserved:</strong> <span id="preserved-ligands"></span></p>
|
| 255 |
+
</div>
|
| 256 |
+
<div class="structure-visualization">
|
| 257 |
+
<div id="prepared-molecule-viewer" class="molecule-viewer">
|
| 258 |
+
<div id="prepared-ngl-viewer" style="width: 100%; height: 100%; min-height: 300px;"></div>
|
| 259 |
+
<div id="prepared-viewer-controls" class="viewer-controls" style="display: none;">
|
| 260 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.resetPreparedView()">
|
| 261 |
+
<i class="fas fa-home"></i> Reset View
|
| 262 |
+
</button>
|
| 263 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.togglePreparedRepresentation()">
|
| 264 |
+
<i class="fas fa-eye"></i> <span id="prepared-style-text">Mixed View</span>
|
| 265 |
+
</button>
|
| 266 |
+
<button class="btn btn-sm btn-secondary" onclick="mdPipeline.togglePreparedSpin()">
|
| 267 |
+
<i class="fas fa-sync"></i> Spin
|
| 268 |
+
</button>
|
| 269 |
+
</div>
|
| 270 |
+
</div>
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
</div>
|
| 274 |
+
</div>
|
| 275 |
+
</div>
|
| 276 |
+
|
| 277 |
+
<!-- Simulation Parameters Tab -->
|
| 278 |
+
<div id="simulation-params" class="tab-content">
|
| 279 |
+
<div class="card">
|
| 280 |
+
<h2><i class="fas fa-sliders-h"></i> Simulation Parameters</h2>
|
| 281 |
+
|
| 282 |
+
<div class="params-grid">
|
| 283 |
+
<div class="param-section">
|
| 284 |
+
<h3><i class="fas fa-cube"></i> System Setup</h3>
|
| 285 |
+
<div class="form-group">
|
| 286 |
+
<label for="box-type">Box Type:</label>
|
| 287 |
+
<select id="box-type">
|
| 288 |
+
<option value="cuboid">Cuboid</option>
|
| 289 |
+
</select>
|
| 290 |
+
</div>
|
| 291 |
+
<div class="form-group">
|
| 292 |
+
<label for="box-size">
|
| 293 |
+
Distance (Å):
|
| 294 |
+
<i class="fas fa-info-circle"
|
| 295 |
+
style="color: #007bff; margin-left: 5px; cursor: help;"
|
| 296 |
+
data-toggle="tooltip"
|
| 297 |
+
data-placement="top"
|
| 298 |
+
data-html="true"
|
| 299 |
+
title="The minimum distance between any atom originally present in solute and the edge of the periodic box is given by the distance parameter.">
|
| 300 |
+
</i>
|
| 301 |
+
</label>
|
| 302 |
+
<input type="number" id="box-size" value="10" step="1" min="5">
|
| 303 |
+
</div>
|
| 304 |
+
</div>
|
| 305 |
+
|
| 306 |
+
<div class="param-section" id="ligand-forcefield-section" style="display: none;">
|
| 307 |
+
<h3><i class="fas fa-atom"></i> Ligand Force Field</h3>
|
| 308 |
+
<div class="form-group">
|
| 309 |
+
<label for="ligand-forcefield">Ligand Force Field:</label>
|
| 310 |
+
<select id="ligand-forcefield">
|
| 311 |
+
<option value="gaff2">gaff2</option>
|
| 312 |
+
<option value="gaff">gaff</option>
|
| 313 |
+
</select>
|
| 314 |
+
</div>
|
| 315 |
+
<div class="form-group">
|
| 316 |
+
<button type="button" class="btn btn-primary" onclick="mdPipeline.generateLigandFF(event)">
|
| 317 |
+
<i class="fas fa-cogs"></i> Generate FF for Ligand
|
| 318 |
+
</button>
|
| 319 |
+
</div>
|
| 320 |
+
</div>
|
| 321 |
+
|
| 322 |
+
<div class="param-section">
|
| 323 |
+
<h3><i class="fas fa-flask"></i> Force Field & Water Model</h3>
|
| 324 |
+
<div class="form-group">
|
| 325 |
+
<label for="force-field">Protein Force Field:</label>
|
| 326 |
+
<select id="force-field">
|
| 327 |
+
<option value="ff14SB">ff14SB</option>
|
| 328 |
+
<option value="ff19SB">ff19SB</option>
|
| 329 |
+
</select>
|
| 330 |
+
</div>
|
| 331 |
+
<div class="form-group">
|
| 332 |
+
<label for="water-model">Water Model:</label>
|
| 333 |
+
<select id="water-model">
|
| 334 |
+
<option value="tip3p">TIP3P</option>
|
| 335 |
+
<option value="spce">SPCE</option>
|
| 336 |
+
</select>
|
| 337 |
+
</div>
|
| 338 |
+
<div class="form-group">
|
| 339 |
+
<label for="add-ions">Add Ions:</label>
|
| 340 |
+
<div class="ion-controls">
|
| 341 |
+
<select id="add-ions">
|
| 342 |
+
<option value="None">None</option>
|
| 343 |
+
<option value="Na+">Na+</option>
|
| 344 |
+
<option value="Cl-">Cl-</option>
|
| 345 |
+
</select>
|
| 346 |
+
<button type="button" class="btn btn-sm btn-outline-primary" onclick="mdPipeline.calculateNetCharge(event)">
|
| 347 |
+
<i class="fas fa-calculator"></i> Net Charge
|
| 348 |
+
</button>
|
| 349 |
+
</div>
|
| 350 |
+
</div>
|
| 351 |
+
</div>
|
| 352 |
+
|
| 353 |
+
<div class="param-section">
|
| 354 |
+
<h3><i class="fas fa-thermometer-half"></i> Temperature & Pressure</h3>
|
| 355 |
+
<div class="form-group">
|
| 356 |
+
<label for="temperature">Temperature (K):</label>
|
| 357 |
+
<input type="number" id="temperature" value="300" step="5" min="200" max="400">
|
| 358 |
+
</div>
|
| 359 |
+
<div class="form-group">
|
| 360 |
+
<label for="pressure">Pressure (atm):</label>
|
| 361 |
+
<input type="number" id="pressure" value="1.0" step="0.1" min="0.1">
|
| 362 |
+
</div>
|
| 363 |
+
<div class="form-group">
|
| 364 |
+
<label for="coupling-type">Thermostat:</label>
|
| 365 |
+
<select id="coupling-type">
|
| 366 |
+
<option value="langevin">Langevin</option>
|
| 367 |
+
</select>
|
| 368 |
+
</div>
|
| 369 |
+
</div>
|
| 370 |
+
|
| 371 |
+
<div class="param-section">
|
| 372 |
+
<h3><i class="fas fa-clock"></i> Integration Parameters</h3>
|
| 373 |
+
<div class="form-group">
|
| 374 |
+
<label for="timestep">Time Step (ps):</label>
|
| 375 |
+
<input type="number" id="timestep" value="0.002" step="0.001" min="0.001" max="0.005">
|
| 376 |
+
</div>
|
| 377 |
+
<div class="form-group">
|
| 378 |
+
<label for="cutoff">Cutoff Distance (Ang):</label>
|
| 379 |
+
<input type="number" id="cutoff" value="10.0" step="1" min="8" max="20">
|
| 380 |
+
</div>
|
| 381 |
+
<div class="form-group">
|
| 382 |
+
<label for="electrostatic">Electrostatic:</label>
|
| 383 |
+
<select id="electrostatic">
|
| 384 |
+
<option value="pme">PME</option>
|
| 385 |
+
</select>
|
| 386 |
+
</div>
|
| 387 |
+
</div>
|
| 388 |
+
|
| 389 |
+
</div>
|
| 390 |
+
</div>
|
| 391 |
+
</div>
|
| 392 |
+
|
| 393 |
+
<!-- Simulation Steps Tab -->
|
| 394 |
+
<div id="simulation-steps" class="tab-content">
|
| 395 |
+
<div class="card">
|
| 396 |
+
<h2><i class="fas fa-list-ol"></i> Simulation Steps Configuration</h2>
|
| 397 |
+
|
| 398 |
+
<div class="steps-container">
|
| 399 |
+
<div class="step-item">
|
| 400 |
+
<div class="step-header">
|
| 401 |
+
<h3><i class="fas fa-lock"></i> Restrained Minimization</h3>
|
| 402 |
+
<label class="switch">
|
| 403 |
+
<input type="checkbox" id="enable-restrained-min" checked>
|
| 404 |
+
<span class="slider"></span>
|
| 405 |
+
</label>
|
| 406 |
+
</div>
|
| 407 |
+
<div class="step-content" id="restrained-min-content">
|
| 408 |
+
<div class="form-row">
|
| 409 |
+
<div class="form-group">
|
| 410 |
+
<label for="restrained-steps">Steps:</label>
|
| 411 |
+
<input type="number" id="restrained-steps" value="10000" step="100" min="100">
|
| 412 |
+
</div>
|
| 413 |
+
<div class="form-group">
|
| 414 |
+
<label for="restrained-force">Force Constant (kJ/mol/Ų):</label>
|
| 415 |
+
<input type="number" id="restrained-force" value="10" step="1" min="1">
|
| 416 |
+
</div>
|
| 417 |
+
</div>
|
| 418 |
+
</div>
|
| 419 |
+
</div>
|
| 420 |
+
|
| 421 |
+
<div class="step-item">
|
| 422 |
+
<div class="step-header">
|
| 423 |
+
<h3><i class="fas fa-compress"></i> Minimization</h3>
|
| 424 |
+
<label class="switch">
|
| 425 |
+
<input type="checkbox" id="enable-minimization" checked>
|
| 426 |
+
<span class="slider"></span>
|
| 427 |
+
</label>
|
| 428 |
+
</div>
|
| 429 |
+
<div class="step-content" id="minimization-content">
|
| 430 |
+
<div class="form-row">
|
| 431 |
+
<div class="form-group">
|
| 432 |
+
<label for="min-steps">Steps:</label>
|
| 433 |
+
<input type="number" id="min-steps" value="20000" step="500" min="1000">
|
| 434 |
+
</div>
|
| 435 |
+
<div class="form-group">
|
| 436 |
+
<label for="min-algorithm">Algorithm:</label>
|
| 437 |
+
<select id="min-algorithm">
|
| 438 |
+
<option value="cg">Conjugate Gradient</option>
|
| 439 |
+
</select>
|
| 440 |
+
</div>
|
| 441 |
+
</div>
|
| 442 |
+
</div>
|
| 443 |
+
</div>
|
| 444 |
+
|
| 445 |
+
<div class="step-item">
|
| 446 |
+
<div class="step-header">
|
| 447 |
+
<h3><i class="fas fa-fire"></i> NPT Heating</h3>
|
| 448 |
+
<label class="switch">
|
| 449 |
+
<input type="checkbox" id="enable-nvt" checked>
|
| 450 |
+
<span class="slider"></span>
|
| 451 |
+
</label>
|
| 452 |
+
</div>
|
| 453 |
+
<div class="step-content" id="nvt-content">
|
| 454 |
+
<div class="form-row">
|
| 455 |
+
<div class="form-group">
|
| 456 |
+
<label for="nvt-steps">Steps:</label>
|
| 457 |
+
<input type="number" id="nvt-steps" value="50000" step="5000" min="10000">
|
| 458 |
+
</div>
|
| 459 |
+
<div class="form-group">
|
| 460 |
+
<label for="nvt-temp">Target Temperature (K):</label>
|
| 461 |
+
<input type="number" id="nvt-temp" value="300" step="5" min="200">
|
| 462 |
+
</div>
|
| 463 |
+
</div>
|
| 464 |
+
</div>
|
| 465 |
+
</div>
|
| 466 |
+
|
| 467 |
+
<div class="step-item">
|
| 468 |
+
<div class="step-header">
|
| 469 |
+
<h3><i class="fas fa-compress-arrows-alt"></i> NPT Equilibration</h3>
|
| 470 |
+
<label class="switch">
|
| 471 |
+
<input type="checkbox" id="enable-npt" checked>
|
| 472 |
+
<span class="slider"></span>
|
| 473 |
+
</label>
|
| 474 |
+
</div>
|
| 475 |
+
<div class="step-content" id="npt-content">
|
| 476 |
+
<div class="form-row">
|
| 477 |
+
<div class="form-group">
|
| 478 |
+
<label for="npt-steps">Steps:</label>
|
| 479 |
+
<input type="number" id="npt-steps" value="100000" step="10000" min="20000">
|
| 480 |
+
</div>
|
| 481 |
+
<div class="form-group">
|
| 482 |
+
<label for="npt-temp">Temperature (K):</label>
|
| 483 |
+
<input type="number" id="npt-temp" value="300" step="5" min="200">
|
| 484 |
+
</div>
|
| 485 |
+
<div class="form-group">
|
| 486 |
+
<label for="npt-pressure">Pressure (atm):</label>
|
| 487 |
+
<input type="number" id="npt-pressure" value="1.0" step="0.1" min="0.1">
|
| 488 |
+
</div>
|
| 489 |
+
</div>
|
| 490 |
+
</div>
|
| 491 |
+
</div>
|
| 492 |
+
|
| 493 |
+
<div class="step-item">
|
| 494 |
+
<div class="step-header">
|
| 495 |
+
<h3><i class="fas fa-play"></i> Production Run(NPT)</h3>
|
| 496 |
+
<label class="switch">
|
| 497 |
+
<input type="checkbox" id="enable-production" checked>
|
| 498 |
+
<span class="slider"></span>
|
| 499 |
+
</label>
|
| 500 |
+
</div>
|
| 501 |
+
<div class="step-content" id="production-content">
|
| 502 |
+
<div class="form-row">
|
| 503 |
+
<div class="form-group">
|
| 504 |
+
<label for="prod-steps">Steps:</label>
|
| 505 |
+
<input type="number" id="prod-steps" value="1000000" step="100000" min="100000">
|
| 506 |
+
</div>
|
| 507 |
+
<div class="form-group">
|
| 508 |
+
<label for="prod-temp">Temperature (K):</label>
|
| 509 |
+
<input type="number" id="prod-temp" value="300" step="5" min="200">
|
| 510 |
+
</div>
|
| 511 |
+
<div class="form-group">
|
| 512 |
+
<label for="prod-pressure">Pressure (atm):</label>
|
| 513 |
+
<input type="number" id="prod-pressure" value="1.0" step="0.1" min="0.1">
|
| 514 |
+
</div>
|
| 515 |
+
</div>
|
| 516 |
+
</div>
|
| 517 |
+
</div>
|
| 518 |
+
</div>
|
| 519 |
+
</div>
|
| 520 |
+
</div>
|
| 521 |
+
|
| 522 |
+
<!-- File Generation Tab -->
|
| 523 |
+
<div id="file-generation" class="tab-content">
|
| 524 |
+
<div class="card">
|
| 525 |
+
<h2><i class="fas fa-file-download"></i> Generate Simulation Files</h2>
|
| 526 |
+
|
| 527 |
+
<div class="generation-controls">
|
| 528 |
+
<button class="btn btn-primary" id="generate-files">
|
| 529 |
+
<i class="fas fa-magic"></i> Generate All Files
|
| 530 |
+
</button>
|
| 531 |
+
<button class="btn btn-secondary" id="preview-files">
|
| 532 |
+
<i class="fas fa-eye"></i> Preview Files
|
| 533 |
+
</button>
|
| 534 |
+
<button class="btn btn-info" id="preview-solvated">
|
| 535 |
+
<i class="fas fa-tint"></i> Preview Solvated Protein
|
| 536 |
+
</button>
|
| 537 |
+
</div>
|
| 538 |
+
|
| 539 |
+
<div class="files-preview" id="files-preview" style="display: none;">
|
| 540 |
+
<h3><i class="fas fa-files"></i> Generated Files</h3>
|
| 541 |
+
<div class="files-list" id="files-list">
|
| 542 |
+
<!-- Generated files will be listed here -->
|
| 543 |
+
</div>
|
| 544 |
+
</div>
|
| 545 |
+
|
| 546 |
+
<div class="download-section" id="download-section" style="display: none;">
|
| 547 |
+
<h3><i class="fas fa-download"></i> Download Files</h3>
|
| 548 |
+
<div class="download-options">
|
| 549 |
+
<button class="btn btn-success" id="download-zip">
|
| 550 |
+
<i class="fas fa-file-archive"></i> Download All as ZIP
|
| 551 |
+
</button>
|
| 552 |
+
|
| 553 |
+
</div>
|
| 554 |
+
</div>
|
| 555 |
+
|
| 556 |
+
<div class="simulation-summary" id="simulation-summary" style="display: none;">
|
| 557 |
+
<h3><i class="fas fa-chart-line"></i> Simulation Summary</h3>
|
| 558 |
+
<div class="summary-content" id="summary-content">
|
| 559 |
+
<!-- Simulation summary will be displayed here -->
|
| 560 |
+
</div>
|
| 561 |
+
</div>
|
| 562 |
+
</div>
|
| 563 |
+
</div>
|
| 564 |
+
</main>
|
| 565 |
+
|
| 566 |
+
<!-- Step Navigation Controls -->
|
| 567 |
+
<div class="step-navigation">
|
| 568 |
+
<button class="nav-btn prev-btn" id="prev-tab" disabled>
|
| 569 |
+
<i class="fas fa-chevron-left"></i> Previous
|
| 570 |
+
</button>
|
| 571 |
+
<div class="step-indicator">
|
| 572 |
+
<span id="current-step">1</span> of <span id="total-steps">5</span>
|
| 573 |
+
</div>
|
| 574 |
+
<button class="nav-btn next-btn" id="next-tab">
|
| 575 |
+
Next <i class="fas fa-chevron-right"></i>
|
| 576 |
+
</button>
|
| 577 |
+
</div>
|
| 578 |
+
|
| 579 |
+
<!-- Footer -->
|
| 580 |
+
<footer class="footer">
|
| 581 |
+
<p>© 2024 MD Simulation Pipeline. Built for molecular dynamics simulations.</p>
|
| 582 |
+
</footer>
|
| 583 |
+
</div>
|
| 584 |
+
|
| 585 |
+
<script src="../js/script.js"></script>
|
| 586 |
+
</body>
|
| 587 |
+
</html>
|
js/script.js
ADDED
|
@@ -0,0 +1,2216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// MD Simulation Pipeline JavaScript
|
| 2 |
+
console.log('Script loading...'); // Debug log
|
| 3 |
+
|
| 4 |
+
class MDSimulationPipeline {
|
| 5 |
+
constructor() {
|
| 6 |
+
this.currentProtein = null;
|
| 7 |
+
this.preparedProtein = null;
|
| 8 |
+
this.simulationParams = {};
|
| 9 |
+
this.generatedFiles = {};
|
| 10 |
+
this.nglStage = null;
|
| 11 |
+
this.preparedNglStage = null;
|
| 12 |
+
this.currentRepresentation = 'cartoon';
|
| 13 |
+
this.preparedRepresentation = 'cartoon';
|
| 14 |
+
this.isSpinning = false;
|
| 15 |
+
this.preparedIsSpinning = false;
|
| 16 |
+
this.currentTabIndex = 0;
|
| 17 |
+
this.tabOrder = ['protein-loading', 'structure-prep', 'simulation-params', 'simulation-steps', 'file-generation'];
|
| 18 |
+
this.init();
|
| 19 |
+
this.initializeTooltips();
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
init() {
|
| 23 |
+
this.setupEventListeners();
|
| 24 |
+
this.initializeTabs();
|
| 25 |
+
this.initializeStepToggles();
|
| 26 |
+
this.loadDefaultParams();
|
| 27 |
+
this.updateNavigationState();
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
initializeTooltips() {
|
| 31 |
+
// Initialize Bootstrap tooltips using vanilla JavaScript
|
| 32 |
+
// Note: This requires Bootstrap to be loaded
|
| 33 |
+
if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
|
| 34 |
+
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-toggle="tooltip"]'));
|
| 35 |
+
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
| 36 |
+
return new bootstrap.Tooltip(tooltipTriggerEl);
|
| 37 |
+
});
|
| 38 |
+
} else {
|
| 39 |
+
console.log('Bootstrap not loaded, tooltips will not work');
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
setupEventListeners() {
|
| 44 |
+
// Tab navigation
|
| 45 |
+
document.querySelectorAll('.tab-button').forEach(button => {
|
| 46 |
+
button.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab));
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
// File upload
|
| 50 |
+
const fileInput = document.getElementById('pdb-file');
|
| 51 |
+
const fileUploadArea = document.getElementById('file-upload-area');
|
| 52 |
+
const chooseFileBtn = document.getElementById('choose-file-btn');
|
| 53 |
+
|
| 54 |
+
console.log('File input element:', fileInput);
|
| 55 |
+
console.log('File upload area:', fileUploadArea);
|
| 56 |
+
console.log('Choose file button:', chooseFileBtn);
|
| 57 |
+
|
| 58 |
+
if (!fileInput) {
|
| 59 |
+
console.error('File input element not found!');
|
| 60 |
+
return;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
fileInput.addEventListener('change', (e) => this.handleFileUpload(e));
|
| 64 |
+
|
| 65 |
+
// Handle click on upload area (but not on the button)
|
| 66 |
+
fileUploadArea.addEventListener('click', (e) => {
|
| 67 |
+
// Only trigger if not clicking on the button
|
| 68 |
+
if (e.target !== chooseFileBtn && !chooseFileBtn.contains(e.target)) {
|
| 69 |
+
console.log('Upload area clicked, triggering file input');
|
| 70 |
+
fileInput.click();
|
| 71 |
+
}
|
| 72 |
+
});
|
| 73 |
+
|
| 74 |
+
// Handle click on choose file button
|
| 75 |
+
chooseFileBtn.addEventListener('click', (e) => {
|
| 76 |
+
e.stopPropagation(); // Prevent triggering the upload area click
|
| 77 |
+
console.log('Choose file button clicked, triggering file input');
|
| 78 |
+
fileInput.click();
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
fileUploadArea.addEventListener('dragover', (e) => this.handleDragOver(e));
|
| 82 |
+
fileUploadArea.addEventListener('drop', (e) => this.handleDrop(e));
|
| 83 |
+
|
| 84 |
+
// PDB fetch
|
| 85 |
+
document.getElementById('fetch-pdb').addEventListener('click', () => this.fetchPDB());
|
| 86 |
+
|
| 87 |
+
// File generation
|
| 88 |
+
document.getElementById('generate-files').addEventListener('click', () => this.generateAllFiles());
|
| 89 |
+
document.getElementById('preview-files').addEventListener('click', () => this.previewFiles());
|
| 90 |
+
document.getElementById('preview-solvated').addEventListener('click', () => this.previewSolvatedProtein());
|
| 91 |
+
document.getElementById('download-zip').addEventListener('click', () => this.downloadZip());
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
// Structure preparation
|
| 95 |
+
document.getElementById('prepare-structure').addEventListener('click', () => this.prepareStructure());
|
| 96 |
+
document.getElementById('preview-prepared').addEventListener('click', () => this.previewPreparedStructure());
|
| 97 |
+
document.getElementById('download-prepared').addEventListener('click', () => this.downloadPreparedStructure());
|
| 98 |
+
|
| 99 |
+
// Ligand download button
|
| 100 |
+
const downloadLigandBtn = document.getElementById('download-ligand');
|
| 101 |
+
if (downloadLigandBtn) {
|
| 102 |
+
downloadLigandBtn.addEventListener('click', (e) => {
|
| 103 |
+
e.preventDefault();
|
| 104 |
+
this.downloadLigandFile();
|
| 105 |
+
});
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
// Navigation buttons
|
| 109 |
+
document.getElementById('prev-tab').addEventListener('click', () => this.previousTab());
|
| 110 |
+
document.getElementById('next-tab').addEventListener('click', () => this.nextTab());
|
| 111 |
+
|
| 112 |
+
// Parameter changes
|
| 113 |
+
document.querySelectorAll('input, select').forEach(input => {
|
| 114 |
+
input.addEventListener('change', () => this.updateSimulationParams());
|
| 115 |
+
});
|
| 116 |
+
|
| 117 |
+
// Render chain and ligand choices when structure tab becomes visible
|
| 118 |
+
document.querySelector('[data-tab="structure-prep"]').addEventListener('click', () => {
|
| 119 |
+
this.renderChainAndLigandSelections();
|
| 120 |
+
});
|
| 121 |
+
|
| 122 |
+
// Separate ligands checkbox change
|
| 123 |
+
document.getElementById('separate-ligands').addEventListener('change', (e) => {
|
| 124 |
+
const downloadBtn = document.getElementById('download-ligand');
|
| 125 |
+
|
| 126 |
+
if (e.target.checked && this.preparedProtein && this.preparedProtein.ligand_present && this.preparedProtein.ligand_content) {
|
| 127 |
+
downloadBtn.disabled = false;
|
| 128 |
+
downloadBtn.classList.remove('btn-outline-secondary');
|
| 129 |
+
downloadBtn.classList.add('btn-outline-primary');
|
| 130 |
+
} else {
|
| 131 |
+
downloadBtn.disabled = true;
|
| 132 |
+
downloadBtn.classList.remove('btn-outline-primary');
|
| 133 |
+
downloadBtn.classList.add('btn-outline-secondary');
|
| 134 |
+
}
|
| 135 |
+
});
|
| 136 |
+
|
| 137 |
+
// Preserve ligands checkbox change
|
| 138 |
+
document.getElementById('preserve-ligands').addEventListener('change', (e) => {
|
| 139 |
+
this.toggleLigandForceFieldGroup(e.target.checked);
|
| 140 |
+
});
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
initializeTabs() {
|
| 144 |
+
const tabs = document.querySelectorAll('.tab-content');
|
| 145 |
+
tabs.forEach(tab => {
|
| 146 |
+
if (!tab.classList.contains('active')) {
|
| 147 |
+
tab.style.display = 'none';
|
| 148 |
+
}
|
| 149 |
+
});
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
initializeStepToggles() {
|
| 153 |
+
document.querySelectorAll('.step-header').forEach(header => {
|
| 154 |
+
header.addEventListener('click', () => {
|
| 155 |
+
const stepItem = header.parentElement;
|
| 156 |
+
const content = stepItem.querySelector('.step-content');
|
| 157 |
+
const isActive = content.classList.contains('active');
|
| 158 |
+
|
| 159 |
+
// Close all other step contents
|
| 160 |
+
document.querySelectorAll('.step-content').forEach(c => c.classList.remove('active'));
|
| 161 |
+
|
| 162 |
+
// Toggle current step
|
| 163 |
+
if (!isActive) {
|
| 164 |
+
content.classList.add('active');
|
| 165 |
+
}
|
| 166 |
+
});
|
| 167 |
+
});
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
loadDefaultParams() {
|
| 171 |
+
this.simulationParams = {
|
| 172 |
+
boxType: 'cubic',
|
| 173 |
+
boxSize: 1.0,
|
| 174 |
+
boxMargin: 1.0,
|
| 175 |
+
forceField: 'amber99sb-ildn',
|
| 176 |
+
waterModel: 'tip3p',
|
| 177 |
+
ionConcentration: 150,
|
| 178 |
+
temperature: 300,
|
| 179 |
+
pressure: 1.0,
|
| 180 |
+
couplingType: 'berendsen',
|
| 181 |
+
timestep: 0.002,
|
| 182 |
+
cutoff: 1.0,
|
| 183 |
+
pmeOrder: 4,
|
| 184 |
+
steps: {
|
| 185 |
+
restrainedMin: { enabled: true, steps: 1000, force: 1000 },
|
| 186 |
+
minimization: { enabled: true, steps: 5000, algorithm: 'steep' },
|
| 187 |
+
nvt: { enabled: true, steps: 50000, temperature: 300 },
|
| 188 |
+
npt: { enabled: true, steps: 100000, temperature: 300, pressure: 1.0 },
|
| 189 |
+
production: { enabled: true, steps: 1000000, temperature: 300, pressure: 1.0 }
|
| 190 |
+
}
|
| 191 |
+
};
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
switchTab(tabName) {
|
| 195 |
+
// Hide all tab contents
|
| 196 |
+
document.querySelectorAll('.tab-content').forEach(tab => {
|
| 197 |
+
tab.classList.remove('active');
|
| 198 |
+
tab.style.display = 'none';
|
| 199 |
+
});
|
| 200 |
+
|
| 201 |
+
// Remove active class from all tab buttons
|
| 202 |
+
document.querySelectorAll('.tab-button').forEach(button => {
|
| 203 |
+
button.classList.remove('active');
|
| 204 |
+
});
|
| 205 |
+
|
| 206 |
+
// Show selected tab
|
| 207 |
+
document.getElementById(tabName).classList.add('active');
|
| 208 |
+
document.getElementById(tabName).style.display = 'block';
|
| 209 |
+
|
| 210 |
+
// Add active class to clicked button
|
| 211 |
+
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
|
| 212 |
+
|
| 213 |
+
// Update current tab index and navigation state
|
| 214 |
+
this.currentTabIndex = this.tabOrder.indexOf(tabName);
|
| 215 |
+
this.updateNavigationState();
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
previousTab() {
|
| 219 |
+
if (this.currentTabIndex > 0) {
|
| 220 |
+
const prevTab = this.tabOrder[this.currentTabIndex - 1];
|
| 221 |
+
this.switchTab(prevTab);
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
nextTab() {
|
| 226 |
+
if (this.currentTabIndex < this.tabOrder.length - 1) {
|
| 227 |
+
const nextTab = this.tabOrder[this.currentTabIndex + 1];
|
| 228 |
+
this.switchTab(nextTab);
|
| 229 |
+
}
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
updateNavigationState() {
|
| 233 |
+
const prevBtn = document.getElementById('prev-tab');
|
| 234 |
+
const nextBtn = document.getElementById('next-tab');
|
| 235 |
+
const currentStepSpan = document.getElementById('current-step');
|
| 236 |
+
const totalStepsSpan = document.getElementById('total-steps');
|
| 237 |
+
|
| 238 |
+
// Update button states
|
| 239 |
+
prevBtn.disabled = this.currentTabIndex === 0;
|
| 240 |
+
nextBtn.disabled = this.currentTabIndex === this.tabOrder.length - 1;
|
| 241 |
+
|
| 242 |
+
// Update step indicator
|
| 243 |
+
if (currentStepSpan) {
|
| 244 |
+
currentStepSpan.textContent = this.currentTabIndex + 1;
|
| 245 |
+
}
|
| 246 |
+
if (totalStepsSpan) {
|
| 247 |
+
totalStepsSpan.textContent = this.tabOrder.length;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
// Update next button text based on current tab
|
| 251 |
+
if (this.currentTabIndex === this.tabOrder.length - 1) {
|
| 252 |
+
nextBtn.innerHTML = 'Complete <i class="fas fa-check"></i>';
|
| 253 |
+
} else {
|
| 254 |
+
nextBtn.innerHTML = 'Next <i class="fas fa-chevron-right"></i>';
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
handleDragOver(e) {
|
| 259 |
+
e.preventDefault();
|
| 260 |
+
e.currentTarget.style.background = '#e3f2fd';
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
handleDrop(e) {
|
| 264 |
+
e.preventDefault();
|
| 265 |
+
e.currentTarget.style.background = '#f8f9fa';
|
| 266 |
+
|
| 267 |
+
const files = e.dataTransfer.files;
|
| 268 |
+
if (files.length > 0) {
|
| 269 |
+
this.processFile(files[0]);
|
| 270 |
+
}
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
handleFileUpload(e) {
|
| 274 |
+
console.log('File upload triggered');
|
| 275 |
+
console.log('Files:', e.target.files);
|
| 276 |
+
const file = e.target.files[0];
|
| 277 |
+
if (file) {
|
| 278 |
+
console.log('File selected:', file.name, file.size, file.type);
|
| 279 |
+
this.processFile(file);
|
| 280 |
+
} else {
|
| 281 |
+
console.log('No file selected');
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
processFile(file) {
|
| 286 |
+
console.log('Processing file:', file.name, file.size, file.type);
|
| 287 |
+
|
| 288 |
+
if (!file.name.toLowerCase().endsWith('.pdb') && !file.name.toLowerCase().endsWith('.ent')) {
|
| 289 |
+
console.log('Invalid file type:', file.name);
|
| 290 |
+
this.showStatus('error', 'Please upload a valid PDB file (.pdb or .ent)');
|
| 291 |
+
return;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
console.log('File validation passed, reading file...');
|
| 295 |
+
const reader = new FileReader();
|
| 296 |
+
reader.onload = (e) => {
|
| 297 |
+
console.log('File read successfully, content length:', e.target.result.length);
|
| 298 |
+
const content = e.target.result;
|
| 299 |
+
this.parsePDBFile(content, file.name);
|
| 300 |
+
};
|
| 301 |
+
reader.onerror = (e) => {
|
| 302 |
+
console.error('Error reading file:', e);
|
| 303 |
+
this.showStatus('error', 'Error reading file');
|
| 304 |
+
};
|
| 305 |
+
reader.readAsText(file);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
async parsePDBFile(content, filename) {
|
| 309 |
+
try {
|
| 310 |
+
// Clean output folder when new PDB is loaded
|
| 311 |
+
try {
|
| 312 |
+
await fetch('/api/clean-output', { method: 'POST' });
|
| 313 |
+
} catch (error) {
|
| 314 |
+
console.log('Could not clean output folder:', error);
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
const lines = content.split('\n');
|
| 318 |
+
let atomCount = 0;
|
| 319 |
+
let chains = new Set();
|
| 320 |
+
let residues = new Set();
|
| 321 |
+
let waterMolecules = 0;
|
| 322 |
+
let ions = 0;
|
| 323 |
+
let ligands = new Set();
|
| 324 |
+
let ligandDetails = [];
|
| 325 |
+
let ligandGroups = new Map(); // Group by RESN-CHAIN
|
| 326 |
+
let hetatoms = 0;
|
| 327 |
+
let structureId = filename.replace(/\.(pdb|ent)$/i, '').toUpperCase();
|
| 328 |
+
|
| 329 |
+
// Common water molecule names
|
| 330 |
+
const waterNames = new Set(['HOH', 'WAT', 'TIP3', 'TIP4', 'SPC', 'SPCE']);
|
| 331 |
+
|
| 332 |
+
// Common ion names (expanded to include more ions)
|
| 333 |
+
const ionNames = new Set(['NA', 'CL', 'K', 'MG', 'CA', 'ZN', 'FE', 'MN', 'CU', 'NI', 'CO',
|
| 334 |
+
'CD', 'HG', 'PB', 'SR', 'BA', 'RB', 'CS', 'LI', 'F', 'BR', 'I', 'SO4', 'PO4', 'CO3', 'NO3', 'NH4']);
|
| 335 |
+
|
| 336 |
+
// Track ligand entities (separated by TER or different chains)
|
| 337 |
+
let ligandEntities = new Map(); // Map to store ligand entities
|
| 338 |
+
let currentLigandEntity = null;
|
| 339 |
+
let currentChain = null;
|
| 340 |
+
let currentResidue = null;
|
| 341 |
+
|
| 342 |
+
// Track unique water molecules by residue
|
| 343 |
+
const uniqueWaterResidues = new Set();
|
| 344 |
+
|
| 345 |
+
lines.forEach(line => {
|
| 346 |
+
if (line.startsWith('ATOM')) {
|
| 347 |
+
atomCount++;
|
| 348 |
+
const chainId = line.substring(21, 22).trim();
|
| 349 |
+
if (chainId) chains.add(chainId);
|
| 350 |
+
|
| 351 |
+
const resName = line.substring(17, 20).trim();
|
| 352 |
+
const resNum = line.substring(22, 26).trim();
|
| 353 |
+
residues.add(`${resName}${resNum}`);
|
| 354 |
+
} else if (line.startsWith('HETATM')) {
|
| 355 |
+
hetatoms++;
|
| 356 |
+
const resName = line.substring(17, 20).trim();
|
| 357 |
+
const resNum = line.substring(22, 26).trim();
|
| 358 |
+
const chainId = line.substring(21, 22).trim();
|
| 359 |
+
const entityKey = `${resName}_${resNum}_${chainId}`;
|
| 360 |
+
|
| 361 |
+
if (waterNames.has(resName)) {
|
| 362 |
+
waterMolecules++;
|
| 363 |
+
uniqueWaterResidues.add(entityKey);
|
| 364 |
+
} else if (ionNames.has(resName)) {
|
| 365 |
+
ions++;
|
| 366 |
+
} else {
|
| 367 |
+
// Everything else is treated as ligand (FALLBACK LOGIC)
|
| 368 |
+
ligands.add(resName);
|
| 369 |
+
ligandDetails.push({ resn: resName, chain: chainId, resi: resNum });
|
| 370 |
+
|
| 371 |
+
// Group by RESN-CHAIN for UI display
|
| 372 |
+
const groupKey = `${resName}-${chainId}`;
|
| 373 |
+
if (!ligandGroups.has(groupKey)) {
|
| 374 |
+
ligandGroups.set(groupKey, { resn: resName, chain: chainId });
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
// Track ligand entities (separated by TER or different chains)
|
| 378 |
+
if (currentChain !== chainId || currentResidue !== resName) {
|
| 379 |
+
// New ligand entity detected
|
| 380 |
+
currentLigandEntity = `${resName}_${chainId}`;
|
| 381 |
+
currentChain = chainId;
|
| 382 |
+
currentResidue = resName;
|
| 383 |
+
|
| 384 |
+
if (!ligandEntities.has(currentLigandEntity)) {
|
| 385 |
+
ligandEntities.set(currentLigandEntity, {
|
| 386 |
+
name: resName,
|
| 387 |
+
chain: chainId,
|
| 388 |
+
residueNum: resNum,
|
| 389 |
+
atomCount: 0
|
| 390 |
+
});
|
| 391 |
+
}
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
// Increment atom count for this ligand entity
|
| 395 |
+
if (ligandEntities.has(currentLigandEntity)) {
|
| 396 |
+
ligandEntities.get(currentLigandEntity).atomCount++;
|
| 397 |
+
}
|
| 398 |
+
}
|
| 399 |
+
} else if (line.startsWith('TER')) {
|
| 400 |
+
// TER record separates entities, reset current ligand tracking
|
| 401 |
+
currentLigandEntity = null;
|
| 402 |
+
currentChain = null;
|
| 403 |
+
currentResidue = null;
|
| 404 |
+
}
|
| 405 |
+
});
|
| 406 |
+
|
| 407 |
+
// Count unique water molecules
|
| 408 |
+
const uniqueWaterCount = uniqueWaterResidues.size;
|
| 409 |
+
|
| 410 |
+
// Count ligand entities (different ligands or same ligand in different chains)
|
| 411 |
+
const ligandEntityCount = ligandEntities.size;
|
| 412 |
+
|
| 413 |
+
// Get unique ligand names
|
| 414 |
+
const uniqueLigandNames = Array.from(ligands);
|
| 415 |
+
|
| 416 |
+
// Create ligand info string
|
| 417 |
+
let ligandInfo = 'None';
|
| 418 |
+
if (uniqueLigandNames.length > 0) {
|
| 419 |
+
if (ligandEntityCount > 1) {
|
| 420 |
+
// Multiple entities - show count and names
|
| 421 |
+
ligandInfo = `${ligandEntityCount} entities: ${uniqueLigandNames.join(', ')}`;
|
| 422 |
+
} else {
|
| 423 |
+
// Single entity
|
| 424 |
+
ligandInfo = uniqueLigandNames.join(', ');
|
| 425 |
+
}
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
this.currentProtein = {
|
| 429 |
+
filename: filename,
|
| 430 |
+
structureId: structureId,
|
| 431 |
+
atomCount: atomCount,
|
| 432 |
+
chains: Array.from(chains),
|
| 433 |
+
residueCount: residues.size,
|
| 434 |
+
waterMolecules: uniqueWaterCount,
|
| 435 |
+
ions: ions,
|
| 436 |
+
ligands: uniqueLigandNames,
|
| 437 |
+
ligandDetails: ligandDetails,
|
| 438 |
+
ligandGroups: Array.from(ligandGroups.values()), // RESN-CHAIN groups for UI
|
| 439 |
+
ligandEntities: ligandEntityCount,
|
| 440 |
+
ligandInfo: ligandInfo,
|
| 441 |
+
hetatoms: hetatoms,
|
| 442 |
+
content: content
|
| 443 |
+
};
|
| 444 |
+
|
| 445 |
+
this.displayProteinInfo();
|
| 446 |
+
this.showStatus('success', `Successfully loaded ${filename}`);
|
| 447 |
+
} catch (error) {
|
| 448 |
+
this.showStatus('error', 'Error parsing PDB file: ' + error.message);
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
displayProteinInfo() {
|
| 453 |
+
if (!this.currentProtein) return;
|
| 454 |
+
|
| 455 |
+
document.getElementById('structure-id').textContent = this.currentProtein.structureId;
|
| 456 |
+
document.getElementById('atom-count').textContent = this.currentProtein.atomCount.toLocaleString();
|
| 457 |
+
document.getElementById('chain-info').textContent = this.currentProtein.chains.join(', ');
|
| 458 |
+
document.getElementById('residue-count').textContent = this.currentProtein.residueCount.toLocaleString();
|
| 459 |
+
document.getElementById('water-count').textContent = this.currentProtein.waterMolecules.toLocaleString();
|
| 460 |
+
document.getElementById('ion-count').textContent = this.currentProtein.ions.toLocaleString();
|
| 461 |
+
document.getElementById('ligand-info').textContent = this.currentProtein.ligandInfo;
|
| 462 |
+
document.getElementById('hetatm-count').textContent = this.currentProtein.hetatoms.toLocaleString();
|
| 463 |
+
|
| 464 |
+
document.getElementById('protein-preview').style.display = 'block';
|
| 465 |
+
|
| 466 |
+
// Load 3D visualization
|
| 467 |
+
this.load3DVisualization();
|
| 468 |
+
|
| 469 |
+
// Also refresh chain/ligand lists when protein info is displayed
|
| 470 |
+
this.renderChainAndLigandSelections();
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
async fetchPDB() {
|
| 474 |
+
const pdbId = document.getElementById('pdb-id').value.trim().toUpperCase();
|
| 475 |
+
if (!pdbId) {
|
| 476 |
+
this.showStatus('error', 'Please enter a PDB ID');
|
| 477 |
+
return;
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
if (!/^[0-9A-Z]{4}$/.test(pdbId)) {
|
| 481 |
+
this.showStatus('error', 'Please enter a valid 4-character PDB ID');
|
| 482 |
+
return;
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
this.showStatus('info', 'Fetching PDB structure...');
|
| 486 |
+
|
| 487 |
+
try {
|
| 488 |
+
const response = await fetch(`https://files.rcsb.org/download/${pdbId}.pdb`);
|
| 489 |
+
if (!response.ok) {
|
| 490 |
+
throw new Error(`PDB ID ${pdbId} not found`);
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
const content = await response.text();
|
| 494 |
+
this.parsePDBFile(content, `${pdbId}.pdb`);
|
| 495 |
+
this.showStatus('success', `Successfully fetched PDB structure ${pdbId}`);
|
| 496 |
+
} catch (error) {
|
| 497 |
+
this.showStatus('error', `Error fetching PDB: ${error.message}`);
|
| 498 |
+
}
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
showStatus(type, message) {
|
| 502 |
+
const statusDiv = document.getElementById('pdb-status');
|
| 503 |
+
statusDiv.className = `status-message ${type}`;
|
| 504 |
+
statusDiv.textContent = message;
|
| 505 |
+
statusDiv.style.display = 'block';
|
| 506 |
+
|
| 507 |
+
// Auto-hide after 5 seconds for success messages
|
| 508 |
+
if (type === 'success') {
|
| 509 |
+
setTimeout(() => {
|
| 510 |
+
statusDiv.style.display = 'none';
|
| 511 |
+
}, 5000);
|
| 512 |
+
}
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
updateSimulationParams() {
|
| 516 |
+
// Update basic parameters
|
| 517 |
+
this.simulationParams.boxType = document.getElementById('box-type').value;
|
| 518 |
+
this.simulationParams.boxSize = parseFloat(document.getElementById('box-size').value);
|
| 519 |
+
this.simulationParams.forceField = document.getElementById('force-field').value;
|
| 520 |
+
this.simulationParams.waterModel = document.getElementById('water-model').value;
|
| 521 |
+
this.simulationParams.addIons = document.getElementById('add-ions').value;
|
| 522 |
+
this.simulationParams.temperature = parseInt(document.getElementById('temperature').value);
|
| 523 |
+
this.simulationParams.pressure = parseFloat(document.getElementById('pressure').value);
|
| 524 |
+
this.simulationParams.couplingType = document.getElementById('coupling-type').value;
|
| 525 |
+
this.simulationParams.timestep = parseFloat(document.getElementById('timestep').value);
|
| 526 |
+
this.simulationParams.cutoff = parseFloat(document.getElementById('cutoff').value);
|
| 527 |
+
this.simulationParams.electrostatic = document.getElementById('electrostatic').value;
|
| 528 |
+
this.simulationParams.ligandForceField = document.getElementById('ligand-forcefield').value;
|
| 529 |
+
|
| 530 |
+
// Update step parameters
|
| 531 |
+
this.simulationParams.steps.restrainedMin = {
|
| 532 |
+
enabled: document.getElementById('enable-restrained-min').checked,
|
| 533 |
+
steps: parseInt(document.getElementById('restrained-steps').value),
|
| 534 |
+
force: parseInt(document.getElementById('restrained-force').value)
|
| 535 |
+
};
|
| 536 |
+
|
| 537 |
+
this.simulationParams.steps.minimization = {
|
| 538 |
+
enabled: document.getElementById('enable-minimization').checked,
|
| 539 |
+
steps: parseInt(document.getElementById('min-steps').value),
|
| 540 |
+
algorithm: document.getElementById('min-algorithm').value
|
| 541 |
+
};
|
| 542 |
+
|
| 543 |
+
this.simulationParams.steps.nvt = {
|
| 544 |
+
enabled: document.getElementById('enable-nvt').checked,
|
| 545 |
+
steps: parseInt(document.getElementById('nvt-steps').value),
|
| 546 |
+
temperature: parseInt(document.getElementById('nvt-temp').value)
|
| 547 |
+
};
|
| 548 |
+
|
| 549 |
+
this.simulationParams.steps.npt = {
|
| 550 |
+
enabled: document.getElementById('enable-npt').checked,
|
| 551 |
+
steps: parseInt(document.getElementById('npt-steps').value),
|
| 552 |
+
temperature: parseInt(document.getElementById('npt-temp').value),
|
| 553 |
+
pressure: parseFloat(document.getElementById('npt-pressure').value)
|
| 554 |
+
};
|
| 555 |
+
|
| 556 |
+
this.simulationParams.steps.production = {
|
| 557 |
+
enabled: document.getElementById('enable-production').checked,
|
| 558 |
+
steps: parseInt(document.getElementById('prod-steps').value),
|
| 559 |
+
temperature: parseInt(document.getElementById('prod-temp').value),
|
| 560 |
+
pressure: parseFloat(document.getElementById('prod-pressure').value)
|
| 561 |
+
};
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
toggleLigandForceFieldGroup(show) {
|
| 565 |
+
const section = document.getElementById('ligand-forcefield-section');
|
| 566 |
+
if (show) {
|
| 567 |
+
section.style.display = 'block';
|
| 568 |
+
section.classList.remove('disabled');
|
| 569 |
+
} else {
|
| 570 |
+
section.style.display = 'none';
|
| 571 |
+
section.classList.add('disabled');
|
| 572 |
+
}
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
async calculateNetCharge(event) {
|
| 576 |
+
console.log('calculateNetCharge called'); // Debug log
|
| 577 |
+
if (!this.preparedProtein) {
|
| 578 |
+
alert('Please prepare structure first before calculating net charge.');
|
| 579 |
+
return;
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
// Show loading state
|
| 583 |
+
const button = event ? event.target : document.querySelector('button[onclick*="calculateNetCharge"]');
|
| 584 |
+
const originalText = button.innerHTML;
|
| 585 |
+
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Calculating...';
|
| 586 |
+
button.disabled = true;
|
| 587 |
+
|
| 588 |
+
try {
|
| 589 |
+
// Get the selected force field
|
| 590 |
+
const selectedForceField = document.getElementById('force-field').value;
|
| 591 |
+
|
| 592 |
+
const response = await fetch('/api/calculate-net-charge', {
|
| 593 |
+
method: 'POST',
|
| 594 |
+
headers: {
|
| 595 |
+
'Content-Type': 'application/json',
|
| 596 |
+
},
|
| 597 |
+
body: JSON.stringify({
|
| 598 |
+
force_field: selectedForceField
|
| 599 |
+
})
|
| 600 |
+
});
|
| 601 |
+
|
| 602 |
+
const result = await response.json();
|
| 603 |
+
|
| 604 |
+
if (result.success) {
|
| 605 |
+
// Update the Add Ions dropdown based on suggestion
|
| 606 |
+
const addIonsSelect = document.getElementById('add-ions');
|
| 607 |
+
if (result.ion_type === 'Cl-') {
|
| 608 |
+
addIonsSelect.value = 'Cl-';
|
| 609 |
+
} else if (result.ion_type === 'Na+') {
|
| 610 |
+
addIonsSelect.value = 'Na+';
|
| 611 |
+
} else {
|
| 612 |
+
addIonsSelect.value = 'None';
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
// Show detailed results
|
| 616 |
+
alert(`✅ System Charge Analysis Complete!\n\n` +
|
| 617 |
+
`Net Charge: ${result.net_charge}\n` +
|
| 618 |
+
`Recommendation: ${result.suggestion}\n` +
|
| 619 |
+
`Ligand Present: ${result.ligand_present ? 'Yes' : 'No'}`);
|
| 620 |
+
} else {
|
| 621 |
+
alert(`❌ Error: ${result.error}`);
|
| 622 |
+
}
|
| 623 |
+
} catch (error) {
|
| 624 |
+
console.error('Error calculating net charge:', error);
|
| 625 |
+
alert(`❌ Error: Failed to calculate net charge. ${error.message}`);
|
| 626 |
+
} finally {
|
| 627 |
+
// Restore button state
|
| 628 |
+
button.innerHTML = originalText;
|
| 629 |
+
button.disabled = false;
|
| 630 |
+
}
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
async generateLigandFF(event) {
|
| 634 |
+
console.log('generateLigandFF called'); // Debug log
|
| 635 |
+
if (!this.preparedProtein || !this.preparedProtein.ligand_present) {
|
| 636 |
+
alert('No ligand found. Please ensure ligands are preserved during structure preparation.');
|
| 637 |
+
return;
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
const selectedFF = document.getElementById('ligand-forcefield').value;
|
| 641 |
+
|
| 642 |
+
// Show loading state
|
| 643 |
+
const button = event ? event.target : document.querySelector('button[onclick*="generateLigandFF"]');
|
| 644 |
+
const originalText = button.innerHTML;
|
| 645 |
+
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating...';
|
| 646 |
+
button.disabled = true;
|
| 647 |
+
|
| 648 |
+
try {
|
| 649 |
+
const response = await fetch('/api/generate-ligand-ff', {
|
| 650 |
+
method: 'POST',
|
| 651 |
+
headers: {
|
| 652 |
+
'Content-Type': 'application/json',
|
| 653 |
+
},
|
| 654 |
+
body: JSON.stringify({
|
| 655 |
+
force_field: selectedFF
|
| 656 |
+
})
|
| 657 |
+
});
|
| 658 |
+
|
| 659 |
+
const result = await response.json();
|
| 660 |
+
|
| 661 |
+
if (result.success) {
|
| 662 |
+
alert(`✅ ${result.message}\n\nNet charge: ${result.net_charge}\n\nGenerated files:\n- ${result.files.mol2}\n- ${result.files.frcmod}`);
|
| 663 |
+
} else {
|
| 664 |
+
alert(`❌ Error: ${result.error}`);
|
| 665 |
+
}
|
| 666 |
+
} catch (error) {
|
| 667 |
+
console.error('Error generating ligand force field:', error);
|
| 668 |
+
alert(`❌ Error: Failed to generate force field parameters. ${error.message}`);
|
| 669 |
+
} finally {
|
| 670 |
+
// Restore button state
|
| 671 |
+
button.innerHTML = originalText;
|
| 672 |
+
button.disabled = false;
|
| 673 |
+
}
|
| 674 |
+
}
|
| 675 |
+
|
| 676 |
+
countAtomsInPDB(pdbContent) {
|
| 677 |
+
const lines = pdbContent.split('\n');
|
| 678 |
+
return lines.filter(line => line.startsWith('ATOM') || line.startsWith('HETATM')).length;
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
async generateAllFiles() {
|
| 682 |
+
if (!this.preparedProtein) {
|
| 683 |
+
alert('Please prepare structure first');
|
| 684 |
+
return;
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
// Show loading state
|
| 688 |
+
const button = document.getElementById('generate-files');
|
| 689 |
+
const originalText = button.innerHTML;
|
| 690 |
+
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating...';
|
| 691 |
+
button.disabled = true;
|
| 692 |
+
|
| 693 |
+
try {
|
| 694 |
+
// Collect all simulation parameters
|
| 695 |
+
const params = {
|
| 696 |
+
cutoff_distance: parseFloat(document.getElementById('cutoff').value),
|
| 697 |
+
temperature: parseFloat(document.getElementById('temperature').value),
|
| 698 |
+
pressure: parseFloat(document.getElementById('pressure').value),
|
| 699 |
+
restrained_steps: parseInt(document.getElementById('restrained-steps').value),
|
| 700 |
+
restrained_force: parseFloat(document.getElementById('restrained-force').value),
|
| 701 |
+
min_steps: parseInt(document.getElementById('min-steps').value),
|
| 702 |
+
npt_heating_steps: parseInt(document.getElementById('nvt-steps').value),
|
| 703 |
+
npt_equilibration_steps: parseInt(document.getElementById('npt-steps').value),
|
| 704 |
+
production_steps: parseInt(document.getElementById('prod-steps').value),
|
| 705 |
+
timestep: parseFloat(document.getElementById('timestep').value),
|
| 706 |
+
// Force field parameters
|
| 707 |
+
force_field: document.getElementById('force-field').value,
|
| 708 |
+
water_model: document.getElementById('water-model').value,
|
| 709 |
+
add_ions: document.getElementById('add-ions').value,
|
| 710 |
+
distance: parseFloat(document.getElementById('box-size').value)
|
| 711 |
+
};
|
| 712 |
+
|
| 713 |
+
const response = await fetch('/api/generate-all-files', {
|
| 714 |
+
method: 'POST',
|
| 715 |
+
headers: {
|
| 716 |
+
'Content-Type': 'application/json',
|
| 717 |
+
},
|
| 718 |
+
body: JSON.stringify(params)
|
| 719 |
+
});
|
| 720 |
+
|
| 721 |
+
const result = await response.json();
|
| 722 |
+
|
| 723 |
+
if (result.success) {
|
| 724 |
+
let message = `✅ ${result.message}\n\nGenerated files:\n`;
|
| 725 |
+
result.files_generated.forEach(file => {
|
| 726 |
+
message += `- ${file}\n`;
|
| 727 |
+
});
|
| 728 |
+
|
| 729 |
+
if (result.warnings && result.warnings.length > 0) {
|
| 730 |
+
message += `\n⚠️ Warnings:\n`;
|
| 731 |
+
result.warnings.forEach(warning => {
|
| 732 |
+
message += `- ${warning}\n`;
|
| 733 |
+
});
|
| 734 |
+
}
|
| 735 |
+
|
| 736 |
+
alert(message);
|
| 737 |
+
|
| 738 |
+
// Reveal the download section
|
| 739 |
+
const downloadSection = document.getElementById('download-section');
|
| 740 |
+
if (downloadSection) {
|
| 741 |
+
downloadSection.style.display = 'block';
|
| 742 |
+
}
|
| 743 |
+
} else {
|
| 744 |
+
alert(`❌ Error: ${result.error}`);
|
| 745 |
+
}
|
| 746 |
+
} catch (error) {
|
| 747 |
+
console.error('Error generating files:', error);
|
| 748 |
+
alert(`❌ Error: Failed to generate simulation files. ${error.message}`);
|
| 749 |
+
} finally {
|
| 750 |
+
// Restore button state
|
| 751 |
+
button.innerHTML = originalText;
|
| 752 |
+
button.disabled = false;
|
| 753 |
+
}
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
createSimulationFiles() {
|
| 757 |
+
const files = {};
|
| 758 |
+
const proteinName = this.currentProtein.structureId.toLowerCase();
|
| 759 |
+
|
| 760 |
+
// Generate GROMACS input files
|
| 761 |
+
files[`${proteinName}.mdp`] = this.generateMDPFile();
|
| 762 |
+
files[`${proteinName}_restrained.mdp`] = this.generateRestrainedMDPFile();
|
| 763 |
+
files[`${proteinName}_min.mdp`] = this.generateMinimizationMDPFile();
|
| 764 |
+
files[`${proteinName}_nvt.mdp`] = this.generateNVTMDPFile();
|
| 765 |
+
files[`${proteinName}_npt.mdp`] = this.generateNPTMDPFile();
|
| 766 |
+
files[`${proteinName}_prod.mdp`] = this.generateProductionMDPFile();
|
| 767 |
+
|
| 768 |
+
// Generate PBS script
|
| 769 |
+
files[`${proteinName}_simulation.pbs`] = this.generatePBSScript();
|
| 770 |
+
|
| 771 |
+
// Generate setup script
|
| 772 |
+
files[`setup_${proteinName}.sh`] = this.generateSetupScript();
|
| 773 |
+
|
| 774 |
+
// Generate analysis script
|
| 775 |
+
files[`analyze_${proteinName}.sh`] = this.generateAnalysisScript();
|
| 776 |
+
|
| 777 |
+
return files;
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
generateMDPFile() {
|
| 781 |
+
const params = this.simulationParams;
|
| 782 |
+
return `; MD Simulation Parameters
|
| 783 |
+
; Generated by MD Simulation Pipeline
|
| 784 |
+
|
| 785 |
+
; Run parameters
|
| 786 |
+
integrator = md
|
| 787 |
+
dt = ${params.timestep}
|
| 788 |
+
nsteps = ${params.steps.production.steps}
|
| 789 |
+
|
| 790 |
+
; Output control
|
| 791 |
+
nstxout = 5000
|
| 792 |
+
nstvout = 5000
|
| 793 |
+
nstenergy = 1000
|
| 794 |
+
nstlog = 1000
|
| 795 |
+
|
| 796 |
+
; Bond parameters
|
| 797 |
+
constraint_algorithm = lincs
|
| 798 |
+
constraints = h-bonds
|
| 799 |
+
lincs_iter = 1
|
| 800 |
+
lincs_order = 4
|
| 801 |
+
|
| 802 |
+
; Neighbor searching
|
| 803 |
+
cutoff-scheme = Verlet
|
| 804 |
+
ns_type = grid
|
| 805 |
+
nstlist = 40
|
| 806 |
+
rlist = ${params.cutoff}
|
| 807 |
+
|
| 808 |
+
; Electrostatics
|
| 809 |
+
coulombtype = PME
|
| 810 |
+
rcoulomb = ${params.cutoff}
|
| 811 |
+
pme_order = ${params.pmeOrder}
|
| 812 |
+
fourierspacing = 0.16
|
| 813 |
+
|
| 814 |
+
; Van der Waals
|
| 815 |
+
vdwtype = Cut-off
|
| 816 |
+
rvdw = ${params.cutoff}
|
| 817 |
+
|
| 818 |
+
; Temperature coupling
|
| 819 |
+
tcoupl = ${params.couplingType}
|
| 820 |
+
tc-grps = Protein Non-Protein
|
| 821 |
+
tau_t = 0.1 0.1
|
| 822 |
+
ref_t = ${params.temperature} ${params.temperature}
|
| 823 |
+
|
| 824 |
+
; Pressure coupling
|
| 825 |
+
pcoupl = ${params.couplingType}
|
| 826 |
+
pcoupltype = isotropic
|
| 827 |
+
tau_p = 2.0
|
| 828 |
+
ref_p = ${params.pressure}
|
| 829 |
+
compressibility = 4.5e-5
|
| 830 |
+
|
| 831 |
+
; Dispersion correction
|
| 832 |
+
DispCorr = EnerPres
|
| 833 |
+
|
| 834 |
+
; Velocity generation
|
| 835 |
+
gen_vel = yes
|
| 836 |
+
gen_temp = ${params.temperature}
|
| 837 |
+
gen_seed = -1
|
| 838 |
+
`;
|
| 839 |
+
}
|
| 840 |
+
|
| 841 |
+
generateRestrainedMDPFile() {
|
| 842 |
+
const params = this.simulationParams;
|
| 843 |
+
return `; Restrained Minimization Parameters
|
| 844 |
+
integrator = steep
|
| 845 |
+
nsteps = ${params.steps.restrainedMin.steps}
|
| 846 |
+
emstep = 0.01
|
| 847 |
+
emtol = 1000
|
| 848 |
+
|
| 849 |
+
; Position restraints
|
| 850 |
+
define = -DPOSRES
|
| 851 |
+
refcoord_scaling = com
|
| 852 |
+
|
| 853 |
+
; Output control
|
| 854 |
+
nstxout = 100
|
| 855 |
+
nstenergy = 100
|
| 856 |
+
nstlog = 100
|
| 857 |
+
|
| 858 |
+
; Bond parameters
|
| 859 |
+
constraint_algorithm = lincs
|
| 860 |
+
constraints = h-bonds
|
| 861 |
+
|
| 862 |
+
; Neighbor searching
|
| 863 |
+
cutoff-scheme = Verlet
|
| 864 |
+
ns_type = grid
|
| 865 |
+
nstlist = 10
|
| 866 |
+
rlist = ${params.cutoff}
|
| 867 |
+
|
| 868 |
+
; Electrostatics
|
| 869 |
+
coulombtype = PME
|
| 870 |
+
rcoulomb = ${params.cutoff}
|
| 871 |
+
pme_order = ${params.pme_order}
|
| 872 |
+
|
| 873 |
+
; Van der Waals
|
| 874 |
+
vdwtype = Cut-off
|
| 875 |
+
rvdw = ${params.cutoff}
|
| 876 |
+
`;
|
| 877 |
+
}
|
| 878 |
+
|
| 879 |
+
generateMinimizationMDPFile() {
|
| 880 |
+
const params = this.simulationParams;
|
| 881 |
+
return `; Minimization Parameters
|
| 882 |
+
integrator = ${params.steps.minimization.algorithm}
|
| 883 |
+
nsteps = ${params.steps.minimization.steps}
|
| 884 |
+
emstep = 0.01
|
| 885 |
+
emtol = 1000
|
| 886 |
+
|
| 887 |
+
; Output control
|
| 888 |
+
nstxout = 100
|
| 889 |
+
nstenergy = 100
|
| 890 |
+
nstlog = 100
|
| 891 |
+
|
| 892 |
+
; Bond parameters
|
| 893 |
+
constraint_algorithm = lincs
|
| 894 |
+
constraints = h-bonds
|
| 895 |
+
|
| 896 |
+
; Neighbor searching
|
| 897 |
+
cutoff-scheme = Verlet
|
| 898 |
+
ns_type = grid
|
| 899 |
+
nstlist = 10
|
| 900 |
+
rlist = ${params.cutoff}
|
| 901 |
+
|
| 902 |
+
; Electrostatics
|
| 903 |
+
coulombtype = PME
|
| 904 |
+
rcoulomb = ${params.cutoff}
|
| 905 |
+
pme_order = ${params.pme_order}
|
| 906 |
+
|
| 907 |
+
; Van der Waals
|
| 908 |
+
vdwtype = Cut-off
|
| 909 |
+
rvdw = ${params.cutoff}
|
| 910 |
+
`;
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
generateNVTMDPFile() {
|
| 914 |
+
const params = this.simulationParams;
|
| 915 |
+
return `; NVT Equilibration Parameters
|
| 916 |
+
integrator = md
|
| 917 |
+
dt = ${params.timestep}
|
| 918 |
+
nsteps = ${params.steps.nvt.steps}
|
| 919 |
+
|
| 920 |
+
; Output control
|
| 921 |
+
nstxout = 5000
|
| 922 |
+
nstvout = 5000
|
| 923 |
+
nstenergy = 1000
|
| 924 |
+
nstlog = 1000
|
| 925 |
+
|
| 926 |
+
; Bond parameters
|
| 927 |
+
constraint_algorithm = lincs
|
| 928 |
+
constraints = h-bonds
|
| 929 |
+
lincs_iter = 1
|
| 930 |
+
lincs_order = 4
|
| 931 |
+
|
| 932 |
+
; Neighbor searching
|
| 933 |
+
cutoff-scheme = Verlet
|
| 934 |
+
ns_type = grid
|
| 935 |
+
nstlist = 40
|
| 936 |
+
rlist = ${params.cutoff}
|
| 937 |
+
|
| 938 |
+
; Electrostatics
|
| 939 |
+
coulombtype = PME
|
| 940 |
+
rcoulomb = ${params.cutoff}
|
| 941 |
+
pme_order = ${params.pme_order}
|
| 942 |
+
|
| 943 |
+
; Van der Waals
|
| 944 |
+
vdwtype = Cut-off
|
| 945 |
+
rvdw = ${params.cutoff}
|
| 946 |
+
|
| 947 |
+
; Temperature coupling
|
| 948 |
+
tcoupl = ${params.couplingType}
|
| 949 |
+
tc-grps = Protein Non-Protein
|
| 950 |
+
tau_t = 0.1 0.1
|
| 951 |
+
ref_t = ${params.steps.nvt.temperature} ${params.steps.nvt.temperature}
|
| 952 |
+
|
| 953 |
+
; Pressure coupling (disabled for NVT)
|
| 954 |
+
pcoupl = no
|
| 955 |
+
|
| 956 |
+
; Velocity generation
|
| 957 |
+
gen_vel = yes
|
| 958 |
+
gen_temp = ${params.steps.nvt.temperature}
|
| 959 |
+
gen_seed = -1
|
| 960 |
+
`;
|
| 961 |
+
}
|
| 962 |
+
|
| 963 |
+
generateNPTMDPFile() {
|
| 964 |
+
const params = this.simulationParams;
|
| 965 |
+
return `; NPT Equilibration Parameters
|
| 966 |
+
integrator = md
|
| 967 |
+
dt = ${params.timestep}
|
| 968 |
+
nsteps = ${params.steps.npt.steps}
|
| 969 |
+
|
| 970 |
+
; Output control
|
| 971 |
+
nstxout = 5000
|
| 972 |
+
nstvout = 5000
|
| 973 |
+
nstenergy = 1000
|
| 974 |
+
nstlog = 1000
|
| 975 |
+
|
| 976 |
+
; Bond parameters
|
| 977 |
+
constraint_algorithm = lincs
|
| 978 |
+
constraints = h-bonds
|
| 979 |
+
lincs_iter = 1
|
| 980 |
+
lincs_order = 4
|
| 981 |
+
|
| 982 |
+
; Neighbor searching
|
| 983 |
+
cutoff-scheme = Verlet
|
| 984 |
+
ns_type = grid
|
| 985 |
+
nstlist = 40
|
| 986 |
+
rlist = ${params.cutoff}
|
| 987 |
+
|
| 988 |
+
; Electrostatics
|
| 989 |
+
coulombtype = PME
|
| 990 |
+
rcoulomb = ${params.cutoff}
|
| 991 |
+
pme_order = ${params.pme_order}
|
| 992 |
+
|
| 993 |
+
; Van der Waals
|
| 994 |
+
vdwtype = Cut-off
|
| 995 |
+
rvdw = ${params.cutoff}
|
| 996 |
+
|
| 997 |
+
; Temperature coupling
|
| 998 |
+
tcoupl = ${params.couplingType}
|
| 999 |
+
tc-grps = Protein Non-Protein
|
| 1000 |
+
tau_t = 0.1 0.1
|
| 1001 |
+
ref_t = ${params.steps.npt.temperature} ${params.steps.npt.temperature}
|
| 1002 |
+
|
| 1003 |
+
; Pressure coupling
|
| 1004 |
+
pcoupl = ${params.couplingType}
|
| 1005 |
+
pcoupltype = isotropic
|
| 1006 |
+
tau_p = 2.0
|
| 1007 |
+
ref_p = ${params.steps.npt.pressure}
|
| 1008 |
+
compressibility = 4.5e-5
|
| 1009 |
+
|
| 1010 |
+
; Velocity generation
|
| 1011 |
+
gen_vel = no
|
| 1012 |
+
`;
|
| 1013 |
+
}
|
| 1014 |
+
|
| 1015 |
+
generateProductionMDPFile() {
|
| 1016 |
+
return this.generateMDPFile(); // Same as main MDP file
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
generatePBSScript() {
|
| 1020 |
+
const proteinName = this.currentProtein.structureId.toLowerCase();
|
| 1021 |
+
const totalSteps = this.simulationParams.steps.production.steps;
|
| 1022 |
+
const timeInNs = (totalSteps * this.simulationParams.timestep) / 1000;
|
| 1023 |
+
|
| 1024 |
+
return `#!/bin/bash
|
| 1025 |
+
#PBS -N ${proteinName}_md
|
| 1026 |
+
#PBS -l nodes=1:ppn=16
|
| 1027 |
+
#PBS -l walltime=24:00:00
|
| 1028 |
+
#PBS -q normal
|
| 1029 |
+
#PBS -j oe
|
| 1030 |
+
|
| 1031 |
+
# Change to the directory where the job was submitted
|
| 1032 |
+
cd $PBS_O_WORKDIR
|
| 1033 |
+
|
| 1034 |
+
# Load required modules
|
| 1035 |
+
module load gromacs/2023.2
|
| 1036 |
+
module load intel/2021.4.0
|
| 1037 |
+
|
| 1038 |
+
# Set up environment
|
| 1039 |
+
export OMP_NUM_THREADS=16
|
| 1040 |
+
export GMX_MAXBACKUP=-1
|
| 1041 |
+
|
| 1042 |
+
# Simulation parameters
|
| 1043 |
+
PROTEIN=${proteinName}
|
| 1044 |
+
STEPS=${totalSteps}
|
| 1045 |
+
TIME_NS=${timeInNs.toFixed(2)}
|
| 1046 |
+
|
| 1047 |
+
echo "Starting MD simulation for $PROTEIN"
|
| 1048 |
+
echo "Total simulation time: $TIME_NS ns"
|
| 1049 |
+
echo "Job started at: $(date)"
|
| 1050 |
+
|
| 1051 |
+
# Run the simulation
|
| 1052 |
+
./run_simulation.sh $PROTEIN
|
| 1053 |
+
|
| 1054 |
+
echo "Simulation completed at: $(date)"
|
| 1055 |
+
echo "Results saved in output directory"
|
| 1056 |
+
`;
|
| 1057 |
+
}
|
| 1058 |
+
|
| 1059 |
+
generateSetupScript() {
|
| 1060 |
+
const proteinName = this.currentProtein.structureId.toLowerCase();
|
| 1061 |
+
return `#!/bin/bash
|
| 1062 |
+
# Setup script for ${proteinName} MD simulation
|
| 1063 |
+
# Generated by MD Simulation Pipeline
|
| 1064 |
+
|
| 1065 |
+
set -e
|
| 1066 |
+
|
| 1067 |
+
PROTEIN=${proteinName}
|
| 1068 |
+
FORCE_FIELD=${this.simulationParams.forceField}
|
| 1069 |
+
WATER_MODEL=${this.simulationParams.waterModel}
|
| 1070 |
+
|
| 1071 |
+
echo "Setting up MD simulation for $PROTEIN"
|
| 1072 |
+
|
| 1073 |
+
# Create output directory
|
| 1074 |
+
mkdir -p output
|
| 1075 |
+
|
| 1076 |
+
# 1. Prepare protein structure
|
| 1077 |
+
echo "Preparing protein structure..."
|
| 1078 |
+
gmx pdb2gmx -f ${PROTEIN}.pdb -o ${PROTEIN}_processed.gro -p ${PROTEIN}.top -ff ${FORCE_FIELD} -water ${WATER_MODEL}
|
| 1079 |
+
|
| 1080 |
+
# 2. Define simulation box
|
| 1081 |
+
echo "Defining simulation box..."
|
| 1082 |
+
gmx editconf -f ${PROTEIN}_processed.gro -o ${PROTEIN}_box.gro -c -d ${this.simulationParams.boxMargin} -bt ${this.simulationParams.boxType}
|
| 1083 |
+
|
| 1084 |
+
# 3. Add solvent
|
| 1085 |
+
echo "Adding solvent..."
|
| 1086 |
+
gmx solvate -cp ${PROTEIN}_box.gro -cs spc216.gro -o ${PROTEIN}_solv.gro -p ${PROTEIN}.top
|
| 1087 |
+
|
| 1088 |
+
# 4. Add ions
|
| 1089 |
+
echo "Adding ions..."
|
| 1090 |
+
gmx grompp -f ${PROTEIN}_restrained.mdp -c ${PROTEIN}_solv.gro -p ${PROTEIN}.top -o ${PROTEIN}_ions.tpr
|
| 1091 |
+
echo "SOL" | gmx genion -s ${PROTEIN}_ions.tpr -o ${PROTEIN}_final.gro -p ${PROTEIN}.top -pname NA -nname CL -neutral
|
| 1092 |
+
|
| 1093 |
+
echo "Setup completed successfully!"
|
| 1094 |
+
echo "Ready to run simulation with: ./run_simulation.sh $PROTEIN"
|
| 1095 |
+
`;
|
| 1096 |
+
}
|
| 1097 |
+
|
| 1098 |
+
generateAnalysisScript() {
|
| 1099 |
+
const proteinName = this.currentProtein.structureId.toLowerCase();
|
| 1100 |
+
return `#!/bin/bash
|
| 1101 |
+
# Analysis script for ${proteinName} MD simulation
|
| 1102 |
+
# Generated by MD Simulation Pipeline
|
| 1103 |
+
|
| 1104 |
+
PROTEIN=${proteinName}
|
| 1105 |
+
|
| 1106 |
+
echo "Analyzing MD simulation results for $PROTEIN"
|
| 1107 |
+
|
| 1108 |
+
# Create analysis directory
|
| 1109 |
+
mkdir -p analysis
|
| 1110 |
+
|
| 1111 |
+
# 1. RMSD analysis
|
| 1112 |
+
echo "Calculating RMSD..."
|
| 1113 |
+
echo "Protein" | gmx rms -s ${PROTEIN}_final.tpr -f ${PROTEIN}_prod.xtc -o analysis/${PROTEIN}_rmsd.xvg -tu ns
|
| 1114 |
+
|
| 1115 |
+
# 2. RMSF analysis
|
| 1116 |
+
echo "Calculating RMSF..."
|
| 1117 |
+
echo "Protein" | gmx rmsf -s ${PROTEIN}_final.tpr -f ${PROTEIN}_prod.xtc -o analysis/${PROTEIN}_rmsf.xvg -res
|
| 1118 |
+
|
| 1119 |
+
# 3. Radius of gyration
|
| 1120 |
+
echo "Calculating radius of gyration..."
|
| 1121 |
+
echo "Protein" | gmx gyrate -s ${PROTEIN}_final.tpr -f ${PROTEIN}_prod.xtc -o analysis/${PROTEIN}_gyrate.xvg
|
| 1122 |
+
|
| 1123 |
+
# 4. Hydrogen bonds
|
| 1124 |
+
echo "Analyzing hydrogen bonds..."
|
| 1125 |
+
echo "Protein" | gmx hbond -s ${PROTEIN}_final.tpr -f ${PROTEIN}_prod.xtc -num analysis/${PROTEIN}_hbonds.xvg
|
| 1126 |
+
|
| 1127 |
+
# 5. Energy analysis
|
| 1128 |
+
echo "Analyzing energies..."
|
| 1129 |
+
gmx energy -f ${PROTEIN}_prod.edr -o analysis/${PROTEIN}_energy.xvg
|
| 1130 |
+
|
| 1131 |
+
# 6. Generate plots
|
| 1132 |
+
echo "Generating analysis plots..."
|
| 1133 |
+
python3 plot_analysis.py ${PROTEIN}
|
| 1134 |
+
|
| 1135 |
+
echo "Analysis completed! Results saved in analysis/ directory"
|
| 1136 |
+
`;
|
| 1137 |
+
}
|
| 1138 |
+
|
| 1139 |
+
displayGeneratedFiles() {
|
| 1140 |
+
const filesList = document.getElementById('files-list');
|
| 1141 |
+
filesList.innerHTML = '';
|
| 1142 |
+
|
| 1143 |
+
Object.entries(this.generatedFiles).forEach(([filename, content]) => {
|
| 1144 |
+
const fileItem = document.createElement('div');
|
| 1145 |
+
fileItem.className = 'file-item';
|
| 1146 |
+
|
| 1147 |
+
const fileType = this.getFileType(filename);
|
| 1148 |
+
const fileSize = this.formatFileSize(content.length);
|
| 1149 |
+
|
| 1150 |
+
fileItem.innerHTML = `
|
| 1151 |
+
<h4><i class="fas ${this.getFileIcon(filename)}"></i> ${filename}</h4>
|
| 1152 |
+
<p><strong>Type:</strong> ${fileType}</p>
|
| 1153 |
+
<p><strong>Size:</strong> ${fileSize}</p>
|
| 1154 |
+
<button class="btn btn-secondary btn-sm" onclick="mdPipeline.previewFile('${filename}')">
|
| 1155 |
+
<i class="fas fa-eye"></i> Preview
|
| 1156 |
+
</button>
|
| 1157 |
+
<button class="btn btn-primary btn-sm" onclick="mdPipeline.downloadFile('${filename}')">
|
| 1158 |
+
<i class="fas fa-download"></i> Download
|
| 1159 |
+
</button>
|
| 1160 |
+
`;
|
| 1161 |
+
|
| 1162 |
+
filesList.appendChild(fileItem);
|
| 1163 |
+
});
|
| 1164 |
+
}
|
| 1165 |
+
|
| 1166 |
+
getFileType(filename) {
|
| 1167 |
+
const extension = filename.split('.').pop().toLowerCase();
|
| 1168 |
+
const types = {
|
| 1169 |
+
'mdp': 'GROMACS MDP',
|
| 1170 |
+
'pbs': 'PBS Script',
|
| 1171 |
+
'sh': 'Shell Script',
|
| 1172 |
+
'gro': 'GROMACS Structure',
|
| 1173 |
+
'top': 'GROMACS Topology',
|
| 1174 |
+
'xvg': 'GROMACS Data'
|
| 1175 |
+
};
|
| 1176 |
+
return types[extension] || 'Text File';
|
| 1177 |
+
}
|
| 1178 |
+
|
| 1179 |
+
getFileIcon(filename) {
|
| 1180 |
+
const extension = filename.split('.').pop().toLowerCase();
|
| 1181 |
+
const icons = {
|
| 1182 |
+
'mdp': 'fa-cogs',
|
| 1183 |
+
'pbs': 'fa-tasks',
|
| 1184 |
+
'sh': 'fa-terminal',
|
| 1185 |
+
'gro': 'fa-cube',
|
| 1186 |
+
'top': 'fa-sitemap',
|
| 1187 |
+
'xvg': 'fa-chart-line'
|
| 1188 |
+
};
|
| 1189 |
+
return icons[extension] || 'fa-file';
|
| 1190 |
+
}
|
| 1191 |
+
|
| 1192 |
+
formatFileSize(bytes) {
|
| 1193 |
+
if (bytes < 1024) return bytes + ' B';
|
| 1194 |
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
| 1195 |
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
| 1196 |
+
}
|
| 1197 |
+
|
| 1198 |
+
previewFile(filename) {
|
| 1199 |
+
const content = this.generatedFiles[filename];
|
| 1200 |
+
const previewWindow = window.open('', '_blank', 'width=800,height=600');
|
| 1201 |
+
previewWindow.document.write(`
|
| 1202 |
+
<html>
|
| 1203 |
+
<head>
|
| 1204 |
+
<title>Preview: ${filename}</title>
|
| 1205 |
+
<style>
|
| 1206 |
+
body { font-family: monospace; margin: 20px; background: #f5f5f5; }
|
| 1207 |
+
pre { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); }
|
| 1208 |
+
h1 { color: #333; }
|
| 1209 |
+
</style>
|
| 1210 |
+
</head>
|
| 1211 |
+
<body>
|
| 1212 |
+
<h1>${filename}</h1>
|
| 1213 |
+
<pre>${content}</pre>
|
| 1214 |
+
</body>
|
| 1215 |
+
</html>
|
| 1216 |
+
`);
|
| 1217 |
+
}
|
| 1218 |
+
|
| 1219 |
+
downloadFile(filename) {
|
| 1220 |
+
const content = this.generatedFiles[filename];
|
| 1221 |
+
const blob = new Blob([content], { type: 'text/plain' });
|
| 1222 |
+
const url = URL.createObjectURL(blob);
|
| 1223 |
+
const a = document.createElement('a');
|
| 1224 |
+
a.href = url;
|
| 1225 |
+
a.download = filename;
|
| 1226 |
+
document.body.appendChild(a);
|
| 1227 |
+
a.click();
|
| 1228 |
+
document.body.removeChild(a);
|
| 1229 |
+
URL.revokeObjectURL(url);
|
| 1230 |
+
}
|
| 1231 |
+
|
| 1232 |
+
async previewFiles() {
|
| 1233 |
+
try {
|
| 1234 |
+
const resp = await fetch('/api/get-generated-files');
|
| 1235 |
+
const data = await resp.json();
|
| 1236 |
+
if (!data.success) {
|
| 1237 |
+
alert('❌ Error: ' + (data.error || 'Unable to load files'));
|
| 1238 |
+
return;
|
| 1239 |
+
}
|
| 1240 |
+
const filesList = document.getElementById('files-list');
|
| 1241 |
+
if (!filesList) return;
|
| 1242 |
+
filesList.innerHTML = '';
|
| 1243 |
+
|
| 1244 |
+
// Store file contents for modal display
|
| 1245 |
+
this.fileContents = data.files;
|
| 1246 |
+
|
| 1247 |
+
Object.entries(data.files).forEach(([name, content]) => {
|
| 1248 |
+
const fileItem = document.createElement('div');
|
| 1249 |
+
fileItem.className = 'file-item';
|
| 1250 |
+
fileItem.style.cssText = 'padding: 10px; margin: 5px 0; border: 1px solid #ddd; border-radius: 5px; cursor: pointer; background: #f9f9f9;';
|
| 1251 |
+
fileItem.innerHTML = `<strong>${name}</strong>`;
|
| 1252 |
+
fileItem.onclick = () => this.showFileContent(name, content);
|
| 1253 |
+
filesList.appendChild(fileItem);
|
| 1254 |
+
});
|
| 1255 |
+
|
| 1256 |
+
// Reveal preview and download areas
|
| 1257 |
+
const preview = document.getElementById('files-preview');
|
| 1258 |
+
if (preview) preview.style.display = 'block';
|
| 1259 |
+
const dl = document.getElementById('download-section');
|
| 1260 |
+
if (dl) dl.style.display = 'block';
|
| 1261 |
+
this.switchTab('file-generation');
|
| 1262 |
+
} catch (e) {
|
| 1263 |
+
console.error('Preview error:', e);
|
| 1264 |
+
alert('❌ Failed to preview files: ' + e.message);
|
| 1265 |
+
}
|
| 1266 |
+
}
|
| 1267 |
+
|
| 1268 |
+
showFileContent(filename, content) {
|
| 1269 |
+
// Create modal if it doesn't exist
|
| 1270 |
+
let modal = document.getElementById('file-content-modal');
|
| 1271 |
+
if (!modal) {
|
| 1272 |
+
modal = document.createElement('div');
|
| 1273 |
+
modal.id = 'file-content-modal';
|
| 1274 |
+
modal.style.cssText = `
|
| 1275 |
+
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
| 1276 |
+
background: rgba(0,0,0,0.5); z-index: 1000; display: none;
|
| 1277 |
+
`;
|
| 1278 |
+
modal.innerHTML = `
|
| 1279 |
+
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
| 1280 |
+
background: white; border-radius: 10px; padding: 20px; max-width: 80%; max-height: 80%;
|
| 1281 |
+
overflow: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3);">
|
| 1282 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
| 1283 |
+
<h3 id="modal-filename" style="margin: 0; color: #333;"></h3>
|
| 1284 |
+
<button id="close-modal" style="background: #dc3545; color: white; border: none;
|
| 1285 |
+
border-radius: 5px; padding: 8px 15px; cursor: pointer;">Close</button>
|
| 1286 |
+
</div>
|
| 1287 |
+
<pre id="modal-content" style="background: #f8f9fa; padding: 15px; border-radius: 5px;
|
| 1288 |
+
overflow: auto; max-height: 60vh; white-space: pre-wrap; font-family: monospace;"></pre>
|
| 1289 |
+
</div>
|
| 1290 |
+
`;
|
| 1291 |
+
document.body.appendChild(modal);
|
| 1292 |
+
|
| 1293 |
+
// Close modal handlers
|
| 1294 |
+
document.getElementById('close-modal').onclick = () => modal.style.display = 'none';
|
| 1295 |
+
modal.onclick = (e) => {
|
| 1296 |
+
if (e.target === modal) modal.style.display = 'none';
|
| 1297 |
+
};
|
| 1298 |
+
}
|
| 1299 |
+
|
| 1300 |
+
// Populate and show modal
|
| 1301 |
+
document.getElementById('modal-filename').textContent = filename;
|
| 1302 |
+
document.getElementById('modal-content').textContent = content;
|
| 1303 |
+
modal.style.display = 'block';
|
| 1304 |
+
}
|
| 1305 |
+
|
| 1306 |
+
async downloadZip() {
|
| 1307 |
+
try {
|
| 1308 |
+
const resp = await fetch('/api/download-output-zip');
|
| 1309 |
+
if (!resp.ok) {
|
| 1310 |
+
const text = await resp.text();
|
| 1311 |
+
throw new Error(text || 'Failed to create ZIP');
|
| 1312 |
+
}
|
| 1313 |
+
const blob = await resp.blob();
|
| 1314 |
+
const url = window.URL.createObjectURL(blob);
|
| 1315 |
+
const a = document.createElement('a');
|
| 1316 |
+
a.href = url;
|
| 1317 |
+
a.download = 'output.zip';
|
| 1318 |
+
document.body.appendChild(a);
|
| 1319 |
+
a.click();
|
| 1320 |
+
a.remove();
|
| 1321 |
+
window.URL.revokeObjectURL(url);
|
| 1322 |
+
} catch (e) {
|
| 1323 |
+
console.error('Download error:', e);
|
| 1324 |
+
alert('❌ Failed to download ZIP: ' + e.message);
|
| 1325 |
+
}
|
| 1326 |
+
}
|
| 1327 |
+
|
| 1328 |
+
async previewSolvatedProtein() {
|
| 1329 |
+
try {
|
| 1330 |
+
// Show loading state
|
| 1331 |
+
const button = document.getElementById('preview-solvated');
|
| 1332 |
+
const originalText = button.innerHTML;
|
| 1333 |
+
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Loading...';
|
| 1334 |
+
button.disabled = true;
|
| 1335 |
+
|
| 1336 |
+
// Fetch a single viewer PDB that marks ligands as HETATM within protein_solvated frame
|
| 1337 |
+
const response = await fetch('/api/get-viewer-pdb');
|
| 1338 |
+
if (!response.ok) {
|
| 1339 |
+
throw new Error('Viewer PDB not available. Please generate files first.');
|
| 1340 |
+
}
|
| 1341 |
+
|
| 1342 |
+
const data = await response.json();
|
| 1343 |
+
if (!data.success) {
|
| 1344 |
+
throw new Error(data.error || 'Failed to load viewer PDB');
|
| 1345 |
+
}
|
| 1346 |
+
|
| 1347 |
+
// Create a new window to display the solvated protein
|
| 1348 |
+
const previewWindow = window.open('', '_blank', 'width=1200,height=800');
|
| 1349 |
+
previewWindow.document.write(`
|
| 1350 |
+
<!DOCTYPE html>
|
| 1351 |
+
<html>
|
| 1352 |
+
<head>
|
| 1353 |
+
<title>Solvated Protein Preview</title>
|
| 1354 |
+
<script src="https://unpkg.com/ngl@2.0.0-dev.35/dist/ngl.js"></script>
|
| 1355 |
+
<style>
|
| 1356 |
+
body { margin: 0; padding: 20px; font-family: Arial, sans-serif; background: #f5f5f5; }
|
| 1357 |
+
.header { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
| 1358 |
+
.viewer-container { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
| 1359 |
+
#ngl-viewer { width: 100%; height: 600px; border: 1px solid #ddd; border-radius: 4px; }
|
| 1360 |
+
.controls { margin-top: 15px; }
|
| 1361 |
+
.btn { padding: 8px 16px; margin: 5px; border: none; border-radius: 4px; cursor: pointer; }
|
| 1362 |
+
.btn-primary { background: #007bff; color: white; }
|
| 1363 |
+
.btn-secondary { background: #6c757d; color: white; }
|
| 1364 |
+
.btn-info { background: #17a2b8; color: white; }
|
| 1365 |
+
.loading { text-align: center; padding: 50px; color: #666; }
|
| 1366 |
+
.error { color: #dc3545; background: #f8d7da; padding: 15px; border-radius: 4px; margin: 20px 0; }
|
| 1367 |
+
.retry-btn { background: #28a745; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; margin-top: 10px; }
|
| 1368 |
+
</style>
|
| 1369 |
+
</head>
|
| 1370 |
+
<body>
|
| 1371 |
+
<div class="header">
|
| 1372 |
+
<h2>💧 Solvated Protein Structure</h2>
|
| 1373 |
+
<p>This is the protein structure after solvation with water molecules and ions.</p>
|
| 1374 |
+
</div>
|
| 1375 |
+
<div class="viewer-container">
|
| 1376 |
+
<div id="ngl-viewer">
|
| 1377 |
+
<div class="loading">Loading 3D viewer...</div>
|
| 1378 |
+
</div>
|
| 1379 |
+
<div class="controls">
|
| 1380 |
+
<button class="btn btn-primary" onclick="resetView()">Reset View</button>
|
| 1381 |
+
<button class="btn btn-secondary" onclick="toggleRepresentation()">Toggle Style</button>
|
| 1382 |
+
<button class="btn btn-info" onclick="toggleSpin()">Toggle Spin</button>
|
| 1383 |
+
</div>
|
| 1384 |
+
</div>
|
| 1385 |
+
<script>
|
| 1386 |
+
let stage;
|
| 1387 |
+
let currentRepresentation = 'cartoon';
|
| 1388 |
+
let isSpinning = false;
|
| 1389 |
+
let pdbContent = \`${data.content}\`;
|
| 1390 |
+
|
| 1391 |
+
// Wait for NGL to load with multiple fallbacks
|
| 1392 |
+
function waitForNGL() {
|
| 1393 |
+
return new Promise((resolve, reject) => {
|
| 1394 |
+
// Check if NGL is already available
|
| 1395 |
+
if (typeof NGL !== 'undefined') {
|
| 1396 |
+
resolve();
|
| 1397 |
+
return;
|
| 1398 |
+
}
|
| 1399 |
+
|
| 1400 |
+
let attempts = 0;
|
| 1401 |
+
const maxAttempts = 100; // 10 seconds max
|
| 1402 |
+
|
| 1403 |
+
const checkNGL = () => {
|
| 1404 |
+
attempts++;
|
| 1405 |
+
if (typeof NGL !== 'undefined') {
|
| 1406 |
+
resolve();
|
| 1407 |
+
} else if (attempts >= maxAttempts) {
|
| 1408 |
+
// Try alternative CDN
|
| 1409 |
+
loadAlternativeNGL().then(resolve).catch(() => {
|
| 1410 |
+
reject(new Error('NGL.js failed to load from all sources'));
|
| 1411 |
+
});
|
| 1412 |
+
} else {
|
| 1413 |
+
setTimeout(checkNGL, 100);
|
| 1414 |
+
}
|
| 1415 |
+
};
|
| 1416 |
+
|
| 1417 |
+
checkNGL();
|
| 1418 |
+
});
|
| 1419 |
+
}
|
| 1420 |
+
|
| 1421 |
+
// Fallback to alternative CDN
|
| 1422 |
+
function loadAlternativeNGL() {
|
| 1423 |
+
return new Promise((resolve, reject) => {
|
| 1424 |
+
const script = document.createElement('script');
|
| 1425 |
+
script.src = 'https://cdn.jsdelivr.net/npm/ngl@2.0.0-dev.35/dist/ngl.js';
|
| 1426 |
+
script.onload = () => {
|
| 1427 |
+
if (typeof NGL !== 'undefined') {
|
| 1428 |
+
resolve();
|
| 1429 |
+
} else {
|
| 1430 |
+
reject(new Error('Alternative CDN failed'));
|
| 1431 |
+
}
|
| 1432 |
+
};
|
| 1433 |
+
script.onerror = () => reject(new Error('Alternative CDN failed'));
|
| 1434 |
+
document.head.appendChild(script);
|
| 1435 |
+
});
|
| 1436 |
+
}
|
| 1437 |
+
|
| 1438 |
+
async function initViewer() {
|
| 1439 |
+
try {
|
| 1440 |
+
// Wait for NGL to be available
|
| 1441 |
+
await waitForNGL();
|
| 1442 |
+
|
| 1443 |
+
// Clear loading message
|
| 1444 |
+
document.getElementById('ngl-viewer').innerHTML = '';
|
| 1445 |
+
|
| 1446 |
+
stage = new NGL.Stage("ngl-viewer", {
|
| 1447 |
+
backgroundColor: "white",
|
| 1448 |
+
quality: "medium"
|
| 1449 |
+
});
|
| 1450 |
+
|
| 1451 |
+
// Create a blob from PDB content
|
| 1452 |
+
const blob = new Blob([pdbContent], { type: 'text/plain' });
|
| 1453 |
+
const url = URL.createObjectURL(blob);
|
| 1454 |
+
|
| 1455 |
+
// Load the structure
|
| 1456 |
+
const component = await stage.loadFile(url, {
|
| 1457 |
+
ext: "pdb",
|
| 1458 |
+
defaultRepresentation: false
|
| 1459 |
+
});
|
| 1460 |
+
|
| 1461 |
+
// Add cartoon representation for protein
|
| 1462 |
+
component.addRepresentation("cartoon", {
|
| 1463 |
+
sele: "protein",
|
| 1464 |
+
colorScheme: "chainname",
|
| 1465 |
+
opacity: 0.9
|
| 1466 |
+
});
|
| 1467 |
+
|
| 1468 |
+
// Add ball and stick for water molecules
|
| 1469 |
+
component.addRepresentation("ball+stick", {
|
| 1470 |
+
sele: "water",
|
| 1471 |
+
color: "cyan",
|
| 1472 |
+
colorScheme: "uniform",
|
| 1473 |
+
radius: 0.1
|
| 1474 |
+
});
|
| 1475 |
+
|
| 1476 |
+
// (Ions not explicitly rendered to avoid overriding visuals)
|
| 1477 |
+
|
| 1478 |
+
// Add ball and stick for ligands
|
| 1479 |
+
component.addRepresentation("ball+stick", {
|
| 1480 |
+
sele: "hetero",
|
| 1481 |
+
color: "element",
|
| 1482 |
+
radius: 0.15
|
| 1483 |
+
});
|
| 1484 |
+
|
| 1485 |
+
// Single combined viewer PDB already includes ligand as HETATM
|
| 1486 |
+
|
| 1487 |
+
// Auto-fit the view
|
| 1488 |
+
stage.autoView();
|
| 1489 |
+
|
| 1490 |
+
// Clean up the blob URL
|
| 1491 |
+
URL.revokeObjectURL(url);
|
| 1492 |
+
|
| 1493 |
+
} catch (error) {
|
| 1494 |
+
console.error('Error loading structure:', error);
|
| 1495 |
+
document.getElementById('ngl-viewer').innerHTML =
|
| 1496 |
+
'<div class="error">' +
|
| 1497 |
+
'<strong>Error:</strong> Failed to load solvated protein: ' + error.message + '<br>' +
|
| 1498 |
+
'<button class="retry-btn" onclick="retryLoad()">Retry Loading</button>' +
|
| 1499 |
+
'</div>';
|
| 1500 |
+
}
|
| 1501 |
+
}
|
| 1502 |
+
|
| 1503 |
+
function retryLoad() {
|
| 1504 |
+
document.getElementById('ngl-viewer').innerHTML = '<div class="loading">Retrying...</div>';
|
| 1505 |
+
initViewer();
|
| 1506 |
+
}
|
| 1507 |
+
|
| 1508 |
+
function resetView() {
|
| 1509 |
+
if (stage) stage.autoView();
|
| 1510 |
+
}
|
| 1511 |
+
|
| 1512 |
+
function toggleRepresentation() {
|
| 1513 |
+
if (!stage) return;
|
| 1514 |
+
const components = stage.compList;
|
| 1515 |
+
if (components.length === 0) return;
|
| 1516 |
+
|
| 1517 |
+
const component = components[0];
|
| 1518 |
+
component.removeAllRepresentations();
|
| 1519 |
+
|
| 1520 |
+
if (currentRepresentation === 'cartoon') {
|
| 1521 |
+
component.addRepresentation("ball+stick", {
|
| 1522 |
+
color: "element",
|
| 1523 |
+
radius: 0.15
|
| 1524 |
+
});
|
| 1525 |
+
currentRepresentation = 'ball+stick';
|
| 1526 |
+
} else {
|
| 1527 |
+
component.addRepresentation("cartoon", {
|
| 1528 |
+
sele: "protein",
|
| 1529 |
+
colorScheme: "chainname",
|
| 1530 |
+
opacity: 0.9
|
| 1531 |
+
});
|
| 1532 |
+
component.addRepresentation("ball+stick", {
|
| 1533 |
+
sele: "water",
|
| 1534 |
+
color: "cyan",
|
| 1535 |
+
colorScheme: "uniform",
|
| 1536 |
+
radius: 0.1
|
| 1537 |
+
});
|
| 1538 |
+
// (Ions not explicitly rendered to avoid overriding visuals)
|
| 1539 |
+
component.addRepresentation("ball+stick", {
|
| 1540 |
+
sele: "hetero",
|
| 1541 |
+
color: "element",
|
| 1542 |
+
radius: 0.15
|
| 1543 |
+
});
|
| 1544 |
+
currentRepresentation = 'cartoon';
|
| 1545 |
+
}
|
| 1546 |
+
}
|
| 1547 |
+
|
| 1548 |
+
function toggleSpin() {
|
| 1549 |
+
if (!stage) return;
|
| 1550 |
+
isSpinning = !isSpinning;
|
| 1551 |
+
stage.setSpin(isSpinning);
|
| 1552 |
+
}
|
| 1553 |
+
|
| 1554 |
+
// Initialize when page loads
|
| 1555 |
+
document.addEventListener('DOMContentLoaded', initViewer);
|
| 1556 |
+
</script>
|
| 1557 |
+
</body>
|
| 1558 |
+
</html>
|
| 1559 |
+
`);
|
| 1560 |
+
previewWindow.document.close();
|
| 1561 |
+
|
| 1562 |
+
} catch (error) {
|
| 1563 |
+
console.error('Error previewing solvated protein:', error);
|
| 1564 |
+
alert('❌ Error: ' + error.message);
|
| 1565 |
+
} finally {
|
| 1566 |
+
// Restore button state
|
| 1567 |
+
const button = document.getElementById('preview-solvated');
|
| 1568 |
+
button.innerHTML = '<i class="fas fa-tint"></i> Preview Solvated Protein';
|
| 1569 |
+
button.disabled = false;
|
| 1570 |
+
}
|
| 1571 |
+
}
|
| 1572 |
+
|
| 1573 |
+
|
| 1574 |
+
|
| 1575 |
+
displaySimulationSummary() {
|
| 1576 |
+
const summaryContent = document.getElementById('summary-content');
|
| 1577 |
+
const params = this.simulationParams;
|
| 1578 |
+
const protein = this.currentProtein;
|
| 1579 |
+
|
| 1580 |
+
const totalTime = (params.steps.production.steps * params.timestep) / 1000; // Convert to ns
|
| 1581 |
+
|
| 1582 |
+
summaryContent.innerHTML = `
|
| 1583 |
+
<div class="summary-item">
|
| 1584 |
+
<h4>Protein Information</h4>
|
| 1585 |
+
<p><strong>Structure ID:</strong> ${protein.structureId}</p>
|
| 1586 |
+
<p><strong>Atoms:</strong> ${protein.atomCount.toLocaleString()}</p>
|
| 1587 |
+
<p><strong>Chains:</strong> ${protein.chains.join(', ')}</p>
|
| 1588 |
+
<p><strong>Residues:</strong> ${protein.residueCount.toLocaleString()}</p>
|
| 1589 |
+
</div>
|
| 1590 |
+
<div class="summary-item">
|
| 1591 |
+
<h4>System Components</h4>
|
| 1592 |
+
<p><strong>Water molecules:</strong> ${protein.waterMolecules.toLocaleString()}</p>
|
| 1593 |
+
<p><strong>Ions:</strong> ${protein.ions.toLocaleString()}</p>
|
| 1594 |
+
<p><strong>Ligands:</strong> ${protein.ligands.length > 0 ? protein.ligands.join(', ') : 'None'}</p>
|
| 1595 |
+
<p><strong>HETATM entries:</strong> ${protein.hetatoms.toLocaleString()}</p>
|
| 1596 |
+
</div>
|
| 1597 |
+
<div class="summary-item">
|
| 1598 |
+
<h4>Simulation Box</h4>
|
| 1599 |
+
<p><strong>Type:</strong> ${params.boxType}</p>
|
| 1600 |
+
<p><strong>Size:</strong> ${params.boxSize} nm</p>
|
| 1601 |
+
<p><strong>Margin:</strong> ${params.boxMargin} nm</p>
|
| 1602 |
+
</div>
|
| 1603 |
+
<div class="summary-item">
|
| 1604 |
+
<h4>Force Field & Water</h4>
|
| 1605 |
+
<p><strong>Force Field:</strong> ${params.forceField}</p>
|
| 1606 |
+
<p><strong>Water Model:</strong> ${params.waterModel}</p>
|
| 1607 |
+
<p><strong>Ion Conc.:</strong> ${params.ionConcentration} mM</p>
|
| 1608 |
+
</div>
|
| 1609 |
+
<div class="summary-item">
|
| 1610 |
+
<h4>Simulation Parameters</h4>
|
| 1611 |
+
<p><strong>Temperature:</strong> ${params.temperature} K</p>
|
| 1612 |
+
<p><strong>Pressure:</strong> ${params.pressure} bar</p>
|
| 1613 |
+
<p><strong>Time Step:</strong> ${params.timestep} ps</p>
|
| 1614 |
+
</div>
|
| 1615 |
+
<div class="summary-item">
|
| 1616 |
+
<h4>Simulation Time</h4>
|
| 1617 |
+
<p><strong>Total Time:</strong> ${totalTime.toFixed(2)} ns</p>
|
| 1618 |
+
<p><strong>Steps:</strong> ${params.steps.production.steps.toLocaleString()}</p>
|
| 1619 |
+
<p><strong>Output Freq:</strong> Every 5 ps</p>
|
| 1620 |
+
</div>
|
| 1621 |
+
<div class="summary-item">
|
| 1622 |
+
<h4>Generated Files</h4>
|
| 1623 |
+
<p><strong>MDP Files:</strong> 6</p>
|
| 1624 |
+
<p><strong>Scripts:</strong> 3</p>
|
| 1625 |
+
<p><strong>Total Size:</strong> ${this.formatFileSize(Object.values(this.generatedFiles).join('').length)}</p>
|
| 1626 |
+
</div>
|
| 1627 |
+
`;
|
| 1628 |
+
}
|
| 1629 |
+
|
| 1630 |
+
// 3D Visualization Methods
|
| 1631 |
+
async load3DVisualization() {
|
| 1632 |
+
if (!this.currentProtein) return;
|
| 1633 |
+
|
| 1634 |
+
try {
|
| 1635 |
+
// Initialize NGL stage if not already done
|
| 1636 |
+
if (!this.nglStage) {
|
| 1637 |
+
this.nglStage = new NGL.Stage("ngl-viewer", {
|
| 1638 |
+
backgroundColor: "white",
|
| 1639 |
+
quality: "medium"
|
| 1640 |
+
});
|
| 1641 |
+
}
|
| 1642 |
+
|
| 1643 |
+
// Clear existing components
|
| 1644 |
+
this.nglStage.removeAllComponents();
|
| 1645 |
+
|
| 1646 |
+
// Create a blob from PDB content
|
| 1647 |
+
const blob = new Blob([this.currentProtein.content], { type: 'text/plain' });
|
| 1648 |
+
const url = URL.createObjectURL(blob);
|
| 1649 |
+
|
| 1650 |
+
// Load the structure
|
| 1651 |
+
const component = await this.nglStage.loadFile(url, {
|
| 1652 |
+
ext: "pdb",
|
| 1653 |
+
defaultRepresentation: false
|
| 1654 |
+
});
|
| 1655 |
+
|
| 1656 |
+
// Add cartoon representation for protein with chain-based colors
|
| 1657 |
+
component.addRepresentation("cartoon", {
|
| 1658 |
+
sele: "protein",
|
| 1659 |
+
colorScheme: "chainname",
|
| 1660 |
+
opacity: 0.9
|
| 1661 |
+
});
|
| 1662 |
+
|
| 1663 |
+
// Add ball and stick for water molecules
|
| 1664 |
+
if (this.currentProtein.waterMolecules > 0) {
|
| 1665 |
+
component.addRepresentation("ball+stick", {
|
| 1666 |
+
sele: "water",
|
| 1667 |
+
color: "cyan",
|
| 1668 |
+
colorScheme: "uniform",
|
| 1669 |
+
radius: 0.1
|
| 1670 |
+
});
|
| 1671 |
+
}
|
| 1672 |
+
|
| 1673 |
+
// Add ball and stick for ions
|
| 1674 |
+
if (this.currentProtein.ions > 0) {
|
| 1675 |
+
component.addRepresentation("ball+stick", {
|
| 1676 |
+
sele: "ion",
|
| 1677 |
+
color: "element",
|
| 1678 |
+
radius: 0.2
|
| 1679 |
+
});
|
| 1680 |
+
}
|
| 1681 |
+
|
| 1682 |
+
// Add ball and stick for ligands
|
| 1683 |
+
if (this.currentProtein.ligands.length > 0) {
|
| 1684 |
+
component.addRepresentation("ball+stick", {
|
| 1685 |
+
sele: "hetero",
|
| 1686 |
+
color: "element",
|
| 1687 |
+
radius: 0.15
|
| 1688 |
+
});
|
| 1689 |
+
}
|
| 1690 |
+
|
| 1691 |
+
// Auto-fit the view
|
| 1692 |
+
this.nglStage.autoView();
|
| 1693 |
+
|
| 1694 |
+
// Show controls
|
| 1695 |
+
document.getElementById('viewer-controls').style.display = 'flex';
|
| 1696 |
+
|
| 1697 |
+
// Clean up the blob URL
|
| 1698 |
+
URL.revokeObjectURL(url);
|
| 1699 |
+
|
| 1700 |
+
} catch (error) {
|
| 1701 |
+
console.error('Error loading 3D visualization:', error);
|
| 1702 |
+
this.showStatus('error', 'Error loading 3D visualization: ' + error.message);
|
| 1703 |
+
}
|
| 1704 |
+
}
|
| 1705 |
+
|
| 1706 |
+
resetView() {
|
| 1707 |
+
if (this.nglStage) {
|
| 1708 |
+
this.nglStage.autoView();
|
| 1709 |
+
}
|
| 1710 |
+
}
|
| 1711 |
+
|
| 1712 |
+
toggleRepresentation() {
|
| 1713 |
+
if (!this.nglStage) return;
|
| 1714 |
+
|
| 1715 |
+
const components = this.nglStage.compList;
|
| 1716 |
+
if (components.length === 0) return;
|
| 1717 |
+
|
| 1718 |
+
const component = components[0];
|
| 1719 |
+
component.removeAllRepresentations();
|
| 1720 |
+
|
| 1721 |
+
if (this.currentRepresentation === 'cartoon') {
|
| 1722 |
+
// Switch to ball and stick for everything
|
| 1723 |
+
component.addRepresentation("ball+stick", {
|
| 1724 |
+
color: "element",
|
| 1725 |
+
radius: 0.15
|
| 1726 |
+
});
|
| 1727 |
+
this.currentRepresentation = 'ball+stick';
|
| 1728 |
+
document.getElementById('style-text').textContent = 'Ball & Stick';
|
| 1729 |
+
} else if (this.currentRepresentation === 'ball+stick') {
|
| 1730 |
+
// Switch to surface (protein only) + ball&stick for others
|
| 1731 |
+
component.addRepresentation("surface", {
|
| 1732 |
+
sele: "protein",
|
| 1733 |
+
colorScheme: "chainname",
|
| 1734 |
+
opacity: 0.7
|
| 1735 |
+
});
|
| 1736 |
+
|
| 1737 |
+
// Add ball and stick for water molecules
|
| 1738 |
+
if (this.currentProtein.waterMolecules > 0) {
|
| 1739 |
+
component.addRepresentation("ball+stick", {
|
| 1740 |
+
sele: "water",
|
| 1741 |
+
color: "cyan",
|
| 1742 |
+
colorScheme: "uniform",
|
| 1743 |
+
radius: 0.1
|
| 1744 |
+
});
|
| 1745 |
+
}
|
| 1746 |
+
|
| 1747 |
+
// Add ball and stick for ions
|
| 1748 |
+
if (this.currentProtein.ions > 0) {
|
| 1749 |
+
component.addRepresentation("ball+stick", {
|
| 1750 |
+
sele: "ion",
|
| 1751 |
+
color: "element",
|
| 1752 |
+
radius: 0.2
|
| 1753 |
+
});
|
| 1754 |
+
}
|
| 1755 |
+
|
| 1756 |
+
// Add ball and stick for ligands
|
| 1757 |
+
if (this.currentProtein.ligands.length > 0) {
|
| 1758 |
+
component.addRepresentation("ball+stick", {
|
| 1759 |
+
sele: "hetero",
|
| 1760 |
+
color: "element",
|
| 1761 |
+
radius: 0.15
|
| 1762 |
+
});
|
| 1763 |
+
}
|
| 1764 |
+
|
| 1765 |
+
this.currentRepresentation = 'surface';
|
| 1766 |
+
document.getElementById('style-text').textContent = 'Surface';
|
| 1767 |
+
} else {
|
| 1768 |
+
// Switch back to mixed representation (protein ribbon + others ball&stick)
|
| 1769 |
+
component.addRepresentation("cartoon", {
|
| 1770 |
+
sele: "protein",
|
| 1771 |
+
colorScheme: "chainname",
|
| 1772 |
+
opacity: 0.8
|
| 1773 |
+
});
|
| 1774 |
+
|
| 1775 |
+
// Add ball and stick for water molecules
|
| 1776 |
+
if (this.currentProtein.waterMolecules > 0) {
|
| 1777 |
+
component.addRepresentation("ball+stick", {
|
| 1778 |
+
sele: "water",
|
| 1779 |
+
color: "cyan",
|
| 1780 |
+
colorScheme: "uniform",
|
| 1781 |
+
radius: 0.1
|
| 1782 |
+
});
|
| 1783 |
+
}
|
| 1784 |
+
|
| 1785 |
+
// Add ball and stick for ions
|
| 1786 |
+
if (this.currentProtein.ions > 0) {
|
| 1787 |
+
component.addRepresentation("ball+stick", {
|
| 1788 |
+
sele: "ion",
|
| 1789 |
+
color: "element",
|
| 1790 |
+
radius: 0.2
|
| 1791 |
+
});
|
| 1792 |
+
}
|
| 1793 |
+
|
| 1794 |
+
// Add ball and stick for ligands
|
| 1795 |
+
if (this.currentProtein.ligands.length > 0) {
|
| 1796 |
+
component.addRepresentation("ball+stick", {
|
| 1797 |
+
sele: "hetero",
|
| 1798 |
+
color: "element",
|
| 1799 |
+
radius: 0.15
|
| 1800 |
+
});
|
| 1801 |
+
}
|
| 1802 |
+
|
| 1803 |
+
this.currentRepresentation = 'cartoon';
|
| 1804 |
+
document.getElementById('style-text').textContent = 'Mixed View';
|
| 1805 |
+
}
|
| 1806 |
+
}
|
| 1807 |
+
|
| 1808 |
+
toggleSpin() {
|
| 1809 |
+
if (!this.nglStage) return;
|
| 1810 |
+
|
| 1811 |
+
this.isSpinning = !this.isSpinning;
|
| 1812 |
+
this.nglStage.setSpin(this.isSpinning);
|
| 1813 |
+
}
|
| 1814 |
+
|
| 1815 |
+
// Structure Preparation Methods
|
| 1816 |
+
async prepareStructure() {
|
| 1817 |
+
if (!this.currentProtein) {
|
| 1818 |
+
alert('Please load a protein structure first');
|
| 1819 |
+
return;
|
| 1820 |
+
}
|
| 1821 |
+
|
| 1822 |
+
// Get preparation options
|
| 1823 |
+
const options = {
|
| 1824 |
+
remove_water: document.getElementById('remove-water').checked,
|
| 1825 |
+
remove_ions: document.getElementById('remove-ions').checked,
|
| 1826 |
+
remove_hydrogens: document.getElementById('remove-hydrogens').checked,
|
| 1827 |
+
add_nme: document.getElementById('add-nme').checked,
|
| 1828 |
+
add_ace: document.getElementById('add-ace').checked,
|
| 1829 |
+
preserve_ligands: document.getElementById('preserve-ligands').checked,
|
| 1830 |
+
separate_ligands: document.getElementById('separate-ligands').checked,
|
| 1831 |
+
selected_chains: this.getSelectedChains(),
|
| 1832 |
+
selected_ligands: this.getSelectedLigands()
|
| 1833 |
+
};
|
| 1834 |
+
|
| 1835 |
+
// Show status
|
| 1836 |
+
document.getElementById('prep-status').style.display = 'block';
|
| 1837 |
+
document.getElementById('prep-status-content').innerHTML = `
|
| 1838 |
+
<p><i class="fas fa-spinner fa-spin"></i> Preparing structure...</p>
|
| 1839 |
+
`;
|
| 1840 |
+
|
| 1841 |
+
try {
|
| 1842 |
+
// Call Python backend
|
| 1843 |
+
const response = await fetch('/api/prepare-structure', {
|
| 1844 |
+
method: 'POST',
|
| 1845 |
+
headers: {
|
| 1846 |
+
'Content-Type': 'application/json',
|
| 1847 |
+
},
|
| 1848 |
+
body: JSON.stringify({
|
| 1849 |
+
pdb_content: this.currentProtein.content,
|
| 1850 |
+
options: options
|
| 1851 |
+
})
|
| 1852 |
+
});
|
| 1853 |
+
|
| 1854 |
+
const result = await response.json();
|
| 1855 |
+
|
| 1856 |
+
if (result.success) {
|
| 1857 |
+
// Store prepared structure
|
| 1858 |
+
this.preparedProtein = {
|
| 1859 |
+
content: result.prepared_structure,
|
| 1860 |
+
original_atoms: result.original_atoms,
|
| 1861 |
+
prepared_atoms: result.prepared_atoms,
|
| 1862 |
+
removed_components: result.removed_components,
|
| 1863 |
+
added_capping: result.added_capping,
|
| 1864 |
+
preserved_ligands: result.preserved_ligands,
|
| 1865 |
+
ligand_present: result.ligand_present,
|
| 1866 |
+
separate_ligands: result.separate_ligands,
|
| 1867 |
+
ligand_content: result.ligand_content || ''
|
| 1868 |
+
};
|
| 1869 |
+
|
| 1870 |
+
// Format removed components
|
| 1871 |
+
const removedText = result.removed_components ?
|
| 1872 |
+
Object.entries(result.removed_components)
|
| 1873 |
+
.filter(([key, value]) => value > 0)
|
| 1874 |
+
.map(([key, value]) => `${key}: ${value}`)
|
| 1875 |
+
.join(', ') || 'None' : 'None';
|
| 1876 |
+
|
| 1877 |
+
// Format added capping
|
| 1878 |
+
const addedText = result.added_capping ?
|
| 1879 |
+
Object.entries(result.added_capping)
|
| 1880 |
+
.filter(([key, value]) => value > 0)
|
| 1881 |
+
.map(([key, value]) => `${key}: ${value}`)
|
| 1882 |
+
.join(', ') || 'None' : 'None';
|
| 1883 |
+
|
| 1884 |
+
// Update status
|
| 1885 |
+
document.getElementById('prep-status-content').innerHTML = `
|
| 1886 |
+
<p><i class="fas fa-check-circle"></i> Structure preparation completed!</p>
|
| 1887 |
+
<p><strong>Original atoms:</strong> ${result.original_atoms.toLocaleString()}</p>
|
| 1888 |
+
<p><strong>Prepared atoms:</strong> ${result.prepared_atoms.toLocaleString()}</p>
|
| 1889 |
+
<p><strong>Removed:</strong> ${removedText}</p>
|
| 1890 |
+
<p><strong>Added:</strong> ${addedText}</p>
|
| 1891 |
+
<p><strong>Ligands:</strong> ${result.preserved_ligands}</p>
|
| 1892 |
+
<p>Ready for AMBER force field generation!</p>
|
| 1893 |
+
`;
|
| 1894 |
+
|
| 1895 |
+
// Enable preview and download buttons
|
| 1896 |
+
document.getElementById('preview-prepared').disabled = false;
|
| 1897 |
+
document.getElementById('download-prepared').disabled = false;
|
| 1898 |
+
|
| 1899 |
+
// Enable ligand download button if ligands are present and separate ligands is checked
|
| 1900 |
+
const separateLigandsChecked = document.getElementById('separate-ligands').checked;
|
| 1901 |
+
const downloadLigandBtn = document.getElementById('download-ligand');
|
| 1902 |
+
if (result.ligand_present && separateLigandsChecked && result.ligand_content) {
|
| 1903 |
+
downloadLigandBtn.disabled = false;
|
| 1904 |
+
downloadLigandBtn.classList.remove('btn-outline-secondary');
|
| 1905 |
+
downloadLigandBtn.classList.add('btn-outline-primary');
|
| 1906 |
+
} else {
|
| 1907 |
+
downloadLigandBtn.disabled = true;
|
| 1908 |
+
downloadLigandBtn.classList.remove('btn-outline-primary');
|
| 1909 |
+
downloadLigandBtn.classList.add('btn-outline-secondary');
|
| 1910 |
+
}
|
| 1911 |
+
|
| 1912 |
+
// Show ligand force field group if preserve ligands is checked
|
| 1913 |
+
const preserveLigandsChecked = document.getElementById('preserve-ligands').checked;
|
| 1914 |
+
if (preserveLigandsChecked && result.ligand_present) {
|
| 1915 |
+
this.toggleLigandForceFieldGroup(true);
|
| 1916 |
+
}
|
| 1917 |
+
} else {
|
| 1918 |
+
throw new Error(result.error || 'Structure preparation failed');
|
| 1919 |
+
}
|
| 1920 |
+
} catch (error) {
|
| 1921 |
+
console.error('Error preparing structure:', error);
|
| 1922 |
+
document.getElementById('prep-status-content').innerHTML = `
|
| 1923 |
+
<p><i class="fas fa-exclamation-triangle"></i> Error preparing structure</p>
|
| 1924 |
+
<p>${error.message}</p>
|
| 1925 |
+
`;
|
| 1926 |
+
}
|
| 1927 |
+
}
|
| 1928 |
+
|
| 1929 |
+
renderChainAndLigandSelections() {
|
| 1930 |
+
if (!this.currentProtein) return;
|
| 1931 |
+
// Render chains
|
| 1932 |
+
const chainContainer = document.getElementById('chain-selection');
|
| 1933 |
+
if (chainContainer) {
|
| 1934 |
+
chainContainer.innerHTML = '';
|
| 1935 |
+
this.currentProtein.chains.forEach(chainId => {
|
| 1936 |
+
const id = `chain-${chainId}`;
|
| 1937 |
+
const wrapper = document.createElement('div');
|
| 1938 |
+
wrapper.className = 'checkbox-inline';
|
| 1939 |
+
wrapper.innerHTML = `
|
| 1940 |
+
<label class="checkbox-container">
|
| 1941 |
+
<input type="checkbox" id="${id}" data-chain="${chainId}">
|
| 1942 |
+
<span class="checkmark"></span>
|
| 1943 |
+
Chain ${chainId}
|
| 1944 |
+
</label>`;
|
| 1945 |
+
chainContainer.appendChild(wrapper);
|
| 1946 |
+
});
|
| 1947 |
+
}
|
| 1948 |
+
|
| 1949 |
+
// Render ligands (RESN-CHAIN groups)
|
| 1950 |
+
const ligandContainer = document.getElementById('ligand-selection');
|
| 1951 |
+
if (ligandContainer) {
|
| 1952 |
+
ligandContainer.innerHTML = '';
|
| 1953 |
+
if (Array.isArray(this.currentProtein.ligandGroups) && this.currentProtein.ligandGroups.length > 0) {
|
| 1954 |
+
this.currentProtein.ligandGroups.forEach(l => {
|
| 1955 |
+
const key = `${l.resn}-${l.chain}`;
|
| 1956 |
+
const id = `lig-${key}`;
|
| 1957 |
+
const wrapper = document.createElement('div');
|
| 1958 |
+
wrapper.className = 'checkbox-inline';
|
| 1959 |
+
wrapper.innerHTML = `
|
| 1960 |
+
<label class="checkbox-container">
|
| 1961 |
+
<input type="checkbox" id="${id}" data-resn="${l.resn}" data-chain="${l.chain}">
|
| 1962 |
+
<span class="checkmark"></span>
|
| 1963 |
+
${key}
|
| 1964 |
+
</label>`;
|
| 1965 |
+
ligandContainer.appendChild(wrapper);
|
| 1966 |
+
});
|
| 1967 |
+
} else {
|
| 1968 |
+
// Fallback: show unique ligand names if detailed positions not parsed
|
| 1969 |
+
if (Array.isArray(this.currentProtein.ligands) && this.currentProtein.ligands.length > 0) {
|
| 1970 |
+
this.currentProtein.ligands.forEach(resn => {
|
| 1971 |
+
const id = `lig-${resn}`;
|
| 1972 |
+
const wrapper = document.createElement('div');
|
| 1973 |
+
wrapper.className = 'checkbox-inline';
|
| 1974 |
+
wrapper.innerHTML = `
|
| 1975 |
+
<label class="checkbox-container">
|
| 1976 |
+
<input type="checkbox" id="${id}" data-resn="${resn}">
|
| 1977 |
+
<span class="checkmark"></span>
|
| 1978 |
+
${resn}
|
| 1979 |
+
</label>`;
|
| 1980 |
+
ligandContainer.appendChild(wrapper);
|
| 1981 |
+
});
|
| 1982 |
+
} else {
|
| 1983 |
+
ligandContainer.innerHTML = '<small>No ligands detected</small>';
|
| 1984 |
+
}
|
| 1985 |
+
}
|
| 1986 |
+
}
|
| 1987 |
+
}
|
| 1988 |
+
|
| 1989 |
+
getSelectedChains() {
|
| 1990 |
+
const container = document.getElementById('chain-selection');
|
| 1991 |
+
if (!container) return [];
|
| 1992 |
+
return Array.from(container.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.getAttribute('data-chain'));
|
| 1993 |
+
}
|
| 1994 |
+
|
| 1995 |
+
getSelectedLigands() {
|
| 1996 |
+
const container = document.getElementById('ligand-selection');
|
| 1997 |
+
if (!container) return [];
|
| 1998 |
+
return Array.from(container.querySelectorAll('input[type="checkbox"]:checked')).map(cb => ({
|
| 1999 |
+
resn: cb.getAttribute('data-resn') || '',
|
| 2000 |
+
chain: cb.getAttribute('data-chain') || ''
|
| 2001 |
+
}));
|
| 2002 |
+
}
|
| 2003 |
+
|
| 2004 |
+
previewPreparedStructure() {
|
| 2005 |
+
if (!this.preparedProtein) {
|
| 2006 |
+
alert('Please prepare a protein structure first');
|
| 2007 |
+
return;
|
| 2008 |
+
}
|
| 2009 |
+
|
| 2010 |
+
// Show prepared structure preview
|
| 2011 |
+
document.getElementById('prepared-structure-preview').style.display = 'block';
|
| 2012 |
+
|
| 2013 |
+
// Format removed components
|
| 2014 |
+
const removedText = this.preparedProtein.removed_components ?
|
| 2015 |
+
Object.entries(this.preparedProtein.removed_components)
|
| 2016 |
+
.filter(([key, value]) => value > 0)
|
| 2017 |
+
.map(([key, value]) => `${key}: ${value}`)
|
| 2018 |
+
.join(', ') || 'None' : 'None';
|
| 2019 |
+
|
| 2020 |
+
// Format added capping
|
| 2021 |
+
const addedText = this.preparedProtein.added_capping ?
|
| 2022 |
+
Object.entries(this.preparedProtein.added_capping)
|
| 2023 |
+
.filter(([key, value]) => value > 0)
|
| 2024 |
+
.map(([key, value]) => `${key}: ${value}`)
|
| 2025 |
+
.join(', ') || 'None' : 'None';
|
| 2026 |
+
|
| 2027 |
+
// Update structure info
|
| 2028 |
+
document.getElementById('original-atoms').textContent = this.preparedProtein.original_atoms.toLocaleString();
|
| 2029 |
+
document.getElementById('prepared-atoms').textContent = this.preparedProtein.prepared_atoms.toLocaleString();
|
| 2030 |
+
document.getElementById('removed-components').textContent = removedText;
|
| 2031 |
+
document.getElementById('added-capping').textContent = addedText;
|
| 2032 |
+
document.getElementById('preserved-ligands').textContent = this.preparedProtein.preserved_ligands;
|
| 2033 |
+
|
| 2034 |
+
// Load 3D visualization of prepared structure
|
| 2035 |
+
this.loadPrepared3DVisualization();
|
| 2036 |
+
}
|
| 2037 |
+
|
| 2038 |
+
downloadPreparedStructure() {
|
| 2039 |
+
if (!this.preparedProtein) {
|
| 2040 |
+
alert('Please prepare a structure first');
|
| 2041 |
+
return;
|
| 2042 |
+
}
|
| 2043 |
+
|
| 2044 |
+
// Download prepared structure
|
| 2045 |
+
const blob = new Blob([this.preparedProtein.content], { type: 'text/plain' });
|
| 2046 |
+
const url = URL.createObjectURL(blob);
|
| 2047 |
+
const a = document.createElement('a');
|
| 2048 |
+
a.href = url;
|
| 2049 |
+
a.download = `tleap_ready.pdb`;
|
| 2050 |
+
document.body.appendChild(a);
|
| 2051 |
+
a.click();
|
| 2052 |
+
document.body.removeChild(a);
|
| 2053 |
+
URL.revokeObjectURL(url);
|
| 2054 |
+
}
|
| 2055 |
+
|
| 2056 |
+
downloadLigandFile() {
|
| 2057 |
+
if (!this.preparedProtein || !this.preparedProtein.ligand_present || !this.preparedProtein.ligand_content) {
|
| 2058 |
+
alert('No ligand file available. Please prepare structure with separate ligands enabled.');
|
| 2059 |
+
return;
|
| 2060 |
+
}
|
| 2061 |
+
|
| 2062 |
+
// Download ligand file
|
| 2063 |
+
const ligandBlob = new Blob([this.preparedProtein.ligand_content], { type: 'text/plain' });
|
| 2064 |
+
const ligandUrl = URL.createObjectURL(ligandBlob);
|
| 2065 |
+
const ligandA = document.createElement('a');
|
| 2066 |
+
ligandA.href = ligandUrl;
|
| 2067 |
+
ligandA.download = `4_ligands_corrected.pdb`;
|
| 2068 |
+
document.body.appendChild(ligandA);
|
| 2069 |
+
ligandA.click();
|
| 2070 |
+
document.body.removeChild(ligandA);
|
| 2071 |
+
URL.revokeObjectURL(ligandUrl);
|
| 2072 |
+
}
|
| 2073 |
+
|
| 2074 |
+
// 3D Visualization for prepared structure
|
| 2075 |
+
async loadPrepared3DVisualization() {
|
| 2076 |
+
if (!this.preparedProtein) return;
|
| 2077 |
+
|
| 2078 |
+
try {
|
| 2079 |
+
// Initialize NGL stage for prepared structure if not already done
|
| 2080 |
+
if (!this.preparedNglStage) {
|
| 2081 |
+
this.preparedNglStage = new NGL.Stage("prepared-ngl-viewer", {
|
| 2082 |
+
backgroundColor: "white",
|
| 2083 |
+
quality: "medium"
|
| 2084 |
+
});
|
| 2085 |
+
}
|
| 2086 |
+
|
| 2087 |
+
// Clear existing components
|
| 2088 |
+
this.preparedNglStage.removeAllComponents();
|
| 2089 |
+
|
| 2090 |
+
// Create a blob from prepared PDB content
|
| 2091 |
+
const blob = new Blob([this.preparedProtein.content], { type: 'text/plain' });
|
| 2092 |
+
const url = URL.createObjectURL(blob);
|
| 2093 |
+
|
| 2094 |
+
// Load the prepared structure
|
| 2095 |
+
const component = await this.preparedNglStage.loadFile(url, {
|
| 2096 |
+
ext: "pdb",
|
| 2097 |
+
defaultRepresentation: false
|
| 2098 |
+
});
|
| 2099 |
+
|
| 2100 |
+
// Add cartoon representation for protein with chain-based colors
|
| 2101 |
+
component.addRepresentation("cartoon", {
|
| 2102 |
+
sele: "protein",
|
| 2103 |
+
colorScheme: "chainname",
|
| 2104 |
+
opacity: 0.9
|
| 2105 |
+
});
|
| 2106 |
+
|
| 2107 |
+
// Add ball and stick for ligands (if any) - check for HETATM records
|
| 2108 |
+
component.addRepresentation("ball+stick", {
|
| 2109 |
+
sele: "hetero",
|
| 2110 |
+
color: "element",
|
| 2111 |
+
radius: 0.2,
|
| 2112 |
+
opacity: 0.8
|
| 2113 |
+
});
|
| 2114 |
+
|
| 2115 |
+
// Auto-fit the view
|
| 2116 |
+
this.preparedNglStage.autoView();
|
| 2117 |
+
|
| 2118 |
+
// Show controls
|
| 2119 |
+
document.getElementById('prepared-viewer-controls').style.display = 'flex';
|
| 2120 |
+
|
| 2121 |
+
// Clean up the blob URL
|
| 2122 |
+
URL.revokeObjectURL(url);
|
| 2123 |
+
|
| 2124 |
+
} catch (error) {
|
| 2125 |
+
console.error('Error loading prepared 3D visualization:', error);
|
| 2126 |
+
}
|
| 2127 |
+
}
|
| 2128 |
+
|
| 2129 |
+
resetPreparedView() {
|
| 2130 |
+
if (this.preparedNglStage) {
|
| 2131 |
+
this.preparedNglStage.autoView();
|
| 2132 |
+
}
|
| 2133 |
+
}
|
| 2134 |
+
|
| 2135 |
+
togglePreparedRepresentation() {
|
| 2136 |
+
if (!this.preparedNglStage) return;
|
| 2137 |
+
|
| 2138 |
+
const components = this.preparedNglStage.compList;
|
| 2139 |
+
if (components.length === 0) return;
|
| 2140 |
+
|
| 2141 |
+
const component = components[0];
|
| 2142 |
+
component.removeAllRepresentations();
|
| 2143 |
+
|
| 2144 |
+
if (this.preparedRepresentation === 'cartoon') {
|
| 2145 |
+
// Switch to ball and stick
|
| 2146 |
+
component.addRepresentation("ball+stick", {
|
| 2147 |
+
color: "element",
|
| 2148 |
+
radius: 0.15
|
| 2149 |
+
});
|
| 2150 |
+
this.preparedRepresentation = 'ball+stick';
|
| 2151 |
+
document.getElementById('prepared-style-text').textContent = 'Ball & Stick';
|
| 2152 |
+
} else if (this.preparedRepresentation === 'ball+stick') {
|
| 2153 |
+
// Switch to surface
|
| 2154 |
+
component.addRepresentation("surface", {
|
| 2155 |
+
sele: "protein",
|
| 2156 |
+
colorScheme: "chainname",
|
| 2157 |
+
opacity: 0.7
|
| 2158 |
+
});
|
| 2159 |
+
this.preparedRepresentation = 'surface';
|
| 2160 |
+
document.getElementById('prepared-style-text').textContent = 'Surface';
|
| 2161 |
+
} else {
|
| 2162 |
+
// Switch back to cartoon
|
| 2163 |
+
component.addRepresentation("cartoon", {
|
| 2164 |
+
sele: "protein",
|
| 2165 |
+
colorScheme: "chainname",
|
| 2166 |
+
opacity: 0.8
|
| 2167 |
+
});
|
| 2168 |
+
|
| 2169 |
+
// Add ball and stick for ligands
|
| 2170 |
+
if (this.preparedProtein.preserved_ligands !== 'None') {
|
| 2171 |
+
component.addRepresentation("ball+stick", {
|
| 2172 |
+
sele: "hetero",
|
| 2173 |
+
color: "element",
|
| 2174 |
+
radius: 0.15
|
| 2175 |
+
});
|
| 2176 |
+
}
|
| 2177 |
+
|
| 2178 |
+
this.preparedRepresentation = 'cartoon';
|
| 2179 |
+
document.getElementById('prepared-style-text').textContent = 'Mixed View';
|
| 2180 |
+
}
|
| 2181 |
+
}
|
| 2182 |
+
|
| 2183 |
+
togglePreparedSpin() {
|
| 2184 |
+
if (!this.preparedNglStage) return;
|
| 2185 |
+
|
| 2186 |
+
this.preparedIsSpinning = !this.preparedIsSpinning;
|
| 2187 |
+
this.preparedNglStage.setSpin(this.preparedIsSpinning);
|
| 2188 |
+
}
|
| 2189 |
+
}
|
| 2190 |
+
|
| 2191 |
+
// Initialize the application when the page loads
|
| 2192 |
+
function initializeApp() {
|
| 2193 |
+
console.log('Initializing mdPipeline...'); // Debug log
|
| 2194 |
+
window.mdPipeline = new MDSimulationPipeline();
|
| 2195 |
+
console.log('mdPipeline initialized:', window.mdPipeline); // Debug log
|
| 2196 |
+
}
|
| 2197 |
+
|
| 2198 |
+
// Try to initialize immediately if DOM is already loaded
|
| 2199 |
+
if (document.readyState === 'loading') {
|
| 2200 |
+
document.addEventListener('DOMContentLoaded', initializeApp);
|
| 2201 |
+
} else {
|
| 2202 |
+
// DOM is already loaded
|
| 2203 |
+
initializeApp();
|
| 2204 |
+
}
|
| 2205 |
+
|
| 2206 |
+
// Add some utility functions for better UX
|
| 2207 |
+
function formatNumber(num) {
|
| 2208 |
+
return num.toLocaleString();
|
| 2209 |
+
}
|
| 2210 |
+
|
| 2211 |
+
function formatTime(seconds) {
|
| 2212 |
+
const hours = Math.floor(seconds / 3600);
|
| 2213 |
+
const minutes = Math.floor((seconds % 3600) / 60);
|
| 2214 |
+
const secs = seconds % 60;
|
| 2215 |
+
return `${hours}h ${minutes}m ${secs}s`;
|
| 2216 |
+
}
|
python/__pycache__/app.cpython-310.pyc
ADDED
|
Binary file (37.7 kB). View file
|
|
|
python/__pycache__/app.cpython-312.pyc
ADDED
|
Binary file (54.5 kB). View file
|
|
|
python/__pycache__/structure_preparation.cpython-310.pyc
ADDED
|
Binary file (21.5 kB). View file
|
|
|
python/app.py
ADDED
|
@@ -0,0 +1,1549 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
MD Simulation Pipeline - Flask Backend
|
| 4 |
+
Provides API endpoints for protein processing and file generation
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from flask import Flask, request, jsonify, send_file, render_template, send_from_directory
|
| 8 |
+
from flask_cors import CORS
|
| 9 |
+
import os
|
| 10 |
+
import json
|
| 11 |
+
import tempfile
|
| 12 |
+
import zipfile
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
import requests
|
| 15 |
+
import subprocess
|
| 16 |
+
from Bio.PDB import PDBParser, PDBList
|
| 17 |
+
import logging
|
| 18 |
+
from structure_preparation import prepare_structure, parse_structure_info
|
| 19 |
+
|
| 20 |
+
app = Flask(__name__,
|
| 21 |
+
template_folder='../html',
|
| 22 |
+
static_folder='../',
|
| 23 |
+
static_url_path='')
|
| 24 |
+
CORS(app)
|
| 25 |
+
|
| 26 |
+
# Configure logging
|
| 27 |
+
logging.basicConfig(level=logging.INFO)
|
| 28 |
+
logger = logging.getLogger(__name__)
|
| 29 |
+
|
| 30 |
+
# Create output directory
|
| 31 |
+
OUTPUT_DIR = Path("output")
|
| 32 |
+
|
| 33 |
+
def clean_and_create_output_folder():
|
| 34 |
+
"""Clean existing output folder and create a new one"""
|
| 35 |
+
try:
|
| 36 |
+
print(f"DEBUG: Starting cleanup. OUTPUT_DIR = {OUTPUT_DIR}")
|
| 37 |
+
print(f"DEBUG: OUTPUT_DIR.exists() = {OUTPUT_DIR.exists()}")
|
| 38 |
+
|
| 39 |
+
# Remove existing output folder if it exists
|
| 40 |
+
if OUTPUT_DIR.exists():
|
| 41 |
+
import shutil
|
| 42 |
+
print(f"DEBUG: Removing existing output folder: {OUTPUT_DIR}")
|
| 43 |
+
shutil.rmtree(OUTPUT_DIR)
|
| 44 |
+
print(f"DEBUG: Successfully removed output folder")
|
| 45 |
+
logger.info(f"Removed existing output folder: {OUTPUT_DIR}")
|
| 46 |
+
|
| 47 |
+
# Create new output folder
|
| 48 |
+
print(f"DEBUG: Creating new output folder: {OUTPUT_DIR}")
|
| 49 |
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 50 |
+
print(f"DEBUG: Successfully created output folder")
|
| 51 |
+
logger.info(f"Created new output folder: {OUTPUT_DIR}")
|
| 52 |
+
|
| 53 |
+
return True
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"DEBUG: Error in cleanup: {str(e)}")
|
| 56 |
+
logger.error(f"Error cleaning output folder: {str(e)}")
|
| 57 |
+
return False
|
| 58 |
+
|
| 59 |
+
class MDSimulationGenerator:
|
| 60 |
+
"""Handles MD simulation file generation and protein processing"""
|
| 61 |
+
|
| 62 |
+
def __init__(self):
|
| 63 |
+
self.pdb_parser = PDBParser(QUIET=True)
|
| 64 |
+
self.pdb_list = PDBList()
|
| 65 |
+
|
| 66 |
+
def fetch_pdb_structure(self, pdb_id):
|
| 67 |
+
"""Fetch PDB structure from RCSB"""
|
| 68 |
+
try:
|
| 69 |
+
# Download PDB file
|
| 70 |
+
pdb_file = self.pdb_list.retrieve_pdb_file(pdb_id, pdir=OUTPUT_DIR, file_format='pdb')
|
| 71 |
+
return str(pdb_file)
|
| 72 |
+
except Exception as e:
|
| 73 |
+
logger.error(f"Error fetching PDB {pdb_id}: {str(e)}")
|
| 74 |
+
raise
|
| 75 |
+
|
| 76 |
+
def parse_pdb_structure(self, pdb_file):
|
| 77 |
+
"""Parse PDB file and extract structure information"""
|
| 78 |
+
try:
|
| 79 |
+
structure = self.pdb_parser.get_structure('protein', pdb_file)
|
| 80 |
+
|
| 81 |
+
# Extract basic information
|
| 82 |
+
atom_count = 0
|
| 83 |
+
chains = set()
|
| 84 |
+
residues = set()
|
| 85 |
+
|
| 86 |
+
for model in structure:
|
| 87 |
+
for chain in model:
|
| 88 |
+
chains.add(chain.id)
|
| 89 |
+
for residue in chain:
|
| 90 |
+
if residue.id[0] == ' ': # Standard residues
|
| 91 |
+
residues.add(f"{residue.resname}{residue.id[1]}")
|
| 92 |
+
for atom in residue:
|
| 93 |
+
atom_count += 1
|
| 94 |
+
|
| 95 |
+
return {
|
| 96 |
+
'atom_count': atom_count,
|
| 97 |
+
'chains': list(chains),
|
| 98 |
+
'residue_count': len(residues),
|
| 99 |
+
'structure_id': Path(pdb_file).stem.upper()
|
| 100 |
+
}
|
| 101 |
+
except Exception as e:
|
| 102 |
+
logger.error(f"Error parsing PDB file: {str(e)}")
|
| 103 |
+
raise
|
| 104 |
+
|
| 105 |
+
def generate_mdp_file(self, params, step_type='production'):
|
| 106 |
+
"""Generate GROMACS MDP file for different simulation steps"""
|
| 107 |
+
|
| 108 |
+
if step_type == 'restrained_min':
|
| 109 |
+
return f"""; Restrained Minimization Parameters
|
| 110 |
+
integrator = steep
|
| 111 |
+
nsteps = {params['steps']['restrainedMin']['steps']}
|
| 112 |
+
emstep = 0.01
|
| 113 |
+
emtol = 1000
|
| 114 |
+
|
| 115 |
+
; Position restraints
|
| 116 |
+
define = -DPOSRES
|
| 117 |
+
refcoord_scaling = com
|
| 118 |
+
|
| 119 |
+
; Output control
|
| 120 |
+
nstxout = 100
|
| 121 |
+
nstenergy = 100
|
| 122 |
+
nstlog = 100
|
| 123 |
+
|
| 124 |
+
; Bond parameters
|
| 125 |
+
constraint_algorithm = lincs
|
| 126 |
+
constraints = h-bonds
|
| 127 |
+
|
| 128 |
+
; Neighbor searching
|
| 129 |
+
cutoff-scheme = Verlet
|
| 130 |
+
ns_type = grid
|
| 131 |
+
nstlist = 10
|
| 132 |
+
rlist = {params['cutoff']}
|
| 133 |
+
|
| 134 |
+
; Electrostatics
|
| 135 |
+
coulombtype = PME
|
| 136 |
+
rcoulomb = {params['cutoff']}
|
| 137 |
+
pme_order = {params['pmeOrder']}
|
| 138 |
+
|
| 139 |
+
; Van der Waals
|
| 140 |
+
vdwtype = Cut-off
|
| 141 |
+
rvdw = {params['cutoff']}
|
| 142 |
+
"""
|
| 143 |
+
|
| 144 |
+
elif step_type == 'minimization':
|
| 145 |
+
return f"""; Minimization Parameters
|
| 146 |
+
integrator = {params['steps']['minimization']['algorithm']}
|
| 147 |
+
nsteps = {params['steps']['minimization']['steps']}
|
| 148 |
+
emstep = 0.01
|
| 149 |
+
emtol = 1000
|
| 150 |
+
|
| 151 |
+
; Output control
|
| 152 |
+
nstxout = 100
|
| 153 |
+
nstenergy = 100
|
| 154 |
+
nstlog = 100
|
| 155 |
+
|
| 156 |
+
; Bond parameters
|
| 157 |
+
constraint_algorithm = lincs
|
| 158 |
+
constraints = h-bonds
|
| 159 |
+
|
| 160 |
+
; Neighbor searching
|
| 161 |
+
cutoff-scheme = Verlet
|
| 162 |
+
ns_type = grid
|
| 163 |
+
nstlist = 10
|
| 164 |
+
rlist = {params['cutoff']}
|
| 165 |
+
|
| 166 |
+
; Electrostatics
|
| 167 |
+
coulombtype = PME
|
| 168 |
+
rcoulomb = {params['cutoff']}
|
| 169 |
+
pme_order = {params['pmeOrder']}
|
| 170 |
+
|
| 171 |
+
; Van der Waals
|
| 172 |
+
vdwtype = Cut-off
|
| 173 |
+
rvdw = {params['cutoff']}
|
| 174 |
+
"""
|
| 175 |
+
|
| 176 |
+
elif step_type == 'nvt':
|
| 177 |
+
return f"""; NVT Equilibration Parameters
|
| 178 |
+
integrator = md
|
| 179 |
+
dt = {params['timestep']}
|
| 180 |
+
nsteps = {params['steps']['nvt']['steps']}
|
| 181 |
+
|
| 182 |
+
; Output control
|
| 183 |
+
nstxout = 5000
|
| 184 |
+
nstvout = 5000
|
| 185 |
+
nstenergy = 1000
|
| 186 |
+
nstlog = 1000
|
| 187 |
+
|
| 188 |
+
; Bond parameters
|
| 189 |
+
constraint_algorithm = lincs
|
| 190 |
+
constraints = h-bonds
|
| 191 |
+
lincs_iter = 1
|
| 192 |
+
lincs_order = 4
|
| 193 |
+
|
| 194 |
+
; Neighbor searching
|
| 195 |
+
cutoff-scheme = Verlet
|
| 196 |
+
ns_type = grid
|
| 197 |
+
nstlist = 40
|
| 198 |
+
rlist = {params['cutoff']}
|
| 199 |
+
|
| 200 |
+
; Electrostatics
|
| 201 |
+
coulombtype = PME
|
| 202 |
+
rcoulomb = {params['cutoff']}
|
| 203 |
+
pme_order = {params['pmeOrder']}
|
| 204 |
+
|
| 205 |
+
; Van der Waals
|
| 206 |
+
vdwtype = Cut-off
|
| 207 |
+
rvdw = {params['cutoff']}
|
| 208 |
+
|
| 209 |
+
; Temperature coupling
|
| 210 |
+
tcoupl = {params['couplingType']}
|
| 211 |
+
tc-grps = Protein Non-Protein
|
| 212 |
+
tau_t = 0.1 0.1
|
| 213 |
+
ref_t = {params['steps']['nvt']['temperature']} {params['steps']['nvt']['temperature']}
|
| 214 |
+
|
| 215 |
+
; Pressure coupling (disabled for NVT)
|
| 216 |
+
pcoupl = no
|
| 217 |
+
|
| 218 |
+
; Velocity generation
|
| 219 |
+
gen_vel = yes
|
| 220 |
+
gen_temp = {params['steps']['nvt']['temperature']}
|
| 221 |
+
gen_seed = -1
|
| 222 |
+
"""
|
| 223 |
+
|
| 224 |
+
elif step_type == 'npt':
|
| 225 |
+
return f"""; NPT Equilibration Parameters
|
| 226 |
+
integrator = md
|
| 227 |
+
dt = {params['timestep']}
|
| 228 |
+
nsteps = {params['steps']['npt']['steps']}
|
| 229 |
+
|
| 230 |
+
; Output control
|
| 231 |
+
nstxout = 5000
|
| 232 |
+
nstvout = 5000
|
| 233 |
+
nstenergy = 1000
|
| 234 |
+
nstlog = 1000
|
| 235 |
+
|
| 236 |
+
; Bond parameters
|
| 237 |
+
constraint_algorithm = lincs
|
| 238 |
+
constraints = h-bonds
|
| 239 |
+
lincs_iter = 1
|
| 240 |
+
lincs_order = 4
|
| 241 |
+
|
| 242 |
+
; Neighbor searching
|
| 243 |
+
cutoff-scheme = Verlet
|
| 244 |
+
ns_type = grid
|
| 245 |
+
nstlist = 40
|
| 246 |
+
rlist = {params['cutoff']}
|
| 247 |
+
|
| 248 |
+
; Electrostatics
|
| 249 |
+
coulombtype = PME
|
| 250 |
+
rcoulomb = {params['cutoff']}
|
| 251 |
+
pme_order = {params['pmeOrder']}
|
| 252 |
+
|
| 253 |
+
; Van der Waals
|
| 254 |
+
vdwtype = Cut-off
|
| 255 |
+
rvdw = {params['cutoff']}
|
| 256 |
+
|
| 257 |
+
; Temperature coupling
|
| 258 |
+
tcoupl = {params['couplingType']}
|
| 259 |
+
tc-grps = Protein Non-Protein
|
| 260 |
+
tau_t = 0.1 0.1
|
| 261 |
+
ref_t = {params['steps']['npt']['temperature']} {params['steps']['npt']['temperature']}
|
| 262 |
+
|
| 263 |
+
; Pressure coupling
|
| 264 |
+
pcoupl = {params['couplingType']}
|
| 265 |
+
pcoupltype = isotropic
|
| 266 |
+
tau_p = 2.0
|
| 267 |
+
ref_p = {params['steps']['npt']['pressure']}
|
| 268 |
+
compressibility = 4.5e-5
|
| 269 |
+
|
| 270 |
+
; Velocity generation
|
| 271 |
+
gen_vel = no
|
| 272 |
+
"""
|
| 273 |
+
|
| 274 |
+
else: # production
|
| 275 |
+
return f"""; MD Simulation Parameters
|
| 276 |
+
; Generated by MD Simulation Pipeline
|
| 277 |
+
|
| 278 |
+
; Run parameters
|
| 279 |
+
integrator = md
|
| 280 |
+
dt = {params['timestep']}
|
| 281 |
+
nsteps = {params['steps']['production']['steps']}
|
| 282 |
+
|
| 283 |
+
; Output control
|
| 284 |
+
nstxout = 5000
|
| 285 |
+
nstvout = 5000
|
| 286 |
+
nstenergy = 1000
|
| 287 |
+
nstlog = 1000
|
| 288 |
+
|
| 289 |
+
; Bond parameters
|
| 290 |
+
constraint_algorithm = lincs
|
| 291 |
+
constraints = h-bonds
|
| 292 |
+
lincs_iter = 1
|
| 293 |
+
lincs_order = 4
|
| 294 |
+
|
| 295 |
+
; Neighbor searching
|
| 296 |
+
cutoff-scheme = Verlet
|
| 297 |
+
ns_type = grid
|
| 298 |
+
nstlist = 40
|
| 299 |
+
rlist = {params['cutoff']}
|
| 300 |
+
|
| 301 |
+
; Electrostatics
|
| 302 |
+
coulombtype = PME
|
| 303 |
+
rcoulomb = {params['cutoff']}
|
| 304 |
+
pme_order = {params['pmeOrder']}
|
| 305 |
+
fourierspacing = 0.16
|
| 306 |
+
|
| 307 |
+
; Van der Waals
|
| 308 |
+
vdwtype = Cut-off
|
| 309 |
+
rvdw = {params['cutoff']}
|
| 310 |
+
|
| 311 |
+
; Temperature coupling
|
| 312 |
+
tcoupl = {params['couplingType']}
|
| 313 |
+
tc-grps = Protein Non-Protein
|
| 314 |
+
tau_t = 0.1 0.1
|
| 315 |
+
ref_t = {params['temperature']} {params['temperature']}
|
| 316 |
+
|
| 317 |
+
; Pressure coupling
|
| 318 |
+
pcoupl = {params['couplingType']}
|
| 319 |
+
pcoupltype = isotropic
|
| 320 |
+
tau_p = 2.0
|
| 321 |
+
ref_p = {params['pressure']}
|
| 322 |
+
compressibility = 4.5e-5
|
| 323 |
+
|
| 324 |
+
; Dispersion correction
|
| 325 |
+
DispCorr = EnerPres
|
| 326 |
+
|
| 327 |
+
; Velocity generation
|
| 328 |
+
gen_vel = yes
|
| 329 |
+
gen_temp = {params['temperature']}
|
| 330 |
+
gen_seed = -1
|
| 331 |
+
"""
|
| 332 |
+
|
| 333 |
+
def generate_pbs_script(self, protein_name, params):
|
| 334 |
+
"""Generate PBS script for HPC submission"""
|
| 335 |
+
total_steps = params['steps']['production']['steps']
|
| 336 |
+
time_in_ns = (total_steps * params['timestep']) / 1000
|
| 337 |
+
|
| 338 |
+
return f"""#!/bin/bash
|
| 339 |
+
#PBS -N {protein_name}_md
|
| 340 |
+
#PBS -l nodes=1:ppn=16
|
| 341 |
+
#PBS -l walltime=24:00:00
|
| 342 |
+
#PBS -q normal
|
| 343 |
+
#PBS -j oe
|
| 344 |
+
|
| 345 |
+
# Change to the directory where the job was submitted
|
| 346 |
+
cd $PBS_O_WORKDIR
|
| 347 |
+
|
| 348 |
+
# Load required modules
|
| 349 |
+
module load gromacs/2023.2
|
| 350 |
+
module load intel/2021.4.0
|
| 351 |
+
|
| 352 |
+
# Set up environment
|
| 353 |
+
export OMP_NUM_THREADS=16
|
| 354 |
+
export GMX_MAXBACKUP=-1
|
| 355 |
+
|
| 356 |
+
# Simulation parameters
|
| 357 |
+
PROTEIN={protein_name}
|
| 358 |
+
STEPS={total_steps}
|
| 359 |
+
TIME_NS={time_in_ns:.2f}
|
| 360 |
+
|
| 361 |
+
echo "Starting MD simulation for $PROTEIN"
|
| 362 |
+
echo "Total simulation time: $TIME_NS ns"
|
| 363 |
+
echo "Job started at: $(date)"
|
| 364 |
+
|
| 365 |
+
# Run the simulation
|
| 366 |
+
./run_simulation.sh $PROTEIN
|
| 367 |
+
|
| 368 |
+
echo "Simulation completed at: $(date)"
|
| 369 |
+
echo "Results saved in output directory"
|
| 370 |
+
"""
|
| 371 |
+
|
| 372 |
+
def generate_setup_script(self, protein_name, params):
|
| 373 |
+
"""Generate setup script for MD simulation"""
|
| 374 |
+
return f"""#!/bin/bash
|
| 375 |
+
# Setup script for {protein_name} MD simulation
|
| 376 |
+
# Generated by MD Simulation Pipeline
|
| 377 |
+
|
| 378 |
+
set -e
|
| 379 |
+
|
| 380 |
+
PROTEIN={protein_name}
|
| 381 |
+
FORCE_FIELD={params['forceField']}
|
| 382 |
+
WATER_MODEL={params['waterModel']}
|
| 383 |
+
|
| 384 |
+
echo "Setting up MD simulation for $PROTEIN"
|
| 385 |
+
|
| 386 |
+
# Create output directory
|
| 387 |
+
mkdir -p output
|
| 388 |
+
|
| 389 |
+
# 1. Prepare protein structure
|
| 390 |
+
echo "Preparing protein structure..."
|
| 391 |
+
gmx pdb2gmx -f $PROTEIN.pdb -o $PROTEIN_processed.gro -p $PROTEIN.top -ff $FORCE_FIELD -water $WATER_MODEL
|
| 392 |
+
|
| 393 |
+
# 2. Define simulation box
|
| 394 |
+
echo "Defining simulation box..."
|
| 395 |
+
gmx editconf -f $PROTEIN_processed.gro -o $PROTEIN_box.gro -c -d {params['boxMargin']} -bt {params['boxType']}
|
| 396 |
+
|
| 397 |
+
# 3. Add solvent
|
| 398 |
+
echo "Adding solvent..."
|
| 399 |
+
gmx solvate -cp $PROTEIN_box.gro -cs spc216.gro -o $PROTEIN_solv.gro -p $PROTEIN.top
|
| 400 |
+
|
| 401 |
+
# 4. Add ions
|
| 402 |
+
echo "Adding ions..."
|
| 403 |
+
gmx grompp -f $PROTEIN_restrained.mdp -c $PROTEIN_solv.gro -p $PROTEIN.top -o $PROTEIN_ions.tpr
|
| 404 |
+
echo "SOL" | gmx genion -s $PROTEIN_ions.tpr -o $PROTEIN_final.gro -p $PROTEIN.top -pname NA -nname CL -neutral
|
| 405 |
+
|
| 406 |
+
echo "Setup completed successfully!"
|
| 407 |
+
echo "Ready to run simulation with: ./run_simulation.sh $PROTEIN"
|
| 408 |
+
"""
|
| 409 |
+
|
| 410 |
+
def generate_analysis_script(self, protein_name):
|
| 411 |
+
"""Generate analysis script for MD simulation results"""
|
| 412 |
+
return f"""#!/bin/bash
|
| 413 |
+
# Analysis script for {protein_name} MD simulation
|
| 414 |
+
# Generated by MD Simulation Pipeline
|
| 415 |
+
|
| 416 |
+
PROTEIN={protein_name}
|
| 417 |
+
|
| 418 |
+
echo "Analyzing MD simulation results for $PROTEIN"
|
| 419 |
+
|
| 420 |
+
# Create analysis directory
|
| 421 |
+
mkdir -p analysis
|
| 422 |
+
|
| 423 |
+
# 1. RMSD analysis
|
| 424 |
+
echo "Calculating RMSD..."
|
| 425 |
+
echo "Protein" | gmx rms -s $PROTEIN_final.tpr -f $PROTEIN_prod.xtc -o analysis/$PROTEIN_rmsd.xvg -tu ns
|
| 426 |
+
|
| 427 |
+
# 2. RMSF analysis
|
| 428 |
+
echo "Calculating RMSF..."
|
| 429 |
+
echo "Protein" | gmx rmsf -s $PROTEIN_final.tpr -f $PROTEIN_prod.xtc -o analysis/$PROTEIN_rmsf.xvg -res
|
| 430 |
+
|
| 431 |
+
# 3. Radius of gyration
|
| 432 |
+
echo "Calculating radius of gyration..."
|
| 433 |
+
echo "Protein" | gmx gyrate -s $PROTEIN_final.tpr -f $PROTEIN_prod.xtc -o analysis/$PROTEIN_gyrate.xvg
|
| 434 |
+
|
| 435 |
+
# 4. Hydrogen bonds
|
| 436 |
+
echo "Analyzing hydrogen bonds..."
|
| 437 |
+
echo "Protein" | gmx hbond -s $PROTEIN_final.tpr -f $PROTEIN_prod.xtc -num analysis/$PROTEIN_hbonds.xvg
|
| 438 |
+
|
| 439 |
+
# 5. Energy analysis
|
| 440 |
+
echo "Analyzing energies..."
|
| 441 |
+
gmx energy -f $PROTEIN_prod.edr -o analysis/$PROTEIN_energy.xvg
|
| 442 |
+
|
| 443 |
+
# 6. Generate plots
|
| 444 |
+
echo "Generating analysis plots..."
|
| 445 |
+
python3 plot_analysis.py $PROTEIN
|
| 446 |
+
|
| 447 |
+
echo "Analysis completed! Results saved in analysis/ directory"
|
| 448 |
+
"""
|
| 449 |
+
|
| 450 |
+
# Initialize the MD simulation generator
|
| 451 |
+
md_generator = MDSimulationGenerator()
|
| 452 |
+
|
| 453 |
+
@app.route('/api/fetch-pdb', methods=['POST'])
|
| 454 |
+
def fetch_pdb():
|
| 455 |
+
"""Fetch PDB structure from RCSB"""
|
| 456 |
+
try:
|
| 457 |
+
print("DEBUG: fetch-pdb endpoint called")
|
| 458 |
+
data = request.get_json()
|
| 459 |
+
pdb_id = data.get('pdb_id', '').upper()
|
| 460 |
+
print(f"DEBUG: pdb_id = {pdb_id}")
|
| 461 |
+
|
| 462 |
+
if not pdb_id or len(pdb_id) != 4:
|
| 463 |
+
return jsonify({'error': 'Invalid PDB ID'}), 400
|
| 464 |
+
|
| 465 |
+
# Clean and create new output folder for fresh start
|
| 466 |
+
print("DEBUG: Calling clean_and_create_output_folder()")
|
| 467 |
+
if not clean_and_create_output_folder():
|
| 468 |
+
return jsonify({'error': 'Failed to clean output folder'}), 500
|
| 469 |
+
print("DEBUG: Output folder cleanup completed successfully")
|
| 470 |
+
|
| 471 |
+
# Fetch PDB structure
|
| 472 |
+
pdb_file = md_generator.fetch_pdb_structure(pdb_id)
|
| 473 |
+
|
| 474 |
+
# Parse structure information
|
| 475 |
+
structure_info = md_generator.parse_pdb_structure(pdb_file)
|
| 476 |
+
|
| 477 |
+
return jsonify({
|
| 478 |
+
'success': True,
|
| 479 |
+
'structure_info': structure_info,
|
| 480 |
+
'pdb_file': pdb_file
|
| 481 |
+
})
|
| 482 |
+
|
| 483 |
+
except Exception as e:
|
| 484 |
+
logger.error(f"Error fetching PDB: {str(e)}")
|
| 485 |
+
return jsonify({'error': str(e)}), 500
|
| 486 |
+
|
| 487 |
+
@app.route('/api/parse-pdb', methods=['POST'])
|
| 488 |
+
def parse_pdb():
|
| 489 |
+
"""Parse uploaded PDB file"""
|
| 490 |
+
try:
|
| 491 |
+
print("DEBUG: parse-pdb endpoint called")
|
| 492 |
+
if 'file' not in request.files:
|
| 493 |
+
return jsonify({'error': 'No file uploaded'}), 400
|
| 494 |
+
|
| 495 |
+
file = request.files['file']
|
| 496 |
+
if file.filename == '':
|
| 497 |
+
return jsonify({'error': 'No file selected'}), 400
|
| 498 |
+
|
| 499 |
+
print(f"DEBUG: Processing uploaded file: {file.filename}")
|
| 500 |
+
|
| 501 |
+
# Clean and create new output folder for fresh start
|
| 502 |
+
print("DEBUG: Calling clean_and_create_output_folder()")
|
| 503 |
+
if not clean_and_create_output_folder():
|
| 504 |
+
return jsonify({'error': 'Failed to clean output folder'}), 500
|
| 505 |
+
print("DEBUG: Output folder cleanup completed successfully")
|
| 506 |
+
|
| 507 |
+
# Save uploaded file temporarily
|
| 508 |
+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pdb')
|
| 509 |
+
file.save(temp_file.name)
|
| 510 |
+
|
| 511 |
+
# Parse structure information
|
| 512 |
+
structure_info = md_generator.parse_pdb_structure(temp_file.name)
|
| 513 |
+
|
| 514 |
+
# Clean up temporary file
|
| 515 |
+
os.unlink(temp_file.name)
|
| 516 |
+
|
| 517 |
+
return jsonify({
|
| 518 |
+
'success': True,
|
| 519 |
+
'structure_info': structure_info
|
| 520 |
+
})
|
| 521 |
+
|
| 522 |
+
except Exception as e:
|
| 523 |
+
logger.error(f"Error parsing PDB: {str(e)}")
|
| 524 |
+
return jsonify({'error': str(e)}), 500
|
| 525 |
+
|
| 526 |
+
@app.route('/api/generate-files', methods=['POST'])
|
| 527 |
+
def generate_files():
|
| 528 |
+
"""Generate MD simulation files"""
|
| 529 |
+
try:
|
| 530 |
+
data = request.get_json()
|
| 531 |
+
protein_name = data.get('protein_name', 'protein')
|
| 532 |
+
simulation_params = data.get('simulation_params', {})
|
| 533 |
+
|
| 534 |
+
# Generate all files
|
| 535 |
+
files = {}
|
| 536 |
+
|
| 537 |
+
# MDP files
|
| 538 |
+
files[f'{protein_name}.mdp'] = md_generator.generate_mdp_file(simulation_params, 'production')
|
| 539 |
+
files[f'{protein_name}_restrained.mdp'] = md_generator.generate_mdp_file(simulation_params, 'restrained_min')
|
| 540 |
+
files[f'{protein_name}_min.mdp'] = md_generator.generate_mdp_file(simulation_params, 'minimization')
|
| 541 |
+
files[f'{protein_name}_nvt.mdp'] = md_generator.generate_mdp_file(simulation_params, 'nvt')
|
| 542 |
+
files[f'{protein_name}_npt.mdp'] = md_generator.generate_mdp_file(simulation_params, 'npt')
|
| 543 |
+
files[f'{protein_name}_prod.mdp'] = md_generator.generate_mdp_file(simulation_params, 'production')
|
| 544 |
+
|
| 545 |
+
# Scripts
|
| 546 |
+
files[f'{protein_name}_simulation.pbs'] = md_generator.generate_pbs_script(protein_name, simulation_params)
|
| 547 |
+
files[f'setup_{protein_name}.sh'] = md_generator.generate_setup_script(protein_name, simulation_params)
|
| 548 |
+
files[f'analyze_{protein_name}.sh'] = md_generator.generate_analysis_script(protein_name)
|
| 549 |
+
|
| 550 |
+
return jsonify({
|
| 551 |
+
'success': True,
|
| 552 |
+
'files': files
|
| 553 |
+
})
|
| 554 |
+
|
| 555 |
+
except Exception as e:
|
| 556 |
+
logger.error(f"Error generating files: {str(e)}")
|
| 557 |
+
return jsonify({'error': str(e)}), 500
|
| 558 |
+
|
| 559 |
+
@app.route('/api/download-zip', methods=['POST'])
|
| 560 |
+
def download_zip():
|
| 561 |
+
"""Download all generated files as a ZIP archive"""
|
| 562 |
+
try:
|
| 563 |
+
data = request.get_json()
|
| 564 |
+
files = data.get('files', {})
|
| 565 |
+
|
| 566 |
+
# Create temporary ZIP file
|
| 567 |
+
temp_zip = tempfile.NamedTemporaryFile(delete=False, suffix='.zip')
|
| 568 |
+
|
| 569 |
+
with zipfile.ZipFile(temp_zip.name, 'w') as zip_file:
|
| 570 |
+
for filename, content in files.items():
|
| 571 |
+
zip_file.writestr(filename, content)
|
| 572 |
+
|
| 573 |
+
return send_file(
|
| 574 |
+
temp_zip.name,
|
| 575 |
+
as_attachment=True,
|
| 576 |
+
download_name='md_simulation_files.zip',
|
| 577 |
+
mimetype='application/zip'
|
| 578 |
+
)
|
| 579 |
+
|
| 580 |
+
except Exception as e:
|
| 581 |
+
logger.error(f"Error creating ZIP file: {str(e)}")
|
| 582 |
+
return jsonify({'error': str(e)}), 500
|
| 583 |
+
|
| 584 |
+
@app.route('/api/get-solvated-protein', methods=['GET'])
|
| 585 |
+
def get_solvated_protein():
|
| 586 |
+
"""Get the solvated protein PDB file content"""
|
| 587 |
+
try:
|
| 588 |
+
solvated_file = os.path.join(OUTPUT_DIR, 'protein_solvated.pdb')
|
| 589 |
+
|
| 590 |
+
if not os.path.exists(solvated_file):
|
| 591 |
+
return jsonify({'success': False, 'error': 'Solvated protein file not found. Please generate files first.'})
|
| 592 |
+
|
| 593 |
+
with open(solvated_file, 'r') as f:
|
| 594 |
+
content = f.read()
|
| 595 |
+
|
| 596 |
+
return jsonify({'success': True, 'content': content})
|
| 597 |
+
except Exception as e:
|
| 598 |
+
logger.error(f"Error reading solvated protein file: {str(e)}")
|
| 599 |
+
return jsonify({'success': False, 'error': str(e)})
|
| 600 |
+
|
| 601 |
+
@app.route('/api/get-viewer-pdb', methods=['GET'])
|
| 602 |
+
def get_viewer_pdb():
|
| 603 |
+
"""Return a single PDB for viewer: start from protein_solvated.pdb and mark ligand residues as HETATM.
|
| 604 |
+
Ligand residues are detected from 4_ligands_corrected.pdb by (resname, chain, resi) tuples; if chains/resi not present, fallback to resname matching.
|
| 605 |
+
"""
|
| 606 |
+
try:
|
| 607 |
+
solvated_path = OUTPUT_DIR / 'protein_solvated.pdb'
|
| 608 |
+
lig_path = OUTPUT_DIR / '4_ligands_corrected.pdb'
|
| 609 |
+
viewer_out = OUTPUT_DIR / 'viewer_protein_with_ligand.pdb'
|
| 610 |
+
|
| 611 |
+
if not solvated_path.exists():
|
| 612 |
+
return jsonify({'success': False, 'error': 'protein_solvated.pdb not found'}), 400
|
| 613 |
+
|
| 614 |
+
# Build ligand index from corrected ligand PDB if present
|
| 615 |
+
ligand_keys = set()
|
| 616 |
+
ligand_resnames = set()
|
| 617 |
+
if lig_path.exists():
|
| 618 |
+
with open(lig_path, 'r') as lf:
|
| 619 |
+
for line in lf:
|
| 620 |
+
if line.startswith(('ATOM', 'HETATM')):
|
| 621 |
+
resn = line[17:20].strip()
|
| 622 |
+
chain = line[21:22].strip()
|
| 623 |
+
resi = line[22:26].strip()
|
| 624 |
+
ligand_resnames.add(resn)
|
| 625 |
+
if chain and resi:
|
| 626 |
+
ligand_keys.add((resn, chain, resi))
|
| 627 |
+
|
| 628 |
+
# Rewrite solvated file marking matching ligand residues and ions (NA/CL) as HETATM
|
| 629 |
+
out_lines = []
|
| 630 |
+
with open(solvated_path, 'r') as sf:
|
| 631 |
+
for line in sf:
|
| 632 |
+
if line.startswith(('ATOM', 'HETATM')):
|
| 633 |
+
resn = line[17:20].strip()
|
| 634 |
+
chain = line[21:22].strip()
|
| 635 |
+
resi = line[22:26].strip()
|
| 636 |
+
is_match = False
|
| 637 |
+
is_ion = resn in { 'NA', 'CL' }
|
| 638 |
+
if (resn, chain, resi) in ligand_keys:
|
| 639 |
+
is_match = True
|
| 640 |
+
elif resn in ligand_resnames:
|
| 641 |
+
# Fallback by residue name only
|
| 642 |
+
is_match = True
|
| 643 |
+
if is_match or is_ion:
|
| 644 |
+
# Force to HETATM
|
| 645 |
+
out_lines.append('HETATM' + line[6:])
|
| 646 |
+
else:
|
| 647 |
+
out_lines.append(line)
|
| 648 |
+
else:
|
| 649 |
+
out_lines.append(line)
|
| 650 |
+
|
| 651 |
+
# Save combined viewer file (optional but useful for debugging)
|
| 652 |
+
try:
|
| 653 |
+
with open(viewer_out, 'w') as vf:
|
| 654 |
+
vf.writelines(out_lines)
|
| 655 |
+
except Exception:
|
| 656 |
+
pass
|
| 657 |
+
|
| 658 |
+
return jsonify({'success': True, 'content': ''.join(out_lines)})
|
| 659 |
+
except Exception as e:
|
| 660 |
+
logger.error(f"Error generating viewer PDB: {str(e)}")
|
| 661 |
+
return jsonify({'success': False, 'error': str(e)})
|
| 662 |
+
|
| 663 |
+
@app.route('/api/get-corrected-ligands', methods=['GET'])
|
| 664 |
+
def get_corrected_ligands():
|
| 665 |
+
"""Get the corrected ligand PDB file content if present"""
|
| 666 |
+
try:
|
| 667 |
+
ligand_file = OUTPUT_DIR / '4_ligands_corrected.pdb'
|
| 668 |
+
if not ligand_file.exists():
|
| 669 |
+
# Return success with exists flag false so frontend can decide gracefully
|
| 670 |
+
return jsonify({'success': True, 'exists': False, 'content': ''})
|
| 671 |
+
# Read and normalize records to HETATM for viewer compatibility
|
| 672 |
+
normalized_lines = []
|
| 673 |
+
with open(ligand_file, 'r') as f:
|
| 674 |
+
for line in f:
|
| 675 |
+
if line.startswith('ATOM'):
|
| 676 |
+
# Replace record name to HETATM, preserve fixed-width columns
|
| 677 |
+
normalized_lines.append('HETATM' + line[6:])
|
| 678 |
+
else:
|
| 679 |
+
normalized_lines.append(line)
|
| 680 |
+
content = ''.join(normalized_lines)
|
| 681 |
+
return jsonify({'success': True, 'exists': True, 'content': content})
|
| 682 |
+
except Exception as e:
|
| 683 |
+
logger.error(f"Error reading corrected ligand file: {str(e)}")
|
| 684 |
+
return jsonify({'success': False, 'error': str(e)})
|
| 685 |
+
|
| 686 |
+
@app.route('/api/get-aligned-ligands', methods=['GET'])
|
| 687 |
+
def get_aligned_ligands():
|
| 688 |
+
"""Return ligand coordinates aligned to protein_solvated.pdb frame using PyMOL transforms."""
|
| 689 |
+
try:
|
| 690 |
+
solvated_file = OUTPUT_DIR / 'protein_solvated.pdb'
|
| 691 |
+
tleap_ready = OUTPUT_DIR / 'tleap_ready.pdb'
|
| 692 |
+
ligand_file = OUTPUT_DIR / '4_ligands_corrected.pdb'
|
| 693 |
+
|
| 694 |
+
if not solvated_file.exists():
|
| 695 |
+
return jsonify({'success': False, 'error': 'protein_solvated.pdb not found'}), 400
|
| 696 |
+
if not tleap_ready.exists():
|
| 697 |
+
return jsonify({'success': False, 'error': 'tleap_ready.pdb not found'}), 400
|
| 698 |
+
if not ligand_file.exists():
|
| 699 |
+
return jsonify({'success': True, 'exists': False, 'content': ''})
|
| 700 |
+
|
| 701 |
+
# Create temp output path
|
| 702 |
+
aligned_lig = OUTPUT_DIR / 'ligand_aligned_for_preview.pdb'
|
| 703 |
+
try:
|
| 704 |
+
if aligned_lig.exists():
|
| 705 |
+
aligned_lig.unlink()
|
| 706 |
+
except Exception:
|
| 707 |
+
pass
|
| 708 |
+
|
| 709 |
+
# PyMOL script: load solvated, load tlready (protein+lig), align tlready protein to solvated protein, then save transformed ligand
|
| 710 |
+
pymol_script = f"""
|
| 711 |
+
import pymol
|
| 712 |
+
pymol.finish_launching(['pymol','-qc'])
|
| 713 |
+
from pymol import cmd
|
| 714 |
+
cmd.load('{solvated_file.as_posix()}', 'solv')
|
| 715 |
+
cmd.load('{tleap_ready.as_posix()}', 'prep')
|
| 716 |
+
cmd.load('{ligand_file.as_posix()}', 'lig')
|
| 717 |
+
# Align prepared protein to solvated protein; use CA atoms to be robust
|
| 718 |
+
cmd.align('prep and polymer.protein and name CA', 'solv and polymer.protein and name CA')
|
| 719 |
+
# Apply same transform implicitly affects 'prep' object; we saved ligand as separate object, so match matrices
|
| 720 |
+
mat = cmd.get_object_matrix('prep')
|
| 721 |
+
cmd.set_object_matrix('lig', mat)
|
| 722 |
+
# Save ligand in aligned frame, as HETATM
|
| 723 |
+
cmd.alter('lig', 'type="HETATM"')
|
| 724 |
+
cmd.save('{aligned_lig.as_posix()}', 'lig')
|
| 725 |
+
cmd.quit()
|
| 726 |
+
"""
|
| 727 |
+
|
| 728 |
+
# Run PyMOL inline
|
| 729 |
+
result = subprocess.run(['python3', '-c', pymol_script], capture_output=True, text=True, cwd=str(OUTPUT_DIR))
|
| 730 |
+
if result.returncode != 0:
|
| 731 |
+
return jsonify({'success': False, 'error': f'PyMOL alignment failed: {result.stderr}'}), 500
|
| 732 |
+
|
| 733 |
+
if not aligned_lig.exists():
|
| 734 |
+
return jsonify({'success': False, 'error': 'Aligned ligand file was not produced'}), 500
|
| 735 |
+
|
| 736 |
+
# Read and return content
|
| 737 |
+
normalized_lines = []
|
| 738 |
+
with open(aligned_lig, 'r') as f:
|
| 739 |
+
for line in f:
|
| 740 |
+
if line.startswith('ATOM'):
|
| 741 |
+
normalized_lines.append('HETATM' + line[6:])
|
| 742 |
+
else:
|
| 743 |
+
normalized_lines.append(line)
|
| 744 |
+
content = ''.join(normalized_lines)
|
| 745 |
+
return jsonify({'success': True, 'exists': True, 'content': content})
|
| 746 |
+
except Exception as e:
|
| 747 |
+
logger.error(f"Error aligning ligands: {str(e)}")
|
| 748 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 749 |
+
|
| 750 |
+
@app.route('/')
|
| 751 |
+
def index():
|
| 752 |
+
"""Serve the main HTML page"""
|
| 753 |
+
return render_template('index.html')
|
| 754 |
+
|
| 755 |
+
@app.route('/<path:filename>')
|
| 756 |
+
def serve_static(filename):
|
| 757 |
+
"""Serve static files (CSS, JS, etc.)"""
|
| 758 |
+
return send_from_directory('../', filename)
|
| 759 |
+
|
| 760 |
+
@app.route('/api/prepare-structure', methods=['POST'])
|
| 761 |
+
def prepare_structure_endpoint():
|
| 762 |
+
"""Prepare protein structure for AMBER"""
|
| 763 |
+
try:
|
| 764 |
+
data = request.get_json()
|
| 765 |
+
pdb_content = data.get('pdb_content', '')
|
| 766 |
+
options = data.get('options', {})
|
| 767 |
+
|
| 768 |
+
if not pdb_content:
|
| 769 |
+
return jsonify({'error': 'No PDB content provided'}), 400
|
| 770 |
+
|
| 771 |
+
# Prepare structure
|
| 772 |
+
result = prepare_structure(pdb_content, options)
|
| 773 |
+
|
| 774 |
+
return jsonify({
|
| 775 |
+
'success': True,
|
| 776 |
+
'prepared_structure': result['prepared_structure'],
|
| 777 |
+
'original_atoms': result['original_atoms'],
|
| 778 |
+
'prepared_atoms': result['prepared_atoms'],
|
| 779 |
+
'removed_components': result['removed_components'],
|
| 780 |
+
'added_capping': result['added_capping'],
|
| 781 |
+
'preserved_ligands': result['preserved_ligands'],
|
| 782 |
+
'ligand_present': result.get('ligand_present', False),
|
| 783 |
+
'separate_ligands': result.get('separate_ligands', False),
|
| 784 |
+
'ligand_content': result.get('ligand_content', '')
|
| 785 |
+
})
|
| 786 |
+
|
| 787 |
+
except Exception as e:
|
| 788 |
+
logger.error(f"Error preparing structure: {str(e)}")
|
| 789 |
+
return jsonify({'error': str(e)}), 500
|
| 790 |
+
|
| 791 |
+
@app.route('/api/parse-structure', methods=['POST'])
|
| 792 |
+
def parse_structure_endpoint():
|
| 793 |
+
"""Parse structure information"""
|
| 794 |
+
try:
|
| 795 |
+
data = request.get_json()
|
| 796 |
+
pdb_content = data.get('pdb_content', '')
|
| 797 |
+
|
| 798 |
+
if not pdb_content:
|
| 799 |
+
return jsonify({'error': 'No PDB content provided'}), 400
|
| 800 |
+
|
| 801 |
+
# Parse structure
|
| 802 |
+
structure_info = parse_structure_info(pdb_content)
|
| 803 |
+
|
| 804 |
+
return jsonify({
|
| 805 |
+
'success': True,
|
| 806 |
+
'structure_info': structure_info
|
| 807 |
+
})
|
| 808 |
+
|
| 809 |
+
except Exception as e:
|
| 810 |
+
logger.error(f"Error parsing structure: {str(e)}")
|
| 811 |
+
return jsonify({'error': str(e)}), 500
|
| 812 |
+
|
| 813 |
+
@app.route('/api/generate-ligand-ff', methods=['POST'])
|
| 814 |
+
def generate_ligand_ff():
|
| 815 |
+
"""Generate force field parameters for ligand"""
|
| 816 |
+
try:
|
| 817 |
+
data = request.get_json()
|
| 818 |
+
force_field = data.get('force_field', 'gaff2')
|
| 819 |
+
|
| 820 |
+
# Determine the s parameter based on force field
|
| 821 |
+
s_param = 2 if force_field == 'gaff2' else 1
|
| 822 |
+
|
| 823 |
+
# Paths for ligand files in output directory
|
| 824 |
+
ligand_pdb = OUTPUT_DIR / "4_ligands_corrected.pdb"
|
| 825 |
+
ligand_mol2 = OUTPUT_DIR / "4_ligands_corrected.mol2"
|
| 826 |
+
ligand_frcmod = OUTPUT_DIR / "4_ligands_corrected.frcmod"
|
| 827 |
+
|
| 828 |
+
print(f"Working directory: {os.getcwd()}")
|
| 829 |
+
print(f"Output directory: {OUTPUT_DIR}")
|
| 830 |
+
print(f"Ligand PDB path: {ligand_pdb}")
|
| 831 |
+
print(f"Ligand MOL2 path: {ligand_mol2}")
|
| 832 |
+
print(f"Ligand FRCMOD path: {ligand_frcmod}")
|
| 833 |
+
|
| 834 |
+
if not ligand_pdb.exists():
|
| 835 |
+
return jsonify({'error': 'Ligand PDB file not found. Please prepare structure with ligands first.'}), 400
|
| 836 |
+
|
| 837 |
+
import re
|
| 838 |
+
|
| 839 |
+
# Command 1: Calculate net charge using awk
|
| 840 |
+
print("Step 1: Calculating net charge from PDB file...")
|
| 841 |
+
# Look for charge in the last field (field 12) - pattern is letter+number+charge
|
| 842 |
+
awk_cmd = "awk '/^HETATM/ {if($NF ~ /[A-Z][0-9]-$/) charge--; if($NF ~ /[A-Z][0-9]\\+$/) charge++} END {print \"Net charge:\", charge+0}'"
|
| 843 |
+
cmd1 = f"{awk_cmd} {ligand_pdb}"
|
| 844 |
+
|
| 845 |
+
try:
|
| 846 |
+
# Run awk command from the main directory, not output directory
|
| 847 |
+
result = subprocess.run(cmd1, shell=True, capture_output=True, text=True)
|
| 848 |
+
output = result.stdout.strip()
|
| 849 |
+
print(f"Awk output: '{output}'")
|
| 850 |
+
print(f"Awk stderr: '{result.stderr}'")
|
| 851 |
+
|
| 852 |
+
# Extract net charge from awk output
|
| 853 |
+
net_charge_match = re.search(r'Net charge:\s*(-?\d+)', output)
|
| 854 |
+
if net_charge_match:
|
| 855 |
+
net_charge = int(net_charge_match.group(1))
|
| 856 |
+
print(f"Calculated net charge: {net_charge}")
|
| 857 |
+
else:
|
| 858 |
+
print("Could not extract net charge from awk output, using 0")
|
| 859 |
+
net_charge = 0
|
| 860 |
+
except Exception as e:
|
| 861 |
+
print(f"Error running awk command: {e}, using net charge 0")
|
| 862 |
+
net_charge = 0
|
| 863 |
+
|
| 864 |
+
# Command 2: antechamber with calculated net charge
|
| 865 |
+
print(f"Step 2: Running antechamber with net charge {net_charge}...")
|
| 866 |
+
# Use relative paths and run in output directory
|
| 867 |
+
cmd2 = f"antechamber -i 4_ligands_corrected.pdb -fi pdb -o 4_ligands_corrected.mol2 -fo mol2 -c bcc -at {force_field} -nc {net_charge}"
|
| 868 |
+
print(f"Running command: {cmd2}")
|
| 869 |
+
result2 = subprocess.run(cmd2, shell=True, cwd=str(OUTPUT_DIR), capture_output=True, text=True)
|
| 870 |
+
|
| 871 |
+
print(f"antechamber return code: {result2.returncode}")
|
| 872 |
+
print(f"antechamber stdout: {result2.stdout}")
|
| 873 |
+
print(f"antechamber stderr: {result2.stderr}")
|
| 874 |
+
|
| 875 |
+
if result2.returncode != 0:
|
| 876 |
+
return jsonify({'error': f'antechamber failed with net charge {net_charge}. Error: {result2.stderr}'}), 500
|
| 877 |
+
|
| 878 |
+
# Command 3: parmchk2
|
| 879 |
+
print("Step 3: Running parmchk2...")
|
| 880 |
+
# Use relative paths and run in output directory
|
| 881 |
+
cmd3 = f"parmchk2 -i 4_ligands_corrected.mol2 -f mol2 -o 4_ligands_corrected.frcmod -a Y -s {s_param}"
|
| 882 |
+
print(f"Running command: {cmd3}")
|
| 883 |
+
result3 = subprocess.run(cmd3, shell=True, cwd=str(OUTPUT_DIR), capture_output=True, text=True)
|
| 884 |
+
|
| 885 |
+
print(f"parmchk2 return code: {result3.returncode}")
|
| 886 |
+
print(f"parmchk2 stdout: {result3.stdout}")
|
| 887 |
+
print(f"parmchk2 stderr: {result3.stderr}")
|
| 888 |
+
|
| 889 |
+
if result3.returncode != 0:
|
| 890 |
+
return jsonify({'error': f'parmchk2 failed to generate force field parameters. Error: {result3.stderr}'}), 500
|
| 891 |
+
|
| 892 |
+
# Check if files were generated successfully
|
| 893 |
+
print(f"After commands - MOL2 exists: {ligand_mol2.exists()}")
|
| 894 |
+
print(f"After commands - FRCMOD exists: {ligand_frcmod.exists()}")
|
| 895 |
+
print(f"Output directory contents: {list(OUTPUT_DIR.glob('*'))}")
|
| 896 |
+
|
| 897 |
+
if not ligand_mol2.exists() or not ligand_frcmod.exists():
|
| 898 |
+
return jsonify({'error': 'Force field generation failed - output files not created'}), 500
|
| 899 |
+
|
| 900 |
+
return jsonify({
|
| 901 |
+
'success': True,
|
| 902 |
+
'message': f'Ligand force field ({force_field}) generated successfully with net charge {net_charge}',
|
| 903 |
+
'net_charge': net_charge,
|
| 904 |
+
'files': {
|
| 905 |
+
'mol2': str(ligand_mol2),
|
| 906 |
+
'frcmod': str(ligand_frcmod)
|
| 907 |
+
}
|
| 908 |
+
})
|
| 909 |
+
|
| 910 |
+
except Exception as e:
|
| 911 |
+
logger.error(f"Error generating ligand force field: {str(e)}")
|
| 912 |
+
return jsonify({'error': f'Internal server error: {str(e)}'}), 500
|
| 913 |
+
|
| 914 |
+
@app.route('/api/calculate-net-charge', methods=['POST'])
|
| 915 |
+
def calculate_net_charge():
|
| 916 |
+
"""Calculate net charge of the system using tleap"""
|
| 917 |
+
try:
|
| 918 |
+
# Check if structure is prepared
|
| 919 |
+
tleap_ready_file = OUTPUT_DIR / "tleap_ready.pdb"
|
| 920 |
+
if not tleap_ready_file.exists():
|
| 921 |
+
return jsonify({'error': 'Structure not prepared. Please prepare structure first.'}), 400
|
| 922 |
+
|
| 923 |
+
# Check if ligand is present
|
| 924 |
+
ligand_pdb = OUTPUT_DIR / "4_ligands_corrected.pdb"
|
| 925 |
+
ligand_present = ligand_pdb.exists()
|
| 926 |
+
|
| 927 |
+
# Create dynamic tleap input file
|
| 928 |
+
tleap_input = OUTPUT_DIR / "calc_charge_on_system.in"
|
| 929 |
+
|
| 930 |
+
# Get the selected force field from the request
|
| 931 |
+
data = request.get_json() if request.get_json() else {}
|
| 932 |
+
selected_force_field = data.get('force_field', 'ff14SB')
|
| 933 |
+
|
| 934 |
+
with open(tleap_input, 'w') as f:
|
| 935 |
+
f.write(f"source leaprc.protein.{selected_force_field}\n")
|
| 936 |
+
f.write("source leaprc.gaff2\n\n")
|
| 937 |
+
|
| 938 |
+
if ligand_present:
|
| 939 |
+
# Load ligand parameters and structure
|
| 940 |
+
f.write("loadamberparams 4_ligands_corrected.frcmod\n\n")
|
| 941 |
+
f.write("COB = loadmol2 4_ligands_corrected.mol2\n\n")
|
| 942 |
+
|
| 943 |
+
f.write("x = loadpdb tleap_ready.pdb\n\n")
|
| 944 |
+
f.write("charge x\n\n")
|
| 945 |
+
|
| 946 |
+
# Run tleap command
|
| 947 |
+
print("Running tleap to calculate system charge...")
|
| 948 |
+
# Find tleap executable dynamically
|
| 949 |
+
try:
|
| 950 |
+
# First try to find tleap in PATH
|
| 951 |
+
which_result = subprocess.run(['which', 'tleap'], capture_output=True, text=True)
|
| 952 |
+
if which_result.returncode == 0:
|
| 953 |
+
tleap_path = which_result.stdout.strip()
|
| 954 |
+
else:
|
| 955 |
+
# Fallback: try common conda environment paths
|
| 956 |
+
conda_env = os.environ.get('CONDA_DEFAULT_ENV', 'MD_pipeline')
|
| 957 |
+
conda_prefix = os.environ.get('CONDA_PREFIX', '')
|
| 958 |
+
if conda_prefix:
|
| 959 |
+
tleap_path = os.path.join(conda_prefix, 'bin', 'tleap')
|
| 960 |
+
else:
|
| 961 |
+
# Last resort: assume it's in PATH
|
| 962 |
+
tleap_path = 'tleap'
|
| 963 |
+
|
| 964 |
+
cmd = f"{tleap_path} -f calc_charge_on_system.in"
|
| 965 |
+
result = subprocess.run(cmd, shell=True, cwd=str(OUTPUT_DIR), capture_output=True, text=True)
|
| 966 |
+
except Exception as e:
|
| 967 |
+
# Fallback to simple tleap command
|
| 968 |
+
cmd = f"tleap -f calc_charge_on_system.in"
|
| 969 |
+
result = subprocess.run(cmd, shell=True, cwd=str(OUTPUT_DIR), capture_output=True, text=True)
|
| 970 |
+
|
| 971 |
+
print(f"tleap return code: {result.returncode}")
|
| 972 |
+
print(f"tleap stdout: {result.stdout}")
|
| 973 |
+
print(f"tleap stderr: {result.stderr}")
|
| 974 |
+
|
| 975 |
+
# Check if we got the charge information even if tleap had a non-zero exit code
|
| 976 |
+
# (tleap often returns non-zero when run non-interactively but still calculates charge)
|
| 977 |
+
if 'Total unperturbed charge' not in result.stdout and 'Total charge' not in result.stdout:
|
| 978 |
+
return jsonify({'error': f'tleap failed to calculate charge. Error: {result.stderr}'}), 500
|
| 979 |
+
|
| 980 |
+
# Parse the output to find the net charge
|
| 981 |
+
output_lines = result.stdout.split('\n')
|
| 982 |
+
net_charge = None
|
| 983 |
+
|
| 984 |
+
for line in output_lines:
|
| 985 |
+
if 'Total unperturbed charge' in line or 'Total charge' in line:
|
| 986 |
+
# Look for patterns like "Total charge: -3.0000" or "Total unperturbed charge: -3.0000"
|
| 987 |
+
import re
|
| 988 |
+
charge_match = re.search(r'charge[:\s]+(-?\d+\.?\d*)', line)
|
| 989 |
+
if charge_match:
|
| 990 |
+
net_charge = float(charge_match.group(1))
|
| 991 |
+
break
|
| 992 |
+
|
| 993 |
+
if net_charge is None:
|
| 994 |
+
return jsonify({'error': 'Could not extract net charge from tleap output'}), 500
|
| 995 |
+
|
| 996 |
+
# Suggest ion addition
|
| 997 |
+
if net_charge > 0:
|
| 998 |
+
suggestion = f"Add {int(net_charge)} Cl- ions to neutralize the system"
|
| 999 |
+
ion_type = "Cl-"
|
| 1000 |
+
ion_count = int(net_charge)
|
| 1001 |
+
elif net_charge < 0:
|
| 1002 |
+
suggestion = f"Add {int(abs(net_charge))} Na+ ions to neutralize the system"
|
| 1003 |
+
ion_type = "Na+"
|
| 1004 |
+
ion_count = int(abs(net_charge))
|
| 1005 |
+
else:
|
| 1006 |
+
suggestion = "System is already neutral, no ions needed"
|
| 1007 |
+
ion_type = "None"
|
| 1008 |
+
ion_count = 0
|
| 1009 |
+
|
| 1010 |
+
return jsonify({
|
| 1011 |
+
'success': True,
|
| 1012 |
+
'net_charge': net_charge,
|
| 1013 |
+
'suggestion': suggestion,
|
| 1014 |
+
'ion_type': ion_type,
|
| 1015 |
+
'ion_count': ion_count,
|
| 1016 |
+
'ligand_present': ligand_present
|
| 1017 |
+
})
|
| 1018 |
+
|
| 1019 |
+
except Exception as e:
|
| 1020 |
+
logger.error(f"Error calculating net charge: {str(e)}")
|
| 1021 |
+
return jsonify({'error': f'Internal server error: {str(e)}'}), 500
|
| 1022 |
+
|
| 1023 |
+
@app.route('/api/generate-all-files', methods=['POST'])
|
| 1024 |
+
def generate_all_files():
|
| 1025 |
+
"""Generate all simulation input files based on UI parameters"""
|
| 1026 |
+
try:
|
| 1027 |
+
data = request.get_json()
|
| 1028 |
+
|
| 1029 |
+
# Get simulation parameters from UI
|
| 1030 |
+
cutoff_distance = data.get('cutoff_distance', 10.0)
|
| 1031 |
+
temperature = data.get('temperature', 310.0)
|
| 1032 |
+
pressure = data.get('pressure', 1.0)
|
| 1033 |
+
|
| 1034 |
+
# Get step parameters
|
| 1035 |
+
restrained_steps = data.get('restrained_steps', 10000)
|
| 1036 |
+
restrained_force = data.get('restrained_force', 10.0)
|
| 1037 |
+
min_steps = data.get('min_steps', 20000)
|
| 1038 |
+
npt_heating_steps = data.get('npt_heating_steps', 50000)
|
| 1039 |
+
npt_equilibration_steps = data.get('npt_equilibration_steps', 100000)
|
| 1040 |
+
production_steps = data.get('production_steps', 1000000)
|
| 1041 |
+
# Integration time step (ps)
|
| 1042 |
+
dt = data.get('timestep', 0.002)
|
| 1043 |
+
|
| 1044 |
+
# Get force field parameters
|
| 1045 |
+
force_field = data.get('force_field', 'ff14SB')
|
| 1046 |
+
water_model = data.get('water_model', 'TIP3P')
|
| 1047 |
+
add_ions = data.get('add_ions', 'None')
|
| 1048 |
+
distance = data.get('distance', 10.0)
|
| 1049 |
+
|
| 1050 |
+
# Validation warnings
|
| 1051 |
+
warnings = []
|
| 1052 |
+
if restrained_steps < 5000:
|
| 1053 |
+
warnings.append("Restrained minimization steps should be at least 5000")
|
| 1054 |
+
if min_steps < 10000:
|
| 1055 |
+
warnings.append("Minimization steps should be at least 10000")
|
| 1056 |
+
|
| 1057 |
+
# Count total residues in tleap_ready.pdb
|
| 1058 |
+
tleap_ready_file = OUTPUT_DIR / "tleap_ready.pdb"
|
| 1059 |
+
if not tleap_ready_file.exists():
|
| 1060 |
+
return jsonify({'error': 'tleap_ready.pdb not found. Please prepare structure first.'}), 400
|
| 1061 |
+
|
| 1062 |
+
total_residues = count_residues_in_pdb(str(tleap_ready_file))
|
| 1063 |
+
|
| 1064 |
+
# Generate min_restrained.in
|
| 1065 |
+
generate_min_restrained_file(restrained_steps, restrained_force, total_residues, cutoff_distance)
|
| 1066 |
+
|
| 1067 |
+
# Generate min.in
|
| 1068 |
+
generate_min_file(min_steps, cutoff_distance)
|
| 1069 |
+
|
| 1070 |
+
# Generate HeatNPT.in
|
| 1071 |
+
generate_heat_npt_file(npt_heating_steps, temperature, pressure, cutoff_distance, dt)
|
| 1072 |
+
|
| 1073 |
+
# Generate mdin_equi.in (NPT Equilibration)
|
| 1074 |
+
generate_npt_equilibration_file(npt_equilibration_steps, temperature, pressure, cutoff_distance, dt)
|
| 1075 |
+
|
| 1076 |
+
# Generate mdin_prod.in (Production)
|
| 1077 |
+
generate_production_file(production_steps, temperature, pressure, cutoff_distance, dt)
|
| 1078 |
+
|
| 1079 |
+
# Generate force field parameters
|
| 1080 |
+
ff_files_generated = []
|
| 1081 |
+
try:
|
| 1082 |
+
generate_ff_parameters_file(force_field, water_model, add_ions, distance)
|
| 1083 |
+
|
| 1084 |
+
# Find tleap executable
|
| 1085 |
+
tleap_path = None
|
| 1086 |
+
try:
|
| 1087 |
+
result = subprocess.run(['which', 'tleap'], capture_output=True, text=True)
|
| 1088 |
+
if result.returncode == 0:
|
| 1089 |
+
tleap_path = result.stdout.strip()
|
| 1090 |
+
except:
|
| 1091 |
+
pass
|
| 1092 |
+
|
| 1093 |
+
if not tleap_path:
|
| 1094 |
+
conda_prefix = os.environ.get('CONDA_PREFIX')
|
| 1095 |
+
if conda_prefix:
|
| 1096 |
+
tleap_path = os.path.join(conda_prefix, 'bin', 'tleap')
|
| 1097 |
+
else:
|
| 1098 |
+
tleap_path = '/home/hn533621/.conda/envs/MD_pipeline/bin/tleap'
|
| 1099 |
+
|
| 1100 |
+
# Run tleap to generate force field parameters
|
| 1101 |
+
cmd = f"{tleap_path} -f generate_ff_parameters.in"
|
| 1102 |
+
result = subprocess.run(cmd, shell=True, cwd=str(OUTPUT_DIR),
|
| 1103 |
+
capture_output=True, text=True, timeout=300)
|
| 1104 |
+
|
| 1105 |
+
if result.returncode != 0:
|
| 1106 |
+
warnings.append(f"Force field generation failed: {result.stderr}")
|
| 1107 |
+
else:
|
| 1108 |
+
# Check if key output files were created
|
| 1109 |
+
ff_output_files = ['protein.prmtop', 'protein.inpcrd', 'protein_solvated.pdb']
|
| 1110 |
+
for ff_file in ff_output_files:
|
| 1111 |
+
if (OUTPUT_DIR / ff_file).exists():
|
| 1112 |
+
ff_files_generated.append(ff_file)
|
| 1113 |
+
|
| 1114 |
+
if len(ff_files_generated) == 0:
|
| 1115 |
+
warnings.append("Force field parameter files were not generated")
|
| 1116 |
+
|
| 1117 |
+
except Exception as ff_error:
|
| 1118 |
+
warnings.append(f"Force field generation error: {str(ff_error)}")
|
| 1119 |
+
|
| 1120 |
+
# Generate PBS submit script into output
|
| 1121 |
+
pbs_generated = generate_submit_pbs_file()
|
| 1122 |
+
|
| 1123 |
+
all_files = [
|
| 1124 |
+
'min_restrained.in',
|
| 1125 |
+
'min.in',
|
| 1126 |
+
'HeatNPT.in',
|
| 1127 |
+
'mdin_equi.in',
|
| 1128 |
+
'mdin_prod.in'
|
| 1129 |
+
] + ff_files_generated
|
| 1130 |
+
|
| 1131 |
+
if pbs_generated:
|
| 1132 |
+
all_files.append('submit_jobs.pbs')
|
| 1133 |
+
|
| 1134 |
+
return jsonify({
|
| 1135 |
+
'success': True,
|
| 1136 |
+
'message': f'All simulation files generated successfully ({len(all_files)} files)',
|
| 1137 |
+
'warnings': warnings,
|
| 1138 |
+
'files_generated': all_files
|
| 1139 |
+
})
|
| 1140 |
+
|
| 1141 |
+
except Exception as e:
|
| 1142 |
+
logger.error(f"Error generating simulation files: {str(e)}")
|
| 1143 |
+
return jsonify({'error': f'Internal server error: {str(e)}'}), 500
|
| 1144 |
+
|
| 1145 |
+
def count_residues_in_pdb(pdb_file):
|
| 1146 |
+
"""Count total number of residues in PDB file"""
|
| 1147 |
+
try:
|
| 1148 |
+
with open(pdb_file, 'r') as f:
|
| 1149 |
+
lines = f.readlines()
|
| 1150 |
+
|
| 1151 |
+
residues = set()
|
| 1152 |
+
for line in lines:
|
| 1153 |
+
if line.startswith(('ATOM', 'HETATM')):
|
| 1154 |
+
# Extract residue number (columns 23-26)
|
| 1155 |
+
residue_num = line[22:26].strip()
|
| 1156 |
+
if residue_num:
|
| 1157 |
+
residues.add(residue_num)
|
| 1158 |
+
|
| 1159 |
+
return len(residues)
|
| 1160 |
+
except Exception as e:
|
| 1161 |
+
logger.error(f"Error counting residues: {str(e)}")
|
| 1162 |
+
return 607 # Default fallback
|
| 1163 |
+
|
| 1164 |
+
def generate_min_restrained_file(steps, force_constant, total_residues, cutoff):
|
| 1165 |
+
"""Generate min_restrained.in file"""
|
| 1166 |
+
content = f"""initial minimization solvent + ions
|
| 1167 |
+
&cntrl
|
| 1168 |
+
imin = 1,
|
| 1169 |
+
maxcyc = {steps},
|
| 1170 |
+
ncyc = {steps // 2},
|
| 1171 |
+
ntb = 1,
|
| 1172 |
+
ntr = 1,
|
| 1173 |
+
ntxo = 1,
|
| 1174 |
+
cut = {cutoff}
|
| 1175 |
+
/
|
| 1176 |
+
Restrain
|
| 1177 |
+
{force_constant}
|
| 1178 |
+
RES 1 {total_residues}
|
| 1179 |
+
END
|
| 1180 |
+
END
|
| 1181 |
+
|
| 1182 |
+
"""
|
| 1183 |
+
|
| 1184 |
+
with open(OUTPUT_DIR / "min_restrained.in", 'w') as f:
|
| 1185 |
+
f.write(content)
|
| 1186 |
+
|
| 1187 |
+
def generate_min_file(steps, cutoff):
|
| 1188 |
+
"""Generate min.in file"""
|
| 1189 |
+
content = f"""Minimization
|
| 1190 |
+
&cntrl
|
| 1191 |
+
imin=1,
|
| 1192 |
+
maxcyc={steps},
|
| 1193 |
+
ncyc={steps // 4},
|
| 1194 |
+
ntb=1,
|
| 1195 |
+
cut={cutoff},
|
| 1196 |
+
igb=0,
|
| 1197 |
+
ntr=0,
|
| 1198 |
+
/
|
| 1199 |
+
|
| 1200 |
+
"""
|
| 1201 |
+
|
| 1202 |
+
with open(OUTPUT_DIR / "min.in", 'w') as f:
|
| 1203 |
+
f.write(content)
|
| 1204 |
+
|
| 1205 |
+
def generate_heat_npt_file(steps, temperature, pressure, cutoff, dt=0.002):
|
| 1206 |
+
"""Generate HeatNPT.in file with temperature ramping"""
|
| 1207 |
+
# Calculate step divisions: 20%, 20%, 20%, 40%
|
| 1208 |
+
step1 = int(steps * 0.2)
|
| 1209 |
+
step2 = int(steps * 0.2)
|
| 1210 |
+
step3 = int(steps * 0.2)
|
| 1211 |
+
step4 = int(steps * 0.4)
|
| 1212 |
+
|
| 1213 |
+
# Calculate temperature values: 3%, 66%, 100%
|
| 1214 |
+
temp1 = temperature * 0.03
|
| 1215 |
+
temp2 = temperature * 0.66
|
| 1216 |
+
temp3 = temperature
|
| 1217 |
+
temp4 = temperature
|
| 1218 |
+
|
| 1219 |
+
content = f"""Heat
|
| 1220 |
+
&cntrl
|
| 1221 |
+
imin = 0, irest = 0, ntx = 1,
|
| 1222 |
+
ntb = 2, pres0 = {pressure}, ntp = 1,
|
| 1223 |
+
taup = 2.0,
|
| 1224 |
+
cut = {cutoff}, ntr = 0,
|
| 1225 |
+
ntc = 2, ntf = 2,
|
| 1226 |
+
tempi = 0, temp0 = {temperature},
|
| 1227 |
+
ntt = 3, gamma_ln = 1.0,
|
| 1228 |
+
nstlim = {steps}, dt = {dt},
|
| 1229 |
+
ntpr = 2000, ntwx = 2000, ntwr = 2000
|
| 1230 |
+
/
|
| 1231 |
+
&wt type='TEMP0', istep1=0, istep2={step1}, value1=0.0, value2={temp1} /
|
| 1232 |
+
&wt type='TEMP0', istep1={step1+1}, istep2={step1+step2}, value1={temp1}, value2={temp2} /
|
| 1233 |
+
&wt type='TEMP0', istep1={step1+step2+1}, istep2={step1+step2+step3}, value1={temp2}, value2={temp3} /
|
| 1234 |
+
&wt type='TEMP0', istep1={step1+step2+step3+1}, istep2={steps}, value1={temp3}, value2={temp4} /
|
| 1235 |
+
&wt type='END' /
|
| 1236 |
+
|
| 1237 |
+
"""
|
| 1238 |
+
|
| 1239 |
+
with open(OUTPUT_DIR / "HeatNPT.in", 'w') as f:
|
| 1240 |
+
f.write(content)
|
| 1241 |
+
|
| 1242 |
+
def generate_npt_equilibration_file(steps, temperature, pressure, cutoff, dt=0.002):
|
| 1243 |
+
"""Generate mdin_equi.in file for NPT equilibration"""
|
| 1244 |
+
content = f"""NPT Equilibration
|
| 1245 |
+
&cntrl
|
| 1246 |
+
imin=0,
|
| 1247 |
+
ntx=1,
|
| 1248 |
+
irest=0,
|
| 1249 |
+
pres0={pressure},
|
| 1250 |
+
taup=1.0,
|
| 1251 |
+
temp0={temperature},
|
| 1252 |
+
tempi={temperature},
|
| 1253 |
+
nstlim={steps},
|
| 1254 |
+
dt={dt},
|
| 1255 |
+
ntf=2,
|
| 1256 |
+
ntc=2,
|
| 1257 |
+
ntpr=500,
|
| 1258 |
+
ntwx=500,
|
| 1259 |
+
ntwr=500,
|
| 1260 |
+
cut={cutoff},
|
| 1261 |
+
ntb=2,
|
| 1262 |
+
ntp=1,
|
| 1263 |
+
ntt=3,
|
| 1264 |
+
gamma_ln=3.0,
|
| 1265 |
+
ig=-1,
|
| 1266 |
+
iwrap=1,
|
| 1267 |
+
ntr=0,
|
| 1268 |
+
/
|
| 1269 |
+
|
| 1270 |
+
"""
|
| 1271 |
+
|
| 1272 |
+
with open(OUTPUT_DIR / "mdin_equi.in", 'w') as f:
|
| 1273 |
+
f.write(content)
|
| 1274 |
+
|
| 1275 |
+
def generate_production_file(steps, temperature, pressure, cutoff, dt=0.002):
|
| 1276 |
+
"""Generate mdin_prod.in file for production run"""
|
| 1277 |
+
content = f"""Production Run
|
| 1278 |
+
&cntrl
|
| 1279 |
+
imin=0,
|
| 1280 |
+
ntx=1,
|
| 1281 |
+
irest=0,
|
| 1282 |
+
pres0={pressure},
|
| 1283 |
+
taup=1.0,
|
| 1284 |
+
temp0={temperature},
|
| 1285 |
+
tempi={temperature},
|
| 1286 |
+
nstlim={steps},
|
| 1287 |
+
dt={dt},
|
| 1288 |
+
ntf=2,
|
| 1289 |
+
ntc=2,
|
| 1290 |
+
ntpr=1000,
|
| 1291 |
+
ntwx=1000,
|
| 1292 |
+
ntwr=1000,
|
| 1293 |
+
cut={cutoff},
|
| 1294 |
+
ntb=2,
|
| 1295 |
+
ntp=1,
|
| 1296 |
+
ntt=3,
|
| 1297 |
+
gamma_ln=3.0,
|
| 1298 |
+
ig=-1,
|
| 1299 |
+
iwrap=1,
|
| 1300 |
+
ntr=0,
|
| 1301 |
+
/
|
| 1302 |
+
|
| 1303 |
+
"""
|
| 1304 |
+
|
| 1305 |
+
with open(OUTPUT_DIR / "mdin_prod.in", 'w') as f:
|
| 1306 |
+
f.write(content)
|
| 1307 |
+
|
| 1308 |
+
def generate_submit_pbs_file():
|
| 1309 |
+
"""Copy submit_jobs.pbs template into output folder"""
|
| 1310 |
+
try:
|
| 1311 |
+
templates_dir = Path("templates")
|
| 1312 |
+
template_path = templates_dir / "submit_jobs.pbs"
|
| 1313 |
+
if not template_path.exists():
|
| 1314 |
+
logger.warning("submit_jobs.pbs template not found; skipping PBS generation")
|
| 1315 |
+
return False
|
| 1316 |
+
with open(template_path, 'r') as tf:
|
| 1317 |
+
content = tf.read()
|
| 1318 |
+
with open(OUTPUT_DIR / "submit_jobs.pbs", 'w') as outf:
|
| 1319 |
+
outf.write(content)
|
| 1320 |
+
return True
|
| 1321 |
+
except Exception as e:
|
| 1322 |
+
logger.error(f"Error generating submit_jobs.pbs: {e}")
|
| 1323 |
+
return False
|
| 1324 |
+
|
| 1325 |
+
@app.route('/api/health', methods=['GET'])
|
| 1326 |
+
def health_check():
|
| 1327 |
+
"""Health check endpoint"""
|
| 1328 |
+
return jsonify({'status': 'healthy', 'message': 'MD Simulation Pipeline API is running'})
|
| 1329 |
+
|
| 1330 |
+
@app.route('/api/clean-output', methods=['POST'])
|
| 1331 |
+
def clean_output():
|
| 1332 |
+
"""Clean output folder endpoint"""
|
| 1333 |
+
try:
|
| 1334 |
+
print("DEBUG: clean-output endpoint called")
|
| 1335 |
+
if clean_and_create_output_folder():
|
| 1336 |
+
return jsonify({'success': True, 'message': 'Output folder cleaned successfully'})
|
| 1337 |
+
else:
|
| 1338 |
+
return jsonify({'success': False, 'error': 'Failed to clean output folder'}), 500
|
| 1339 |
+
except Exception as e:
|
| 1340 |
+
print(f"DEBUG: Error in clean-output: {str(e)}")
|
| 1341 |
+
return jsonify({'success': False, 'error': str(e)}), 500
|
| 1342 |
+
|
| 1343 |
+
@app.route('/api/download-output-zip', methods=['GET'])
|
| 1344 |
+
def download_output_zip():
|
| 1345 |
+
"""Create a ZIP of the output folder and return it for download"""
|
| 1346 |
+
try:
|
| 1347 |
+
if not OUTPUT_DIR.exists():
|
| 1348 |
+
return jsonify({'error': 'Output directory not found'}), 404
|
| 1349 |
+
|
| 1350 |
+
import tempfile
|
| 1351 |
+
import shutil
|
| 1352 |
+
|
| 1353 |
+
# Create a temporary zip file
|
| 1354 |
+
tmp_dir = tempfile.mkdtemp()
|
| 1355 |
+
zip_base = os.path.join(tmp_dir, 'output')
|
| 1356 |
+
zip_path = shutil.make_archive(zip_base, 'zip', root_dir=str(OUTPUT_DIR))
|
| 1357 |
+
|
| 1358 |
+
# Send file for download
|
| 1359 |
+
return send_file(zip_path, as_attachment=True, download_name='output.zip')
|
| 1360 |
+
except Exception as e:
|
| 1361 |
+
logger.error(f"Error creating output ZIP: {str(e)}")
|
| 1362 |
+
return jsonify({'error': f'Failed to create ZIP: {str(e)}'}), 500
|
| 1363 |
+
|
| 1364 |
+
@app.route('/api/get-generated-files', methods=['GET'])
|
| 1365 |
+
def get_generated_files():
|
| 1366 |
+
"""Return contents of known generated input files for preview"""
|
| 1367 |
+
try:
|
| 1368 |
+
files_to_read = [
|
| 1369 |
+
'min_restrained.in',
|
| 1370 |
+
'min.in',
|
| 1371 |
+
'HeatNPT.in',
|
| 1372 |
+
'mdin_equi.in',
|
| 1373 |
+
'mdin_prod.in'
|
| 1374 |
+
]
|
| 1375 |
+
# Note: Force field parameter files (protein.prmtop, protein.inpcrd, protein_solvated.pdb)
|
| 1376 |
+
# are excluded from preview as they are binary/large files
|
| 1377 |
+
result = {}
|
| 1378 |
+
for name in files_to_read:
|
| 1379 |
+
path = OUTPUT_DIR / name
|
| 1380 |
+
if path.exists():
|
| 1381 |
+
try:
|
| 1382 |
+
with open(path, 'r') as f:
|
| 1383 |
+
result[name] = f.read()
|
| 1384 |
+
except Exception as fe:
|
| 1385 |
+
result[name] = f"<error reading file: {fe}>"
|
| 1386 |
+
else:
|
| 1387 |
+
result[name] = "<file not found>"
|
| 1388 |
+
return jsonify({'success': True, 'files': result})
|
| 1389 |
+
except Exception as e:
|
| 1390 |
+
logger.error(f"Error reading generated files: {str(e)}")
|
| 1391 |
+
return jsonify({'error': f'Failed to read files: {str(e)}'}), 500
|
| 1392 |
+
|
| 1393 |
+
def get_ligand_residue_name():
|
| 1394 |
+
"""Extract ligand residue name from tleap_ready.pdb"""
|
| 1395 |
+
try:
|
| 1396 |
+
with open(OUTPUT_DIR / "tleap_ready.pdb", 'r') as f:
|
| 1397 |
+
for line in f:
|
| 1398 |
+
if line.startswith('HETATM'):
|
| 1399 |
+
# Extract residue name (columns 18-20)
|
| 1400 |
+
residue_name = line[17:20].strip()
|
| 1401 |
+
if residue_name and residue_name not in ['HOH', 'WAT', 'TIP', 'SPC']: # Exclude water
|
| 1402 |
+
return residue_name
|
| 1403 |
+
return "LIG" # Default fallback
|
| 1404 |
+
except:
|
| 1405 |
+
return "LIG" # Default fallback
|
| 1406 |
+
|
| 1407 |
+
def generate_ff_parameters_file(force_field, water_model, add_ions, distance):
|
| 1408 |
+
"""Generate the final force field parameters file with dynamic values"""
|
| 1409 |
+
# Debug logging
|
| 1410 |
+
print(f"DEBUG: force_field={force_field}, water_model={water_model}, add_ions={add_ions}, distance={distance}")
|
| 1411 |
+
|
| 1412 |
+
# Determine if ligand is present
|
| 1413 |
+
ligand_present = (OUTPUT_DIR / "4_ligands_corrected.mol2").exists()
|
| 1414 |
+
|
| 1415 |
+
# Get dynamic ligand residue name
|
| 1416 |
+
ligand_name = get_ligand_residue_name()
|
| 1417 |
+
|
| 1418 |
+
# Build the content dynamically
|
| 1419 |
+
content = f"source leaprc.protein.{force_field}\n"
|
| 1420 |
+
|
| 1421 |
+
# Add water model source
|
| 1422 |
+
print(f"DEBUG: water_model={water_model}")
|
| 1423 |
+
if water_model.lower() == "tip3p":
|
| 1424 |
+
content += "source leaprc.water.tip3p\n"
|
| 1425 |
+
elif water_model == "spce":
|
| 1426 |
+
content += "source leaprc.water.spce\n"
|
| 1427 |
+
|
| 1428 |
+
# Add ligand-related commands only if ligand is present
|
| 1429 |
+
if ligand_present:
|
| 1430 |
+
content += "source leaprc.gaff2\n\n"
|
| 1431 |
+
content += "loadamberparams 4_ligands_corrected.frcmod\n\n"
|
| 1432 |
+
content += f"{ligand_name} = loadmol2 4_ligands_corrected.mol2\n\n"
|
| 1433 |
+
else:
|
| 1434 |
+
content += "\n"
|
| 1435 |
+
|
| 1436 |
+
content += "x = loadpdb tleap_ready.pdb\n\n"
|
| 1437 |
+
content += "charge x\n\n"
|
| 1438 |
+
|
| 1439 |
+
# Add ions based on selection
|
| 1440 |
+
if add_ions == "Na+":
|
| 1441 |
+
content += "addions x Na+ 0.0\n\n"
|
| 1442 |
+
elif add_ions == "Cl-":
|
| 1443 |
+
content += "addions x Cl- 0.0\n\n"
|
| 1444 |
+
# If "None", skip adding ions
|
| 1445 |
+
|
| 1446 |
+
# Add solvation with selected water model and distance
|
| 1447 |
+
if water_model.lower() == "tip3p":
|
| 1448 |
+
content += f"solvateBox x TIP3PBOX {distance}\n\n"
|
| 1449 |
+
elif water_model.lower() == "spce":
|
| 1450 |
+
content += f"solvateBox x SPCBOX {distance}\n\n"
|
| 1451 |
+
|
| 1452 |
+
content += "saveamberparm x protein.prmtop protein.inpcrd\n\n"
|
| 1453 |
+
content += "savepdb x protein_solvated.pdb\n\n"
|
| 1454 |
+
content += "quit\n"
|
| 1455 |
+
|
| 1456 |
+
# Debug: print the generated content
|
| 1457 |
+
print("DEBUG: Generated content:")
|
| 1458 |
+
print(content)
|
| 1459 |
+
|
| 1460 |
+
# Write the file
|
| 1461 |
+
with open(OUTPUT_DIR / "generate_ff_parameters.in", 'w') as f:
|
| 1462 |
+
f.write(content)
|
| 1463 |
+
|
| 1464 |
+
@app.route('/api/generate-ff-parameters', methods=['POST'])
|
| 1465 |
+
def generate_ff_parameters():
|
| 1466 |
+
"""Generate final force field parameters using tleap"""
|
| 1467 |
+
try:
|
| 1468 |
+
data = request.get_json()
|
| 1469 |
+
force_field = data.get('force_field', 'ff14SB')
|
| 1470 |
+
water_model = data.get('water_model', 'TIP3P')
|
| 1471 |
+
add_ions = data.get('add_ions', 'None')
|
| 1472 |
+
distance = data.get('distance', 10.0)
|
| 1473 |
+
|
| 1474 |
+
# Generate the dynamic input file
|
| 1475 |
+
generate_ff_parameters_file(force_field, water_model, add_ions, distance)
|
| 1476 |
+
|
| 1477 |
+
# Find tleap executable
|
| 1478 |
+
tleap_path = None
|
| 1479 |
+
try:
|
| 1480 |
+
result = subprocess.run(['which', 'tleap'], capture_output=True, text=True)
|
| 1481 |
+
if result.returncode == 0:
|
| 1482 |
+
tleap_path = result.stdout.strip()
|
| 1483 |
+
except:
|
| 1484 |
+
pass
|
| 1485 |
+
|
| 1486 |
+
if not tleap_path:
|
| 1487 |
+
conda_prefix = os.environ.get('CONDA_PREFIX')
|
| 1488 |
+
if conda_prefix:
|
| 1489 |
+
tleap_path = os.path.join(conda_prefix, 'bin', 'tleap')
|
| 1490 |
+
else:
|
| 1491 |
+
tleap_path = '/home/hn533621/.conda/envs/MD_pipeline/bin/tleap'
|
| 1492 |
+
|
| 1493 |
+
# Run tleap
|
| 1494 |
+
cmd = f"{tleap_path} -f generate_ff_parameters.in"
|
| 1495 |
+
result = subprocess.run(cmd, shell=True, cwd=str(OUTPUT_DIR),
|
| 1496 |
+
capture_output=True, text=True, timeout=300)
|
| 1497 |
+
|
| 1498 |
+
if result.returncode != 0:
|
| 1499 |
+
logger.error(f"tleap failed: {result.stderr}")
|
| 1500 |
+
return jsonify({
|
| 1501 |
+
'success': False,
|
| 1502 |
+
'error': f'tleap failed: {result.stderr}'
|
| 1503 |
+
}), 500
|
| 1504 |
+
|
| 1505 |
+
# Check if key output files were created
|
| 1506 |
+
output_files = ['protein.prmtop', 'protein.inpcrd', 'protein_solvated.pdb']
|
| 1507 |
+
missing_files = [f for f in output_files if not (OUTPUT_DIR / f).exists()]
|
| 1508 |
+
|
| 1509 |
+
if missing_files:
|
| 1510 |
+
return jsonify({
|
| 1511 |
+
'success': False,
|
| 1512 |
+
'error': f'Missing output files: {", ".join(missing_files)}'
|
| 1513 |
+
}), 500
|
| 1514 |
+
|
| 1515 |
+
return jsonify({
|
| 1516 |
+
'success': True,
|
| 1517 |
+
'message': 'Force field parameters generated successfully',
|
| 1518 |
+
'files_generated': output_files
|
| 1519 |
+
})
|
| 1520 |
+
|
| 1521 |
+
except subprocess.TimeoutExpired:
|
| 1522 |
+
return jsonify({
|
| 1523 |
+
'success': False,
|
| 1524 |
+
'error': 'tleap command timed out after 5 minutes'
|
| 1525 |
+
}), 500
|
| 1526 |
+
except Exception as e:
|
| 1527 |
+
logger.error(f"Error generating FF parameters: {str(e)}")
|
| 1528 |
+
return jsonify({
|
| 1529 |
+
'success': False,
|
| 1530 |
+
'error': f'Failed to generate force field parameters: {str(e)}'
|
| 1531 |
+
}), 500
|
| 1532 |
+
|
| 1533 |
+
if __name__ == '__main__':
|
| 1534 |
+
print("🧬 MD Simulation Pipeline")
|
| 1535 |
+
print("=========================")
|
| 1536 |
+
print("🌐 Starting Flask server...")
|
| 1537 |
+
print("📡 Backend API: http://localhost:5000")
|
| 1538 |
+
print("🔗 Web Interface: http://localhost:5000")
|
| 1539 |
+
print("")
|
| 1540 |
+
print("Press Ctrl+C to stop the server")
|
| 1541 |
+
print("")
|
| 1542 |
+
|
| 1543 |
+
# Clean and create fresh output folder on startup
|
| 1544 |
+
print("🧹 Cleaning output folder...")
|
| 1545 |
+
clean_and_create_output_folder()
|
| 1546 |
+
print("✅ Output folder ready!")
|
| 1547 |
+
print("")
|
| 1548 |
+
|
| 1549 |
+
app.run(debug=False, host='0.0.0.0', port=5000)
|
python/structure_preparation.py
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 os
|
| 8 |
+
import subprocess
|
| 9 |
+
import sys
|
| 10 |
+
import shutil
|
| 11 |
+
|
| 12 |
+
def run_command(cmd, description=""):
|
| 13 |
+
"""Run a command and return success status"""
|
| 14 |
+
try:
|
| 15 |
+
print(f"Running: {description}")
|
| 16 |
+
print(f"Command: {cmd}")
|
| 17 |
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=120)
|
| 18 |
+
print(f"Return code: {result.returncode}")
|
| 19 |
+
if result.stdout:
|
| 20 |
+
print(f"STDOUT: {result.stdout}")
|
| 21 |
+
if result.stderr:
|
| 22 |
+
print(f"STDERR: {result.stderr}")
|
| 23 |
+
if result.returncode != 0:
|
| 24 |
+
print(f"Error: {result.stderr}")
|
| 25 |
+
return False
|
| 26 |
+
return True
|
| 27 |
+
except subprocess.TimeoutExpired:
|
| 28 |
+
print(f"Timeout: {description}")
|
| 29 |
+
return False
|
| 30 |
+
except Exception as e:
|
| 31 |
+
print(f"Error running {description}: {str(e)}")
|
| 32 |
+
return False
|
| 33 |
+
|
| 34 |
+
def extract_protein_only(pdb_content, output_file, selected_chains=None):
|
| 35 |
+
"""Extract protein without hydrogens using MDAnalysis. Optionally restrict to selected chains."""
|
| 36 |
+
# Write input content to output file first
|
| 37 |
+
with open(output_file, 'w') as f:
|
| 38 |
+
f.write(pdb_content)
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
# Run MDAnalysis command with the output file as input
|
| 42 |
+
chain_sel = ''
|
| 43 |
+
if selected_chains:
|
| 44 |
+
chain_filters = ' or '.join([f'chain {c}' for c in selected_chains])
|
| 45 |
+
chain_sel = f' and ({chain_filters})'
|
| 46 |
+
selection = f"protein{chain_sel} and not name H* 1H* 2H* 3H*"
|
| 47 |
+
cmd = f'python -c "import MDAnalysis as mda; u=mda.Universe(\'{output_file}\'); u.select_atoms(\'{selection}\').write(\'{output_file}\')"'
|
| 48 |
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
|
| 49 |
+
|
| 50 |
+
if result.returncode != 0:
|
| 51 |
+
raise Exception(f"MDAnalysis error: {result.stderr}")
|
| 52 |
+
|
| 53 |
+
return True
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f"Error in extract_protein_only: {e}")
|
| 56 |
+
return False
|
| 57 |
+
|
| 58 |
+
def add_capping_groups(input_file, output_file):
|
| 59 |
+
"""Add ACE and NME capping groups using add_caps.py"""
|
| 60 |
+
# First add caps
|
| 61 |
+
temp_capped = output_file.replace('.pdb', '_temp.pdb')
|
| 62 |
+
cmd = f"python add_caps.py -i {input_file} -o {temp_capped}"
|
| 63 |
+
if not run_command(cmd, f"Adding capping groups to {input_file}"):
|
| 64 |
+
return False
|
| 65 |
+
|
| 66 |
+
# Then add TER cards using awk
|
| 67 |
+
cmd = f"awk '/NME/{{nme=NR}} /ACE/ && nme && NR > nme {{print \"TER\"; nme=0}} {{print}}' {temp_capped} > {output_file}"
|
| 68 |
+
if not run_command(cmd, f"Adding TER cards to {temp_capped}"):
|
| 69 |
+
return False
|
| 70 |
+
|
| 71 |
+
# Clean up temp file
|
| 72 |
+
if os.path.exists(temp_capped):
|
| 73 |
+
os.remove(temp_capped)
|
| 74 |
+
|
| 75 |
+
return True
|
| 76 |
+
|
| 77 |
+
def extract_selected_chains(pdb_content, output_file, selected_chains):
|
| 78 |
+
"""Extract selected chains using PyMOL commands"""
|
| 79 |
+
try:
|
| 80 |
+
# Write input content to temp file
|
| 81 |
+
temp_input = output_file.replace('.pdb', '_temp_input.pdb')
|
| 82 |
+
with open(temp_input, 'w') as f:
|
| 83 |
+
f.write(pdb_content)
|
| 84 |
+
|
| 85 |
+
# Build chain selection string
|
| 86 |
+
chain_filters = ' or '.join([f'chain {c}' for c in selected_chains])
|
| 87 |
+
selection = f"({chain_filters}) and polymer.protein"
|
| 88 |
+
|
| 89 |
+
# Use PyMOL to extract chains
|
| 90 |
+
cmd = f'''python -c "
|
| 91 |
+
import pymol
|
| 92 |
+
pymol.finish_launching(['pymol', '-c'])
|
| 93 |
+
pymol.cmd.load('{temp_input}')
|
| 94 |
+
pymol.cmd.save('{output_file}', '{selection}')
|
| 95 |
+
pymol.cmd.quit()
|
| 96 |
+
"'''
|
| 97 |
+
|
| 98 |
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
|
| 99 |
+
|
| 100 |
+
# Clean up temp file
|
| 101 |
+
if os.path.exists(temp_input):
|
| 102 |
+
os.remove(temp_input)
|
| 103 |
+
|
| 104 |
+
if result.returncode != 0:
|
| 105 |
+
print(f"PyMOL chain extraction error: {result.stderr}")
|
| 106 |
+
return False
|
| 107 |
+
|
| 108 |
+
return True
|
| 109 |
+
except Exception as e:
|
| 110 |
+
print(f"Error extracting selected chains: {e}")
|
| 111 |
+
return False
|
| 112 |
+
|
| 113 |
+
def extract_selected_ligands(pdb_content, output_file, selected_ligands):
|
| 114 |
+
"""Extract selected ligands using PyMOL commands"""
|
| 115 |
+
try:
|
| 116 |
+
# Write input content to temp file
|
| 117 |
+
temp_input = output_file.replace('.pdb', '_temp_input.pdb')
|
| 118 |
+
with open(temp_input, 'w') as f:
|
| 119 |
+
f.write(pdb_content)
|
| 120 |
+
|
| 121 |
+
# Build ligand selection string
|
| 122 |
+
parts = []
|
| 123 |
+
for lig in selected_ligands:
|
| 124 |
+
resn = lig.get('resn', '').strip()
|
| 125 |
+
chain = lig.get('chain', '').strip()
|
| 126 |
+
if resn and chain:
|
| 127 |
+
parts.append(f"(resn {resn} and chain {chain})")
|
| 128 |
+
elif resn:
|
| 129 |
+
parts.append(f"resn {resn}")
|
| 130 |
+
|
| 131 |
+
if not parts:
|
| 132 |
+
# No ligands to extract
|
| 133 |
+
with open(output_file, 'w') as f:
|
| 134 |
+
f.write('\n')
|
| 135 |
+
return True
|
| 136 |
+
|
| 137 |
+
selection = ' or '.join(parts)
|
| 138 |
+
|
| 139 |
+
# Use PyMOL to extract ligands
|
| 140 |
+
cmd = f'''python -c "
|
| 141 |
+
import pymol
|
| 142 |
+
pymol.finish_launching(['pymol', '-c'])
|
| 143 |
+
pymol.cmd.load('{temp_input}')
|
| 144 |
+
pymol.cmd.save('{output_file}', '{selection}')
|
| 145 |
+
pymol.cmd.quit()
|
| 146 |
+
"'''
|
| 147 |
+
|
| 148 |
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
|
| 149 |
+
|
| 150 |
+
# Clean up temp file
|
| 151 |
+
if os.path.exists(temp_input):
|
| 152 |
+
os.remove(temp_input)
|
| 153 |
+
|
| 154 |
+
if result.returncode != 0:
|
| 155 |
+
print(f"PyMOL ligand extraction error: {result.stderr}")
|
| 156 |
+
return False
|
| 157 |
+
|
| 158 |
+
return True
|
| 159 |
+
except Exception as e:
|
| 160 |
+
print(f"Error extracting selected ligands: {e}")
|
| 161 |
+
return False
|
| 162 |
+
|
| 163 |
+
def extract_ligands(pdb_content, output_file, ligand_residue_name=None, selected_ligands=None):
|
| 164 |
+
"""Extract ligands using MDAnalysis. Optionally restrict to selected ligands (list of dicts with resn, chain, resi)."""
|
| 165 |
+
# Write input content to output file first
|
| 166 |
+
with open(output_file, 'w') as f:
|
| 167 |
+
f.write(pdb_content)
|
| 168 |
+
|
| 169 |
+
try:
|
| 170 |
+
# Run MDAnalysis command with the output file as input
|
| 171 |
+
if selected_ligands:
|
| 172 |
+
# Build selection from provided ligand list (RESN-CHAIN groups)
|
| 173 |
+
parts = []
|
| 174 |
+
for lig in selected_ligands:
|
| 175 |
+
resn = lig.get('resn', '').strip()
|
| 176 |
+
chain = lig.get('chain', '').strip()
|
| 177 |
+
if resn and chain:
|
| 178 |
+
parts.append(f"(resname {resn} and segid {chain})")
|
| 179 |
+
elif resn:
|
| 180 |
+
parts.append(f"resname {resn}")
|
| 181 |
+
if parts:
|
| 182 |
+
selection = ' or '.join(parts)
|
| 183 |
+
cmd = f'''python -c "
|
| 184 |
+
import MDAnalysis as mda
|
| 185 |
+
u = mda.Universe('{output_file}')
|
| 186 |
+
u.select_atoms('{selection}').write('{output_file}')
|
| 187 |
+
"'''
|
| 188 |
+
else:
|
| 189 |
+
cmd = f"python -c \"open('{output_file}','w').write('\\n')\""
|
| 190 |
+
elif ligand_residue_name:
|
| 191 |
+
# Use specified ligand residue name - extract from both ATOM and HETATM records
|
| 192 |
+
cmd = f'''python -c "
|
| 193 |
+
import MDAnalysis as mda
|
| 194 |
+
u = mda.Universe('{output_file}')
|
| 195 |
+
# Extract specific ligand residue from both ATOM and HETATM records
|
| 196 |
+
u.select_atoms('resname {ligand_residue_name}').write('{output_file}')
|
| 197 |
+
"'''
|
| 198 |
+
else:
|
| 199 |
+
# Auto-detect ligand residues
|
| 200 |
+
cmd = f'''python -c "
|
| 201 |
+
import MDAnalysis as mda
|
| 202 |
+
u = mda.Universe('{output_file}')
|
| 203 |
+
# Get all unique residue names from HETATM records
|
| 204 |
+
hetatm_residues = set()
|
| 205 |
+
for atom in u.atoms:
|
| 206 |
+
if atom.record_type == 'HETATM':
|
| 207 |
+
hetatm_residues.add(atom.resname)
|
| 208 |
+
# Remove water and ions
|
| 209 |
+
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'}}
|
| 210 |
+
if ligand_residues:
|
| 211 |
+
resname_sel = ' or '.join([f'resname {{res}}' for res in ligand_residues])
|
| 212 |
+
u.select_atoms(resname_sel).write('{output_file}')
|
| 213 |
+
else:
|
| 214 |
+
# No ligands found, create empty file
|
| 215 |
+
with open('{output_file}', 'w') as f:
|
| 216 |
+
f.write('\\n')
|
| 217 |
+
"'''
|
| 218 |
+
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=60)
|
| 219 |
+
|
| 220 |
+
if result.returncode != 0:
|
| 221 |
+
raise Exception(f"MDAnalysis error: {result.stderr}")
|
| 222 |
+
|
| 223 |
+
# If specific ligand residue name was provided, convert ATOM to HETATM
|
| 224 |
+
if ligand_residue_name:
|
| 225 |
+
convert_atom_to_hetatm(output_file)
|
| 226 |
+
|
| 227 |
+
return True
|
| 228 |
+
except Exception as e:
|
| 229 |
+
print(f"Error in extract_ligands: {e}")
|
| 230 |
+
return False
|
| 231 |
+
|
| 232 |
+
def convert_atom_to_hetatm(pdb_file):
|
| 233 |
+
"""Convert ATOM records to HETATM in PDB file"""
|
| 234 |
+
try:
|
| 235 |
+
with open(pdb_file, 'r') as f:
|
| 236 |
+
lines = f.readlines()
|
| 237 |
+
|
| 238 |
+
# Convert ATOM to HETATM
|
| 239 |
+
converted_lines = []
|
| 240 |
+
for line in lines:
|
| 241 |
+
if line.startswith('ATOM'):
|
| 242 |
+
# Replace ATOM with HETATM
|
| 243 |
+
converted_line = 'HETATM' + line[6:]
|
| 244 |
+
converted_lines.append(converted_line)
|
| 245 |
+
else:
|
| 246 |
+
converted_lines.append(line)
|
| 247 |
+
|
| 248 |
+
# Write back to file
|
| 249 |
+
with open(pdb_file, 'w') as f:
|
| 250 |
+
f.writelines(converted_lines)
|
| 251 |
+
|
| 252 |
+
print(f"Converted ATOM records to HETATM in {pdb_file}")
|
| 253 |
+
return True
|
| 254 |
+
except Exception as e:
|
| 255 |
+
print(f"Error converting ATOM to HETATM: {e}")
|
| 256 |
+
return False
|
| 257 |
+
|
| 258 |
+
def correct_ligand_with_pymol(ligand_file, corrected_file):
|
| 259 |
+
"""Correct ligand using PyMOL"""
|
| 260 |
+
ligand_path = os.path.abspath(ligand_file)
|
| 261 |
+
corrected_path = os.path.abspath(corrected_file)
|
| 262 |
+
if not os.path.isfile(ligand_path) or os.path.getsize(ligand_path) == 0:
|
| 263 |
+
print("Ligand file missing or empty:", ligand_path)
|
| 264 |
+
return False
|
| 265 |
+
|
| 266 |
+
# Use PyMOL to add hydrogens and save corrected ligand
|
| 267 |
+
cmd = f'pymol -cq {ligand_path} -d "h_add; save {corrected_path}; quit"'
|
| 268 |
+
return run_command(cmd, f"Correcting ligand with PyMOL")
|
| 269 |
+
|
| 270 |
+
def remove_connect_records(pdb_file):
|
| 271 |
+
"""Remove CONNECT records from PDB file"""
|
| 272 |
+
try:
|
| 273 |
+
with open(pdb_file, 'r') as f:
|
| 274 |
+
lines = f.readlines()
|
| 275 |
+
|
| 276 |
+
# Filter out CONNECT records
|
| 277 |
+
filtered_lines = [line for line in lines if not line.startswith('CONECT')]
|
| 278 |
+
|
| 279 |
+
with open(pdb_file, 'w') as f:
|
| 280 |
+
f.writelines(filtered_lines)
|
| 281 |
+
|
| 282 |
+
print(f"Removed CONNECT records from {pdb_file}")
|
| 283 |
+
return True
|
| 284 |
+
except Exception as e:
|
| 285 |
+
print(f"Error removing CONNECT records: {e}")
|
| 286 |
+
return False
|
| 287 |
+
|
| 288 |
+
def merge_protein_and_ligand(protein_file, ligand_file, output_file):
|
| 289 |
+
"""Merge capped protein and corrected ligand with proper PDB formatting"""
|
| 290 |
+
try:
|
| 291 |
+
# Read protein file
|
| 292 |
+
with open(protein_file, 'r') as f:
|
| 293 |
+
protein_lines = f.readlines()
|
| 294 |
+
|
| 295 |
+
# Read ligand file
|
| 296 |
+
with open(ligand_file, 'r') as f:
|
| 297 |
+
ligand_lines = f.readlines()
|
| 298 |
+
|
| 299 |
+
# Process protein file: remove 'END' and add properly formatted 'TER'
|
| 300 |
+
protein_processed = []
|
| 301 |
+
last_atom_line = None
|
| 302 |
+
for line in protein_lines:
|
| 303 |
+
if line.strip() == 'END':
|
| 304 |
+
# Create properly formatted TER card using the last atom's info
|
| 305 |
+
if last_atom_line and last_atom_line.startswith('ATOM'):
|
| 306 |
+
# Extract atom number and residue info from last atom
|
| 307 |
+
atom_num = last_atom_line[6:11].strip()
|
| 308 |
+
res_name = last_atom_line[17:20].strip()
|
| 309 |
+
chain_id = last_atom_line[21:22].strip()
|
| 310 |
+
res_num = last_atom_line[22:26].strip()
|
| 311 |
+
ter_line = f"TER {atom_num:>5} {res_name} {chain_id}{res_num}\n"
|
| 312 |
+
protein_processed.append(ter_line)
|
| 313 |
+
else:
|
| 314 |
+
protein_processed.append('TER\n')
|
| 315 |
+
else:
|
| 316 |
+
protein_processed.append(line)
|
| 317 |
+
if line.startswith('ATOM'):
|
| 318 |
+
last_atom_line = line
|
| 319 |
+
|
| 320 |
+
# Process ligand file: remove header info (CRYST, REMARK, etc.) and keep only ATOM/HETATM
|
| 321 |
+
ligand_processed = []
|
| 322 |
+
for line in ligand_lines:
|
| 323 |
+
if line.startswith(('ATOM', 'HETATM')):
|
| 324 |
+
ligand_processed.append(line)
|
| 325 |
+
|
| 326 |
+
# Combine: protein + TER + ligand + END (no extra newline between TER and ligand)
|
| 327 |
+
merged_content = ''.join(protein_processed) + ''.join(ligand_processed) + 'END\n'
|
| 328 |
+
|
| 329 |
+
with open(output_file, 'w') as f:
|
| 330 |
+
f.write(merged_content)
|
| 331 |
+
|
| 332 |
+
return True
|
| 333 |
+
except Exception as e:
|
| 334 |
+
print(f"Error merging files: {str(e)}")
|
| 335 |
+
return False
|
| 336 |
+
|
| 337 |
+
def prepare_structure(pdb_content, options, output_dir="output"):
|
| 338 |
+
"""Main function to prepare structure for AMBER simulation"""
|
| 339 |
+
try:
|
| 340 |
+
# Create output directory if it doesn't exist
|
| 341 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 342 |
+
|
| 343 |
+
# Define all file paths in output directory
|
| 344 |
+
input_file = os.path.join(output_dir, "0_original_input.pdb")
|
| 345 |
+
user_chain_file = os.path.join(output_dir, "0_user_chain_selected.pdb")
|
| 346 |
+
protein_file = os.path.join(output_dir, "1_protein_no_hydrogens.pdb")
|
| 347 |
+
protein_capped_file = os.path.join(output_dir, "2_protein_with_caps.pdb")
|
| 348 |
+
ligand_file = os.path.join(output_dir, "3_ligands_extracted.pdb")
|
| 349 |
+
ligand_corrected_file = os.path.join(output_dir, "4_ligands_corrected.pdb")
|
| 350 |
+
tleap_ready_file = os.path.join(output_dir, "tleap_ready.pdb")
|
| 351 |
+
|
| 352 |
+
# Step 0: Save original input for reference
|
| 353 |
+
print("Step 0: Saving original input...")
|
| 354 |
+
with open(input_file, 'w') as f:
|
| 355 |
+
f.write(pdb_content)
|
| 356 |
+
|
| 357 |
+
# Step 0.5: Extract user-selected chains and ligands
|
| 358 |
+
selected_chains = options.get('selected_chains', [])
|
| 359 |
+
selected_ligands = options.get('selected_ligands', [])
|
| 360 |
+
|
| 361 |
+
if selected_chains:
|
| 362 |
+
print(f"Step 0.5a: Extracting selected chains: {', '.join(selected_chains)}")
|
| 363 |
+
if not extract_selected_chains(pdb_content, user_chain_file, selected_chains):
|
| 364 |
+
raise Exception("Failed to extract selected chains")
|
| 365 |
+
else:
|
| 366 |
+
print("Step 0.5a: No chains selected, using original structure")
|
| 367 |
+
shutil.copy2(input_file, user_chain_file)
|
| 368 |
+
|
| 369 |
+
if selected_ligands:
|
| 370 |
+
ligand_names = [f"{l.get('resn', '')}-{l.get('chain', '')}" for l in selected_ligands]
|
| 371 |
+
print(f"Step 0.5b: Extracting selected ligands: {ligand_names}")
|
| 372 |
+
if not extract_selected_ligands(pdb_content, ligand_file, selected_ligands):
|
| 373 |
+
raise Exception("Failed to extract selected ligands")
|
| 374 |
+
else:
|
| 375 |
+
print("Step 0.5b: No ligands selected, creating empty ligand file")
|
| 376 |
+
with open(ligand_file, 'w') as f:
|
| 377 |
+
f.write('\n')
|
| 378 |
+
|
| 379 |
+
# Step 1: Extract protein only (remove hydrogens) from user-selected chains
|
| 380 |
+
print("Step 1: Extracting protein without hydrogens from selected chains...")
|
| 381 |
+
# Read the user-selected chain file
|
| 382 |
+
with open(user_chain_file, 'r') as f:
|
| 383 |
+
chain_content = f.read()
|
| 384 |
+
|
| 385 |
+
if not extract_protein_only(chain_content, protein_file):
|
| 386 |
+
raise Exception("Failed to extract protein")
|
| 387 |
+
|
| 388 |
+
# Step 2: Add capping groups (only if add_ace or add_nme is True)
|
| 389 |
+
add_ace = options.get('add_ace', True)
|
| 390 |
+
add_nme = options.get('add_nme', True)
|
| 391 |
+
|
| 392 |
+
if add_ace or add_nme:
|
| 393 |
+
print("Step 2: Adding ACE and NME capping groups...")
|
| 394 |
+
if not add_capping_groups(protein_file, protein_capped_file):
|
| 395 |
+
raise Exception("Failed to add capping groups")
|
| 396 |
+
else:
|
| 397 |
+
print("Step 2: Skipping capping groups (add_ace=False, add_nme=False)")
|
| 398 |
+
print("Using protein without capping - copying to capped file")
|
| 399 |
+
# Copy protein file to capped file (no capping)
|
| 400 |
+
shutil.copy2(protein_file, protein_capped_file)
|
| 401 |
+
|
| 402 |
+
# Step 3: Handle ligands (use pre-extracted ligand file)
|
| 403 |
+
preserve_ligands = options.get('preserve_ligands', True)
|
| 404 |
+
ligand_present = False
|
| 405 |
+
|
| 406 |
+
if preserve_ligands:
|
| 407 |
+
print("Step 3: Processing pre-extracted ligands...")
|
| 408 |
+
|
| 409 |
+
# Check if ligand file has content (not just empty or newline)
|
| 410 |
+
with open(ligand_file, 'r') as f:
|
| 411 |
+
ligand_content = f.read().strip()
|
| 412 |
+
|
| 413 |
+
if ligand_content and len(ligand_content) > 1:
|
| 414 |
+
ligand_present = True
|
| 415 |
+
print("Found pre-extracted ligands")
|
| 416 |
+
|
| 417 |
+
# Correct ligand with PyMOL
|
| 418 |
+
if not correct_ligand_with_pymol(ligand_file, ligand_corrected_file):
|
| 419 |
+
print("Error: Failed to process ligand")
|
| 420 |
+
return {
|
| 421 |
+
'error': 'Failed to process ligand with PyMOL',
|
| 422 |
+
'prepared_structure': '',
|
| 423 |
+
'original_atoms': 0,
|
| 424 |
+
'prepared_atoms': 0,
|
| 425 |
+
'removed_components': {},
|
| 426 |
+
'added_capping': {},
|
| 427 |
+
'preserved_ligands': 0,
|
| 428 |
+
'ligand_present': False
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
# Merge protein and ligand
|
| 432 |
+
if not merge_protein_and_ligand(protein_capped_file, ligand_corrected_file, tleap_ready_file):
|
| 433 |
+
raise Exception("Failed to merge protein and ligand")
|
| 434 |
+
else:
|
| 435 |
+
print("No ligands found in pre-extracted file, using protein only")
|
| 436 |
+
# Copy protein file to tleap_ready
|
| 437 |
+
shutil.copy2(protein_capped_file, tleap_ready_file)
|
| 438 |
+
else:
|
| 439 |
+
print("Step 3: Skipping ligand processing (preserve_ligands=False)")
|
| 440 |
+
print("Using protein only - copying capped protein to tleap_ready")
|
| 441 |
+
# Copy protein file to tleap_ready (protein only, no ligands)
|
| 442 |
+
shutil.copy2(protein_capped_file, tleap_ready_file)
|
| 443 |
+
|
| 444 |
+
# Remove CONNECT records from tleap_ready.pdb (PyMOL adds them)
|
| 445 |
+
print("Removing CONNECT records from tleap_ready.pdb...")
|
| 446 |
+
remove_connect_records(tleap_ready_file)
|
| 447 |
+
|
| 448 |
+
# Read the final prepared structure
|
| 449 |
+
with open(tleap_ready_file, 'r') as f:
|
| 450 |
+
prepared_content = f.read()
|
| 451 |
+
|
| 452 |
+
# Calculate statistics
|
| 453 |
+
original_atoms = len([line for line in pdb_content.split('\n') if line.startswith('ATOM')])
|
| 454 |
+
prepared_atoms = len([line for line in prepared_content.split('\n') if line.startswith('ATOM')])
|
| 455 |
+
|
| 456 |
+
# Calculate removed components
|
| 457 |
+
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']])
|
| 458 |
+
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']])
|
| 459 |
+
hydrogen_count = len([line for line in pdb_content.split('\n') if line.startswith('ATOM') and line[76:78].strip() == 'H'])
|
| 460 |
+
|
| 461 |
+
# If not preserving ligands, count them as removed
|
| 462 |
+
ligand_count = 0
|
| 463 |
+
if not preserve_ligands and ligand_present:
|
| 464 |
+
# Count ligands from the pre-extracted file
|
| 465 |
+
with open(ligand_file, 'r') as f:
|
| 466 |
+
ligand_lines = [line for line in f if line.startswith('HETATM')]
|
| 467 |
+
ligand_count = len(set(line[17:20].strip() for line in ligand_lines))
|
| 468 |
+
|
| 469 |
+
removed_components = {
|
| 470 |
+
'water': water_count,
|
| 471 |
+
'ions': ion_count,
|
| 472 |
+
'hydrogens': hydrogen_count,
|
| 473 |
+
'ligands': ligand_count
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
# Calculate added capping groups (only if capping was performed)
|
| 477 |
+
if add_ace or add_nme:
|
| 478 |
+
# Count unique ACE and NME residues, not individual atoms
|
| 479 |
+
ace_residues = set()
|
| 480 |
+
nme_residues = set()
|
| 481 |
+
|
| 482 |
+
for line in prepared_content.split('\n'):
|
| 483 |
+
if line.startswith('ATOM') and 'ACE' in line:
|
| 484 |
+
# Extract residue number to count unique ACE groups
|
| 485 |
+
res_num = line[22:26].strip()
|
| 486 |
+
ace_residues.add(res_num)
|
| 487 |
+
elif line.startswith('ATOM') and 'NME' in line:
|
| 488 |
+
# Extract residue number to count unique NME groups
|
| 489 |
+
res_num = line[22:26].strip()
|
| 490 |
+
nme_residues.add(res_num)
|
| 491 |
+
|
| 492 |
+
added_capping = {
|
| 493 |
+
'ace_groups': len(ace_residues),
|
| 494 |
+
'nme_groups': len(nme_residues)
|
| 495 |
+
}
|
| 496 |
+
else:
|
| 497 |
+
added_capping = {
|
| 498 |
+
'ace_groups': 0,
|
| 499 |
+
'nme_groups': 0
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
# Count preserved ligands from the pre-extracted file
|
| 503 |
+
preserved_ligands = 0
|
| 504 |
+
if ligand_present and preserve_ligands:
|
| 505 |
+
with open(ligand_file, 'r') as f:
|
| 506 |
+
ligand_lines = [line for line in f if line.startswith('HETATM')]
|
| 507 |
+
preserved_ligands = len(set(line[17:20].strip() for line in ligand_lines))
|
| 508 |
+
|
| 509 |
+
result = {
|
| 510 |
+
'prepared_structure': prepared_content,
|
| 511 |
+
'original_atoms': original_atoms,
|
| 512 |
+
'prepared_atoms': prepared_atoms,
|
| 513 |
+
'removed_components': removed_components,
|
| 514 |
+
'added_capping': added_capping,
|
| 515 |
+
'preserved_ligands': preserved_ligands,
|
| 516 |
+
'ligand_present': ligand_present,
|
| 517 |
+
'separate_ligands': options.get('separate_ligands', False)
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
# If separate ligands is enabled and ligands are present, include ligand content
|
| 521 |
+
if ligand_present and options.get('separate_ligands', False):
|
| 522 |
+
with open(ligand_corrected_file, 'r') as f:
|
| 523 |
+
result['ligand_content'] = f.read()
|
| 524 |
+
|
| 525 |
+
return result
|
| 526 |
+
|
| 527 |
+
except Exception as e:
|
| 528 |
+
return {
|
| 529 |
+
'error': str(e),
|
| 530 |
+
'prepared_structure': '',
|
| 531 |
+
'original_atoms': 0,
|
| 532 |
+
'prepared_atoms': 0,
|
| 533 |
+
'removed_components': {},
|
| 534 |
+
'added_capping': {},
|
| 535 |
+
'preserved_ligands': 0,
|
| 536 |
+
'ligand_present': False
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
def parse_structure_info(pdb_content):
|
| 540 |
+
"""Parse structure information for display"""
|
| 541 |
+
lines = pdb_content.split('\n')
|
| 542 |
+
atom_count = 0
|
| 543 |
+
chains = set()
|
| 544 |
+
residues = set()
|
| 545 |
+
water_molecules = 0
|
| 546 |
+
ions = 0
|
| 547 |
+
ligands = set()
|
| 548 |
+
hetatoms = 0
|
| 549 |
+
|
| 550 |
+
# Common water molecule names
|
| 551 |
+
water_names = {'HOH', 'WAT', 'TIP3', 'TIP4', 'SPC', 'SPCE'}
|
| 552 |
+
|
| 553 |
+
# Common ion names
|
| 554 |
+
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'}
|
| 555 |
+
|
| 556 |
+
# Common ligand indicators
|
| 557 |
+
ligand_indicators = {'ATP', 'ADP', 'AMP', 'GDP', 'GTP', 'NAD', 'FAD', 'HEM', 'HEME', 'COA', 'SAM', 'PLP', 'THF', 'FMN', 'FAD', 'NADP', 'UDP', 'CDP', 'TDP', 'GDP', 'ADP', 'ATP'}
|
| 558 |
+
|
| 559 |
+
for line in lines:
|
| 560 |
+
if line.startswith('ATOM'):
|
| 561 |
+
atom_count += 1
|
| 562 |
+
chain_id = line[21:22].strip()
|
| 563 |
+
if chain_id:
|
| 564 |
+
chains.add(chain_id)
|
| 565 |
+
|
| 566 |
+
res_name = line[17:20].strip()
|
| 567 |
+
res_num = line[22:26].strip()
|
| 568 |
+
residues.add(f"{res_name}{res_num}")
|
| 569 |
+
elif line.startswith('HETATM'):
|
| 570 |
+
hetatoms += 1
|
| 571 |
+
res_name = line[17:20].strip()
|
| 572 |
+
|
| 573 |
+
if res_name in water_names:
|
| 574 |
+
water_molecules += 1
|
| 575 |
+
elif res_name in ion_names:
|
| 576 |
+
ions += 1
|
| 577 |
+
elif res_name in ligand_indicators:
|
| 578 |
+
ligands.add(res_name)
|
| 579 |
+
|
| 580 |
+
# Count unique water molecules
|
| 581 |
+
unique_water_residues = set()
|
| 582 |
+
for line in lines:
|
| 583 |
+
if line.startswith('HETATM'):
|
| 584 |
+
res_name = line[17:20].strip()
|
| 585 |
+
res_num = line[22:26].strip()
|
| 586 |
+
if res_name in water_names:
|
| 587 |
+
unique_water_residues.add(f"{res_name}{res_num}")
|
| 588 |
+
|
| 589 |
+
return {
|
| 590 |
+
'atom_count': atom_count,
|
| 591 |
+
'chains': list(chains),
|
| 592 |
+
'residue_count': len(residues),
|
| 593 |
+
'water_molecules': len(unique_water_residues),
|
| 594 |
+
'ions': ions,
|
| 595 |
+
'ligands': list(ligands),
|
| 596 |
+
'hetatoms': hetatoms
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
def test_structure_preparation():
|
| 600 |
+
"""Test function to verify structure preparation works correctly"""
|
| 601 |
+
# Create a simple test PDB content
|
| 602 |
+
test_pdb = """HEADER TEST PROTEIN
|
| 603 |
+
ATOM 1 N MET A 1 16.347 37.019 21.335 1.00 50.73 N
|
| 604 |
+
ATOM 2 CA MET A 1 15.737 37.120 20.027 1.00 45.30 C
|
| 605 |
+
ATOM 3 C MET A 1 15.955 35.698 19.546 1.00 41.78 C
|
| 606 |
+
ATOM 4 O MET A 1 16.847 35.123 20.123 1.00 40.15 O
|
| 607 |
+
ATOM 5 CB MET A 1 14.234 37.456 19.789 1.00 44.12 C
|
| 608 |
+
ATOM 6 CG MET A 1 13.456 36.123 19.234 1.00 43.45 C
|
| 609 |
+
ATOM 7 SD MET A 1 12.123 35.456 18.123 1.00 42.78 S
|
| 610 |
+
ATOM 8 CE MET A 1 11.456 34.123 17.456 1.00 42.11 C
|
| 611 |
+
ATOM 9 N ALA A 2 15.123 35.456 18.789 1.00 40.44 N
|
| 612 |
+
ATOM 10 CA ALA A 2 14.456 34.123 18.123 1.00 39.77 C
|
| 613 |
+
ATOM 11 C ALA A 2 13.123 33.456 17.456 1.00 39.10 C
|
| 614 |
+
ATOM 12 O ALA A 2 12.456 32.123 16.789 1.00 38.43 O
|
| 615 |
+
ATOM 13 CB ALA A 2 13.789 33.123 17.123 1.00 38.76 C
|
| 616 |
+
ATOM 14 N ALA A 3 12.789 32.456 16.123 1.00 38.09 N
|
| 617 |
+
ATOM 15 CA ALA A 3 11.456 31.789 15.456 1.00 37.42 C
|
| 618 |
+
ATOM 16 C ALA A 3 10.123 30.456 14.789 1.00 36.75 C
|
| 619 |
+
ATOM 17 O ALA A 3 9.456 29.123 14.123 1.00 36.08 O
|
| 620 |
+
ATOM 18 CB ALA A 3 9.789 29.456 13.456 1.00 35.41 C
|
| 621 |
+
ATOM 19 OXT ALA A 3 8.123 28.789 13.456 1.00 35.74 O
|
| 622 |
+
HETATM 20 O HOH A 4 20.000 20.000 20.000 1.00 20.00 O
|
| 623 |
+
HETATM 21 H1 HOH A 4 20.500 20.500 20.500 1.00 20.00 H
|
| 624 |
+
HETATM 22 H2 HOH A 4 19.500 19.500 19.500 1.00 20.00 H
|
| 625 |
+
HETATM 23 NA NA A 5 25.000 25.000 25.000 1.00 25.00 NA
|
| 626 |
+
HETATM 24 CL CL A 6 30.000 30.000 30.000 1.00 30.00 CL
|
| 627 |
+
HETATM 1 PG GTP A 180 29.710 30.132 -5.989 1.00 52.48 A P
|
| 628 |
+
HETATM 2 O1G GTP A 180 29.197 28.937 -5.265 1.00 43.51 A O
|
| 629 |
+
HETATM 3 O2G GTP A 180 30.881 29.816 -6.827 1.00 63.11 A O
|
| 630 |
+
HETATM 4 O3G GTP A 180 30.013 31.278 -5.117 1.00 29.97 A O
|
| 631 |
+
HETATM 5 O3B GTP A 180 28.517 30.631 -6.995 1.00 23.23 A O
|
| 632 |
+
HETATM 6 PB GTP A 180 27.017 31.171 -6.766 1.00 29.58 A P
|
| 633 |
+
HETATM 7 O1B GTP A 180 26.072 30.050 -6.958 1.00 17.62 A O
|
| 634 |
+
HETATM 8 O2B GTP A 180 26.960 31.913 -5.483 1.00 38.76 A O
|
| 635 |
+
HETATM 9 O3A GTP A 180 26.807 32.212 -7.961 1.00 13.12 A O
|
| 636 |
+
HETATM 10 PA GTP A 180 26.277 33.726 -8.045 1.00 25.06 A P
|
| 637 |
+
HETATM 11 O1A GTP A 180 25.089 33.867 -7.187 1.00 44.06 A O
|
| 638 |
+
HETATM 12 O2A GTP A 180 27.427 34.635 -7.843 1.00 23.47 A O
|
| 639 |
+
HETATM 13 O5' GTP A 180 25.804 33.834 -9.555 1.00 42.05 A O
|
| 640 |
+
HETATM 14 C5' GTP A 180 26.615 33.475 -10.679 1.00 19.97 A C
|
| 641 |
+
HETATM 15 C4' GTP A 180 26.219 34.288 -11.894 1.00 14.90 A C
|
| 642 |
+
HETATM 16 O4' GTP A 180 24.826 34.017 -12.143 1.00 19.00 A O
|
| 643 |
+
HETATM 17 C3' GTP A 180 26.372 35.802 -11.724 1.00 4.96 A C
|
| 644 |
+
HETATM 18 O3' GTP A 180 26.880 36.347 -12.936 1.00 44.49 A O
|
| 645 |
+
HETATM 19 C2' GTP A 180 24.932 36.243 -11.481 1.00 17.12 A C
|
| 646 |
+
HETATM 20 O2' GTP A 180 24.719 37.581 -11.901 1.00 32.45 A O
|
| 647 |
+
HETATM 21 C1' GTP A 180 24.069 35.240 -12.240 1.00 16.17 A C
|
| 648 |
+
HETATM 22 N9 GTP A 180 22.724 35.005 -11.630 1.00 28.10 A N
|
| 649 |
+
HETATM 23 C8 GTP A 180 22.443 34.655 -10.325 1.00 27.05 A C
|
| 650 |
+
HETATM 24 N7 GTP A 180 21.168 34.483 -10.079 1.00 33.25 A N
|
| 651 |
+
HETATM 25 C5 GTP A 180 20.554 34.737 -11.307 1.00 26.23 A C
|
| 652 |
+
HETATM 26 C6 GTP A 180 19.183 34.712 -11.659 1.00 29.31 A C
|
| 653 |
+
HETATM 27 O6 GTP A 180 18.205 34.448 -10.957 1.00 40.80 A O
|
| 654 |
+
HETATM 28 N1 GTP A 180 19.000 35.036 -13.013 1.00 26.85 A N
|
| 655 |
+
HETATM 29 C2 GTP A 180 20.022 35.339 -13.903 1.00 28.70 A C
|
| 656 |
+
HETATM 30 N2 GTP A 180 19.627 35.619 -15.147 1.00 44.24 A N
|
| 657 |
+
HETATM 31 N3 GTP A 180 21.301 35.367 -13.569 1.00 21.67 A N
|
| 658 |
+
HETATM 32 C4 GTP A 180 21.489 35.054 -12.257 1.00 41.91 A C
|
| 659 |
+
END
|
| 660 |
+
"""
|
| 661 |
+
|
| 662 |
+
options = {
|
| 663 |
+
'remove_water': True,
|
| 664 |
+
'remove_ions': True,
|
| 665 |
+
'remove_hydrogens': True,
|
| 666 |
+
'add_ace': True,
|
| 667 |
+
'add_nme': True,
|
| 668 |
+
'preserve_ligands': True,
|
| 669 |
+
'separate_ligands': False,
|
| 670 |
+
'fix_missing_atoms': False,
|
| 671 |
+
'standardize_residues': False
|
| 672 |
+
}
|
| 673 |
+
|
| 674 |
+
print("Testing structure preparation...")
|
| 675 |
+
result = prepare_structure(test_pdb, options, "output")
|
| 676 |
+
|
| 677 |
+
print("\n=== STATISTICS ===")
|
| 678 |
+
print(f"Original atoms: {result['original_atoms']}")
|
| 679 |
+
print(f"Prepared atoms: {result['prepared_atoms']}")
|
| 680 |
+
print(f"Removed: {result['removed_components']}")
|
| 681 |
+
print(f"Added: {result['added_capping']}")
|
| 682 |
+
print(f"Ligands: {result['preserved_ligands']}")
|
| 683 |
+
print(f"Ligand present: {result['ligand_present']}")
|
| 684 |
+
|
| 685 |
+
print(f"\nTest completed! Check 'output' folder for results:")
|
| 686 |
+
print("- 1_protein_no_hydrogens.pdb (protein without hydrogens)")
|
| 687 |
+
print("- 2_protein_with_caps.pdb (protein with ACE/NME caps)")
|
| 688 |
+
print("- 3_ligands_extracted.pdb (extracted ligands, if any)")
|
| 689 |
+
print("- 4_ligands_corrected.pdb (corrected ligands, if any)")
|
| 690 |
+
print("- tleap_ready.pdb (final structure ready for tleap)")
|
| 691 |
+
|
| 692 |
+
if __name__ == "__main__":
|
| 693 |
+
test_structure_preparation()
|
start_web_server.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AmberFlow - Hugging Face Spaces Entry Point
|
| 4 |
+
This file serves as the entry point for Hugging Face Spaces deployment.
|
| 5 |
+
It imports and runs the existing Flask application from python/app.py
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
import subprocess
|
| 11 |
+
|
| 12 |
+
# Add the python directory to the path so we can import the Flask app
|
| 13 |
+
sys.path.append(os.path.join(os.path.dirname(__file__), 'python'))
|
| 14 |
+
|
| 15 |
+
# Import the Flask app from the existing python/app.py
|
| 16 |
+
from python.app import app
|
| 17 |
+
|
| 18 |
+
if __name__ == "__main__":
|
| 19 |
+
# Run the Flask app
|
| 20 |
+
app.run(debug=False, host='0.0.0.0', port=7860)
|