hemantn commited on
Commit
cc7c981
·
1 Parent(s): 6d0ef0d

Deploy AmberFlow to Hugging Face Spaces

Browse files
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: indigo
5
  colorTo: purple
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
- short_description: AMBER input simulation files generator for proteins
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>&copy; 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)