|
|
|
|
|
""" |
|
|
MD Simulation Pipeline - Flask Backend |
|
|
Provides API endpoints for protein processing and file generation |
|
|
""" |
|
|
|
|
|
from flask import Flask, request, jsonify, send_file, render_template, send_from_directory |
|
|
from flask_cors import CORS |
|
|
import os |
|
|
import json |
|
|
import tempfile |
|
|
import zipfile |
|
|
from pathlib import Path |
|
|
import requests |
|
|
import subprocess |
|
|
from Bio.PDB import PDBParser, PDBList |
|
|
import logging |
|
|
from structure_preparation import prepare_structure, parse_structure_info |
|
|
|
|
|
app = Flask(__name__, |
|
|
template_folder='../html', |
|
|
static_folder='../', |
|
|
static_url_path='') |
|
|
CORS(app) |
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
OUTPUT_DIR = Path(__file__).parent.parent / "output" |
|
|
|
|
|
def clean_and_create_output_folder(): |
|
|
"""Clean existing output folder and create a new one""" |
|
|
try: |
|
|
print(f"DEBUG: Starting cleanup. OUTPUT_DIR = {OUTPUT_DIR}") |
|
|
print(f"DEBUG: OUTPUT_DIR.exists() = {OUTPUT_DIR.exists()}") |
|
|
|
|
|
|
|
|
if OUTPUT_DIR.exists(): |
|
|
import shutil |
|
|
print(f"DEBUG: Removing existing output folder: {OUTPUT_DIR}") |
|
|
shutil.rmtree(OUTPUT_DIR) |
|
|
print(f"DEBUG: Successfully removed output folder") |
|
|
logger.info(f"Removed existing output folder: {OUTPUT_DIR}") |
|
|
|
|
|
|
|
|
print(f"DEBUG: Creating new output folder: {OUTPUT_DIR}") |
|
|
OUTPUT_DIR.mkdir(parents=True, exist_ok=True) |
|
|
print(f"DEBUG: Successfully created output folder") |
|
|
logger.info(f"Created new output folder: {OUTPUT_DIR}") |
|
|
|
|
|
return True |
|
|
except Exception as e: |
|
|
print(f"DEBUG: Error in cleanup: {str(e)}") |
|
|
logger.error(f"Error cleaning output folder: {str(e)}") |
|
|
return False |
|
|
|
|
|
class MDSimulationGenerator: |
|
|
"""Handles MD simulation file generation and protein processing""" |
|
|
|
|
|
def __init__(self): |
|
|
self.pdb_parser = PDBParser(QUIET=True) |
|
|
self.pdb_list = PDBList() |
|
|
|
|
|
def fetch_pdb_structure(self, pdb_id): |
|
|
"""Fetch PDB structure from RCSB""" |
|
|
try: |
|
|
|
|
|
pdb_file = self.pdb_list.retrieve_pdb_file(pdb_id, pdir=OUTPUT_DIR, file_format='pdb') |
|
|
return str(pdb_file) |
|
|
except Exception as e: |
|
|
logger.error(f"Error fetching PDB {pdb_id}: {str(e)}") |
|
|
raise |
|
|
|
|
|
def parse_pdb_structure(self, pdb_file): |
|
|
"""Parse PDB file and extract structure information""" |
|
|
try: |
|
|
structure = self.pdb_parser.get_structure('protein', pdb_file) |
|
|
|
|
|
|
|
|
atom_count = 0 |
|
|
chains = set() |
|
|
residues = set() |
|
|
|
|
|
for model in structure: |
|
|
for chain in model: |
|
|
chains.add(chain.id) |
|
|
for residue in chain: |
|
|
if residue.id[0] == ' ': |
|
|
residues.add(f"{residue.resname}{residue.id[1]}") |
|
|
for atom in residue: |
|
|
atom_count += 1 |
|
|
|
|
|
return { |
|
|
'atom_count': atom_count, |
|
|
'chains': list(chains), |
|
|
'residue_count': len(residues), |
|
|
'structure_id': Path(pdb_file).stem.upper() |
|
|
} |
|
|
except Exception as e: |
|
|
logger.error(f"Error parsing PDB file: {str(e)}") |
|
|
raise |
|
|
|
|
|
def generate_mdp_file(self, params, step_type='production'): |
|
|
"""Generate GROMACS MDP file for different simulation steps""" |
|
|
|
|
|
if step_type == 'restrained_min': |
|
|
return f"""; Restrained Minimization Parameters |
|
|
integrator = steep |
|
|
nsteps = {params['steps']['restrainedMin']['steps']} |
|
|
emstep = 0.01 |
|
|
emtol = 1000 |
|
|
|
|
|
; Position restraints |
|
|
define = -DPOSRES |
|
|
refcoord_scaling = com |
|
|
|
|
|
; Output control |
|
|
nstxout = 100 |
|
|
nstenergy = 100 |
|
|
nstlog = 100 |
|
|
|
|
|
; Bond parameters |
|
|
constraint_algorithm = lincs |
|
|
constraints = h-bonds |
|
|
|
|
|
; Neighbor searching |
|
|
cutoff-scheme = Verlet |
|
|
ns_type = grid |
|
|
nstlist = 10 |
|
|
rlist = {params['cutoff']} |
|
|
|
|
|
; Electrostatics |
|
|
coulombtype = PME |
|
|
rcoulomb = {params['cutoff']} |
|
|
pme_order = {params['pmeOrder']} |
|
|
|
|
|
; Van der Waals |
|
|
vdwtype = Cut-off |
|
|
rvdw = {params['cutoff']} |
|
|
""" |
|
|
|
|
|
elif step_type == 'minimization': |
|
|
return f"""; Minimization Parameters |
|
|
integrator = {params['steps']['minimization']['algorithm']} |
|
|
nsteps = {params['steps']['minimization']['steps']} |
|
|
emstep = 0.01 |
|
|
emtol = 1000 |
|
|
|
|
|
; Output control |
|
|
nstxout = 100 |
|
|
nstenergy = 100 |
|
|
nstlog = 100 |
|
|
|
|
|
; Bond parameters |
|
|
constraint_algorithm = lincs |
|
|
constraints = h-bonds |
|
|
|
|
|
; Neighbor searching |
|
|
cutoff-scheme = Verlet |
|
|
ns_type = grid |
|
|
nstlist = 10 |
|
|
rlist = {params['cutoff']} |
|
|
|
|
|
; Electrostatics |
|
|
coulombtype = PME |
|
|
rcoulomb = {params['cutoff']} |
|
|
pme_order = {params['pmeOrder']} |
|
|
|
|
|
; Van der Waals |
|
|
vdwtype = Cut-off |
|
|
rvdw = {params['cutoff']} |
|
|
""" |
|
|
|
|
|
elif step_type == 'nvt': |
|
|
return f"""; NVT Equilibration Parameters |
|
|
integrator = md |
|
|
dt = {params['timestep']} |
|
|
nsteps = {params['steps']['nvt']['steps']} |
|
|
|
|
|
; Output control |
|
|
nstxout = 5000 |
|
|
nstvout = 5000 |
|
|
nstenergy = 1000 |
|
|
nstlog = 1000 |
|
|
|
|
|
; Bond parameters |
|
|
constraint_algorithm = lincs |
|
|
constraints = h-bonds |
|
|
lincs_iter = 1 |
|
|
lincs_order = 4 |
|
|
|
|
|
; Neighbor searching |
|
|
cutoff-scheme = Verlet |
|
|
ns_type = grid |
|
|
nstlist = 40 |
|
|
rlist = {params['cutoff']} |
|
|
|
|
|
; Electrostatics |
|
|
coulombtype = PME |
|
|
rcoulomb = {params['cutoff']} |
|
|
pme_order = {params['pmeOrder']} |
|
|
|
|
|
; Van der Waals |
|
|
vdwtype = Cut-off |
|
|
rvdw = {params['cutoff']} |
|
|
|
|
|
; Temperature coupling |
|
|
tcoupl = {params['couplingType']} |
|
|
tc-grps = Protein Non-Protein |
|
|
tau_t = 0.1 0.1 |
|
|
ref_t = {params['steps']['nvt']['temperature']} {params['steps']['nvt']['temperature']} |
|
|
|
|
|
; Pressure coupling (disabled for NVT) |
|
|
pcoupl = no |
|
|
|
|
|
; Velocity generation |
|
|
gen_vel = yes |
|
|
gen_temp = {params['steps']['nvt']['temperature']} |
|
|
gen_seed = -1 |
|
|
""" |
|
|
|
|
|
elif step_type == 'npt': |
|
|
return f"""; NPT Equilibration Parameters |
|
|
integrator = md |
|
|
dt = {params['timestep']} |
|
|
nsteps = {params['steps']['npt']['steps']} |
|
|
|
|
|
; Output control |
|
|
nstxout = 5000 |
|
|
nstvout = 5000 |
|
|
nstenergy = 1000 |
|
|
nstlog = 1000 |
|
|
|
|
|
; Bond parameters |
|
|
constraint_algorithm = lincs |
|
|
constraints = h-bonds |
|
|
lincs_iter = 1 |
|
|
lincs_order = 4 |
|
|
|
|
|
; Neighbor searching |
|
|
cutoff-scheme = Verlet |
|
|
ns_type = grid |
|
|
nstlist = 40 |
|
|
rlist = {params['cutoff']} |
|
|
|
|
|
; Electrostatics |
|
|
coulombtype = PME |
|
|
rcoulomb = {params['cutoff']} |
|
|
pme_order = {params['pmeOrder']} |
|
|
|
|
|
; Van der Waals |
|
|
vdwtype = Cut-off |
|
|
rvdw = {params['cutoff']} |
|
|
|
|
|
; Temperature coupling |
|
|
tcoupl = {params['couplingType']} |
|
|
tc-grps = Protein Non-Protein |
|
|
tau_t = 0.1 0.1 |
|
|
ref_t = {params['steps']['npt']['temperature']} {params['steps']['npt']['temperature']} |
|
|
|
|
|
; Pressure coupling |
|
|
pcoupl = {params['couplingType']} |
|
|
pcoupltype = isotropic |
|
|
tau_p = 2.0 |
|
|
ref_p = {params['steps']['npt']['pressure']} |
|
|
compressibility = 4.5e-5 |
|
|
|
|
|
; Velocity generation |
|
|
gen_vel = no |
|
|
""" |
|
|
|
|
|
else: |
|
|
return f"""; MD Simulation Parameters |
|
|
; Generated by MD Simulation Pipeline |
|
|
|
|
|
; Run parameters |
|
|
integrator = md |
|
|
dt = {params['timestep']} |
|
|
nsteps = {params['steps']['production']['steps']} |
|
|
|
|
|
; Output control |
|
|
nstxout = 5000 |
|
|
nstvout = 5000 |
|
|
nstenergy = 1000 |
|
|
nstlog = 1000 |
|
|
|
|
|
; Bond parameters |
|
|
constraint_algorithm = lincs |
|
|
constraints = h-bonds |
|
|
lincs_iter = 1 |
|
|
lincs_order = 4 |
|
|
|
|
|
; Neighbor searching |
|
|
cutoff-scheme = Verlet |
|
|
ns_type = grid |
|
|
nstlist = 40 |
|
|
rlist = {params['cutoff']} |
|
|
|
|
|
; Electrostatics |
|
|
coulombtype = PME |
|
|
rcoulomb = {params['cutoff']} |
|
|
pme_order = {params['pmeOrder']} |
|
|
fourierspacing = 0.16 |
|
|
|
|
|
; Van der Waals |
|
|
vdwtype = Cut-off |
|
|
rvdw = {params['cutoff']} |
|
|
|
|
|
; Temperature coupling |
|
|
tcoupl = {params['couplingType']} |
|
|
tc-grps = Protein Non-Protein |
|
|
tau_t = 0.1 0.1 |
|
|
ref_t = {params['temperature']} {params['temperature']} |
|
|
|
|
|
; Pressure coupling |
|
|
pcoupl = {params['couplingType']} |
|
|
pcoupltype = isotropic |
|
|
tau_p = 2.0 |
|
|
ref_p = {params['pressure']} |
|
|
compressibility = 4.5e-5 |
|
|
|
|
|
; Dispersion correction |
|
|
DispCorr = EnerPres |
|
|
|
|
|
; Velocity generation |
|
|
gen_vel = yes |
|
|
gen_temp = {params['temperature']} |
|
|
gen_seed = -1 |
|
|
""" |
|
|
|
|
|
def generate_pbs_script(self, protein_name, params): |
|
|
"""Generate PBS script for HPC submission""" |
|
|
total_steps = params['steps']['production']['steps'] |
|
|
time_in_ns = (total_steps * params['timestep']) / 1000 |
|
|
|
|
|
return f"""#!/bin/bash |
|
|
#PBS -N {protein_name}_md |
|
|
#PBS -l nodes=1:ppn=16 |
|
|
#PBS -l walltime=24:00:00 |
|
|
#PBS -q normal |
|
|
#PBS -j oe |
|
|
|
|
|
# Change to the directory where the job was submitted |
|
|
cd $PBS_O_WORKDIR |
|
|
|
|
|
# Load required modules |
|
|
module load gromacs/2023.2 |
|
|
module load intel/2021.4.0 |
|
|
|
|
|
# Set up environment |
|
|
export OMP_NUM_THREADS=16 |
|
|
export GMX_MAXBACKUP=-1 |
|
|
|
|
|
# Simulation parameters |
|
|
PROTEIN={protein_name} |
|
|
STEPS={total_steps} |
|
|
TIME_NS={time_in_ns:.2f} |
|
|
|
|
|
echo "Starting MD simulation for $PROTEIN" |
|
|
echo "Total simulation time: $TIME_NS ns" |
|
|
echo "Job started at: $(date)" |
|
|
|
|
|
# Run the simulation |
|
|
./run_simulation.sh $PROTEIN |
|
|
|
|
|
echo "Simulation completed at: $(date)" |
|
|
echo "Results saved in output directory" |
|
|
""" |
|
|
|
|
|
def generate_setup_script(self, protein_name, params): |
|
|
"""Generate setup script for MD simulation""" |
|
|
return f"""#!/bin/bash |
|
|
# Setup script for {protein_name} MD simulation |
|
|
# Generated by MD Simulation Pipeline |
|
|
|
|
|
set -e |
|
|
|
|
|
PROTEIN={protein_name} |
|
|
FORCE_FIELD={params['forceField']} |
|
|
WATER_MODEL={params['waterModel']} |
|
|
|
|
|
echo "Setting up MD simulation for $PROTEIN" |
|
|
|
|
|
# Create output directory |
|
|
mkdir -p output |
|
|
|
|
|
# 1. Prepare protein structure |
|
|
echo "Preparing protein structure..." |
|
|
gmx pdb2gmx -f $PROTEIN.pdb -o $PROTEIN_processed.gro -p $PROTEIN.top -ff $FORCE_FIELD -water $WATER_MODEL |
|
|
|
|
|
# 2. Define simulation box |
|
|
echo "Defining simulation box..." |
|
|
gmx editconf -f $PROTEIN_processed.gro -o $PROTEIN_box.gro -c -d {params['boxMargin']} -bt {params['boxType']} |
|
|
|
|
|
# 3. Add solvent |
|
|
echo "Adding solvent..." |
|
|
gmx solvate -cp $PROTEIN_box.gro -cs spc216.gro -o $PROTEIN_solv.gro -p $PROTEIN.top |
|
|
|
|
|
# 4. Add ions |
|
|
echo "Adding ions..." |
|
|
gmx grompp -f $PROTEIN_restrained.mdp -c $PROTEIN_solv.gro -p $PROTEIN.top -o $PROTEIN_ions.tpr |
|
|
echo "SOL" | gmx genion -s $PROTEIN_ions.tpr -o $PROTEIN_final.gro -p $PROTEIN.top -pname NA -nname CL -neutral |
|
|
|
|
|
echo "Setup completed successfully!" |
|
|
echo "Ready to run simulation with: ./run_simulation.sh $PROTEIN" |
|
|
""" |
|
|
|
|
|
def generate_analysis_script(self, protein_name): |
|
|
"""Generate analysis script for MD simulation results""" |
|
|
return f"""#!/bin/bash |
|
|
# Analysis script for {protein_name} MD simulation |
|
|
# Generated by MD Simulation Pipeline |
|
|
|
|
|
PROTEIN={protein_name} |
|
|
|
|
|
echo "Analyzing MD simulation results for $PROTEIN" |
|
|
|
|
|
# Create analysis directory |
|
|
mkdir -p analysis |
|
|
|
|
|
# 1. RMSD analysis |
|
|
echo "Calculating RMSD..." |
|
|
echo "Protein" | gmx rms -s $PROTEIN_final.tpr -f $PROTEIN_prod.xtc -o analysis/$PROTEIN_rmsd.xvg -tu ns |
|
|
|
|
|
# 2. RMSF analysis |
|
|
echo "Calculating RMSF..." |
|
|
echo "Protein" | gmx rmsf -s $PROTEIN_final.tpr -f $PROTEIN_prod.xtc -o analysis/$PROTEIN_rmsf.xvg -res |
|
|
|
|
|
# 3. Radius of gyration |
|
|
echo "Calculating radius of gyration..." |
|
|
echo "Protein" | gmx gyrate -s $PROTEIN_final.tpr -f $PROTEIN_prod.xtc -o analysis/$PROTEIN_gyrate.xvg |
|
|
|
|
|
# 4. Hydrogen bonds |
|
|
echo "Analyzing hydrogen bonds..." |
|
|
echo "Protein" | gmx hbond -s $PROTEIN_final.tpr -f $PROTEIN_prod.xtc -num analysis/$PROTEIN_hbonds.xvg |
|
|
|
|
|
# 5. Energy analysis |
|
|
echo "Analyzing energies..." |
|
|
gmx energy -f $PROTEIN_prod.edr -o analysis/$PROTEIN_energy.xvg |
|
|
|
|
|
# 6. Generate plots |
|
|
echo "Generating analysis plots..." |
|
|
python3 plot_analysis.py $PROTEIN |
|
|
|
|
|
echo "Analysis completed! Results saved in analysis/ directory" |
|
|
""" |
|
|
|
|
|
|
|
|
md_generator = MDSimulationGenerator() |
|
|
|
|
|
@app.route('/api/fetch-pdb', methods=['POST']) |
|
|
def fetch_pdb(): |
|
|
"""Fetch PDB structure from RCSB""" |
|
|
try: |
|
|
print("DEBUG: fetch-pdb endpoint called") |
|
|
data = request.get_json() |
|
|
pdb_id = data.get('pdb_id', '').upper() |
|
|
print(f"DEBUG: pdb_id = {pdb_id}") |
|
|
|
|
|
if not pdb_id or len(pdb_id) != 4: |
|
|
return jsonify({'error': 'Invalid PDB ID'}), 400 |
|
|
|
|
|
|
|
|
print("DEBUG: Calling clean_and_create_output_folder()") |
|
|
if not clean_and_create_output_folder(): |
|
|
return jsonify({'error': 'Failed to clean output folder'}), 500 |
|
|
print("DEBUG: Output folder cleanup completed successfully") |
|
|
|
|
|
|
|
|
pdb_file = md_generator.fetch_pdb_structure(pdb_id) |
|
|
|
|
|
|
|
|
structure_info = md_generator.parse_pdb_structure(pdb_file) |
|
|
|
|
|
return jsonify({ |
|
|
'success': True, |
|
|
'structure_info': structure_info, |
|
|
'pdb_file': pdb_file |
|
|
}) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error fetching PDB: {str(e)}") |
|
|
return jsonify({'error': str(e)}), 500 |
|
|
|
|
|
@app.route('/api/parse-pdb', methods=['POST']) |
|
|
def parse_pdb(): |
|
|
"""Parse uploaded PDB file""" |
|
|
try: |
|
|
print("DEBUG: parse-pdb endpoint called") |
|
|
if 'file' not in request.files: |
|
|
return jsonify({'error': 'No file uploaded'}), 400 |
|
|
|
|
|
file = request.files['file'] |
|
|
if file.filename == '': |
|
|
return jsonify({'error': 'No file selected'}), 400 |
|
|
|
|
|
print(f"DEBUG: Processing uploaded file: {file.filename}") |
|
|
|
|
|
|
|
|
print("DEBUG: Calling clean_and_create_output_folder()") |
|
|
if not clean_and_create_output_folder(): |
|
|
return jsonify({'error': 'Failed to clean output folder'}), 500 |
|
|
print("DEBUG: Output folder cleanup completed successfully") |
|
|
|
|
|
|
|
|
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.pdb') |
|
|
file.save(temp_file.name) |
|
|
|
|
|
|
|
|
structure_info = md_generator.parse_pdb_structure(temp_file.name) |
|
|
|
|
|
|
|
|
os.unlink(temp_file.name) |
|
|
|
|
|
return jsonify({ |
|
|
'success': True, |
|
|
'structure_info': structure_info |
|
|
}) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error parsing PDB: {str(e)}") |
|
|
return jsonify({'error': str(e)}), 500 |
|
|
|
|
|
@app.route('/api/generate-files', methods=['POST']) |
|
|
def generate_files(): |
|
|
"""Generate MD simulation files""" |
|
|
try: |
|
|
data = request.get_json() |
|
|
protein_name = data.get('protein_name', 'protein') |
|
|
simulation_params = data.get('simulation_params', {}) |
|
|
|
|
|
|
|
|
files = {} |
|
|
|
|
|
|
|
|
files[f'{protein_name}.mdp'] = md_generator.generate_mdp_file(simulation_params, 'production') |
|
|
files[f'{protein_name}_restrained.mdp'] = md_generator.generate_mdp_file(simulation_params, 'restrained_min') |
|
|
files[f'{protein_name}_min.mdp'] = md_generator.generate_mdp_file(simulation_params, 'minimization') |
|
|
files[f'{protein_name}_nvt.mdp'] = md_generator.generate_mdp_file(simulation_params, 'nvt') |
|
|
files[f'{protein_name}_npt.mdp'] = md_generator.generate_mdp_file(simulation_params, 'npt') |
|
|
files[f'{protein_name}_prod.mdp'] = md_generator.generate_mdp_file(simulation_params, 'production') |
|
|
|
|
|
|
|
|
files[f'{protein_name}_simulation.pbs'] = md_generator.generate_pbs_script(protein_name, simulation_params) |
|
|
files[f'setup_{protein_name}.sh'] = md_generator.generate_setup_script(protein_name, simulation_params) |
|
|
files[f'analyze_{protein_name}.sh'] = md_generator.generate_analysis_script(protein_name) |
|
|
|
|
|
return jsonify({ |
|
|
'success': True, |
|
|
'files': files |
|
|
}) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error generating files: {str(e)}") |
|
|
return jsonify({'error': str(e)}), 500 |
|
|
|
|
|
@app.route('/api/download-zip', methods=['POST']) |
|
|
def download_zip(): |
|
|
"""Download all generated files as a ZIP archive""" |
|
|
try: |
|
|
data = request.get_json() |
|
|
files = data.get('files', {}) |
|
|
|
|
|
|
|
|
temp_zip = tempfile.NamedTemporaryFile(delete=False, suffix='.zip') |
|
|
|
|
|
with zipfile.ZipFile(temp_zip.name, 'w') as zip_file: |
|
|
for filename, content in files.items(): |
|
|
zip_file.writestr(filename, content) |
|
|
|
|
|
return send_file( |
|
|
temp_zip.name, |
|
|
as_attachment=True, |
|
|
download_name='md_simulation_files.zip', |
|
|
mimetype='application/zip' |
|
|
) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error creating ZIP file: {str(e)}") |
|
|
return jsonify({'error': str(e)}), 500 |
|
|
|
|
|
@app.route('/api/get-solvated-protein', methods=['GET']) |
|
|
def get_solvated_protein(): |
|
|
"""Get the solvated protein PDB file content""" |
|
|
try: |
|
|
solvated_file = os.path.join(OUTPUT_DIR, 'protein_solvated.pdb') |
|
|
|
|
|
if not os.path.exists(solvated_file): |
|
|
return jsonify({'success': False, 'error': 'Solvated protein file not found. Please generate files first.'}) |
|
|
|
|
|
with open(solvated_file, 'r') as f: |
|
|
content = f.read() |
|
|
|
|
|
return jsonify({'success': True, 'content': content}) |
|
|
except Exception as e: |
|
|
logger.error(f"Error reading solvated protein file: {str(e)}") |
|
|
return jsonify({'success': False, 'error': str(e)}) |
|
|
|
|
|
@app.route('/api/get-viewer-pdb', methods=['GET']) |
|
|
def get_viewer_pdb(): |
|
|
"""Return a single PDB for viewer: start from protein_solvated.pdb and mark ligand residues as HETATM. |
|
|
Ligand residues are detected from 4_ligands_corrected.pdb by (resname, chain, resi) tuples; if chains/resi not present, fallback to resname matching. |
|
|
""" |
|
|
try: |
|
|
solvated_path = OUTPUT_DIR / 'protein_solvated.pdb' |
|
|
lig_path = OUTPUT_DIR / '4_ligands_corrected.pdb' |
|
|
viewer_out = OUTPUT_DIR / 'viewer_protein_with_ligand.pdb' |
|
|
|
|
|
if not solvated_path.exists(): |
|
|
return jsonify({'success': False, 'error': 'protein_solvated.pdb not found'}), 400 |
|
|
|
|
|
|
|
|
ligand_keys = set() |
|
|
ligand_resnames = set() |
|
|
if lig_path.exists(): |
|
|
with open(lig_path, 'r') as lf: |
|
|
for line in lf: |
|
|
if line.startswith(('ATOM', 'HETATM')): |
|
|
resn = line[17:20].strip() |
|
|
chain = line[21:22].strip() |
|
|
resi = line[22:26].strip() |
|
|
ligand_resnames.add(resn) |
|
|
if chain and resi: |
|
|
ligand_keys.add((resn, chain, resi)) |
|
|
|
|
|
|
|
|
out_lines = [] |
|
|
with open(solvated_path, 'r') as sf: |
|
|
for line in sf: |
|
|
if line.startswith(('ATOM', 'HETATM')): |
|
|
resn = line[17:20].strip() |
|
|
chain = line[21:22].strip() |
|
|
resi = line[22:26].strip() |
|
|
is_match = False |
|
|
is_ion = resn in { 'NA', 'CL' } |
|
|
if (resn, chain, resi) in ligand_keys: |
|
|
is_match = True |
|
|
elif resn in ligand_resnames: |
|
|
|
|
|
is_match = True |
|
|
if is_match or is_ion: |
|
|
|
|
|
out_lines.append('HETATM' + line[6:]) |
|
|
else: |
|
|
out_lines.append(line) |
|
|
else: |
|
|
out_lines.append(line) |
|
|
|
|
|
|
|
|
try: |
|
|
with open(viewer_out, 'w') as vf: |
|
|
vf.writelines(out_lines) |
|
|
except Exception: |
|
|
pass |
|
|
|
|
|
return jsonify({'success': True, 'content': ''.join(out_lines)}) |
|
|
except Exception as e: |
|
|
logger.error(f"Error generating viewer PDB: {str(e)}") |
|
|
return jsonify({'success': False, 'error': str(e)}) |
|
|
|
|
|
@app.route('/api/get-corrected-ligands', methods=['GET']) |
|
|
def get_corrected_ligands(): |
|
|
"""Get the corrected ligand PDB file content if present""" |
|
|
try: |
|
|
ligand_file = OUTPUT_DIR / '4_ligands_corrected.pdb' |
|
|
if not ligand_file.exists(): |
|
|
|
|
|
return jsonify({'success': True, 'exists': False, 'content': ''}) |
|
|
|
|
|
normalized_lines = [] |
|
|
with open(ligand_file, 'r') as f: |
|
|
for line in f: |
|
|
if line.startswith('ATOM'): |
|
|
|
|
|
normalized_lines.append('HETATM' + line[6:]) |
|
|
else: |
|
|
normalized_lines.append(line) |
|
|
content = ''.join(normalized_lines) |
|
|
return jsonify({'success': True, 'exists': True, 'content': content}) |
|
|
except Exception as e: |
|
|
logger.error(f"Error reading corrected ligand file: {str(e)}") |
|
|
return jsonify({'success': False, 'error': str(e)}) |
|
|
|
|
|
@app.route('/api/get-aligned-ligands', methods=['GET']) |
|
|
def get_aligned_ligands(): |
|
|
"""Return ligand coordinates aligned to protein_solvated.pdb frame using PyMOL transforms.""" |
|
|
try: |
|
|
solvated_file = OUTPUT_DIR / 'protein_solvated.pdb' |
|
|
tleap_ready = OUTPUT_DIR / 'tleap_ready.pdb' |
|
|
ligand_file = OUTPUT_DIR / '4_ligands_corrected.pdb' |
|
|
|
|
|
if not solvated_file.exists(): |
|
|
return jsonify({'success': False, 'error': 'protein_solvated.pdb not found'}), 400 |
|
|
if not tleap_ready.exists(): |
|
|
return jsonify({'success': False, 'error': 'tleap_ready.pdb not found'}), 400 |
|
|
if not ligand_file.exists(): |
|
|
return jsonify({'success': True, 'exists': False, 'content': ''}) |
|
|
|
|
|
|
|
|
aligned_lig = OUTPUT_DIR / 'ligand_aligned_for_preview.pdb' |
|
|
try: |
|
|
if aligned_lig.exists(): |
|
|
aligned_lig.unlink() |
|
|
except Exception: |
|
|
pass |
|
|
|
|
|
|
|
|
pymol_script = f""" |
|
|
import pymol |
|
|
pymol.finish_launching(['pymol','-qc']) |
|
|
from pymol import cmd |
|
|
cmd.load('{solvated_file.as_posix()}', 'solv') |
|
|
cmd.load('{tleap_ready.as_posix()}', 'prep') |
|
|
cmd.load('{ligand_file.as_posix()}', 'lig') |
|
|
# Align prepared protein to solvated protein; use CA atoms to be robust |
|
|
cmd.align('prep and polymer.protein and name CA', 'solv and polymer.protein and name CA') |
|
|
# Apply same transform implicitly affects 'prep' object; we saved ligand as separate object, so match matrices |
|
|
mat = cmd.get_object_matrix('prep') |
|
|
cmd.set_object_matrix('lig', mat) |
|
|
# Save ligand in aligned frame, as HETATM |
|
|
cmd.alter('lig', 'type="HETATM"') |
|
|
cmd.save('{aligned_lig.as_posix()}', 'lig') |
|
|
cmd.quit() |
|
|
""" |
|
|
|
|
|
|
|
|
result = subprocess.run(['python3', '-c', pymol_script], capture_output=True, text=True, cwd=str(OUTPUT_DIR)) |
|
|
if result.returncode != 0: |
|
|
return jsonify({'success': False, 'error': f'PyMOL alignment failed: {result.stderr}'}), 500 |
|
|
|
|
|
if not aligned_lig.exists(): |
|
|
return jsonify({'success': False, 'error': 'Aligned ligand file was not produced'}), 500 |
|
|
|
|
|
|
|
|
normalized_lines = [] |
|
|
with open(aligned_lig, 'r') as f: |
|
|
for line in f: |
|
|
if line.startswith('ATOM'): |
|
|
normalized_lines.append('HETATM' + line[6:]) |
|
|
else: |
|
|
normalized_lines.append(line) |
|
|
content = ''.join(normalized_lines) |
|
|
return jsonify({'success': True, 'exists': True, 'content': content}) |
|
|
except Exception as e: |
|
|
logger.error(f"Error aligning ligands: {str(e)}") |
|
|
return jsonify({'success': False, 'error': str(e)}), 500 |
|
|
|
|
|
@app.route('/viewer/<filename>') |
|
|
def viewer(filename): |
|
|
"""Serve NGL viewer page""" |
|
|
|
|
|
file_path = OUTPUT_DIR / filename |
|
|
if not file_path.exists(): |
|
|
|
|
|
if filename == 'viewer_protein_with_ligand.pdb': |
|
|
try: |
|
|
|
|
|
result = get_viewer_pdb() |
|
|
if result[1] == 200: |
|
|
pass |
|
|
except: |
|
|
pass |
|
|
|
|
|
return f""" |
|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<title>NGL Viewer - {filename}</title> |
|
|
<script src="https://cdn.jsdelivr.net/npm/ngl@2.0.0-dev.37/dist/ngl.js"></script> |
|
|
<style> |
|
|
body {{ margin: 0; padding: 0; font-family: Arial, sans-serif; }} |
|
|
#viewport {{ width: 100%; height: 100vh; }} |
|
|
.header {{ background: #f8f9fa; padding: 10px; border-bottom: 1px solid #ddd; }} |
|
|
.controls {{ padding: 10px; background: #f8f9fa; }} |
|
|
.btn {{ padding: 8px 16px; margin: 5px; border: none; border-radius: 4px; cursor: pointer; }} |
|
|
.btn-primary {{ background: #007bff; color: white; }} |
|
|
.btn-secondary {{ background: #6c757d; color: white; }} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="header"> |
|
|
<h3>🧬 3D Structure Viewer - {filename}</h3> |
|
|
</div> |
|
|
<div id="viewport"></div> |
|
|
<div class="controls"> |
|
|
<button class="btn btn-primary" onclick="resetView()">Reset View</button> |
|
|
<button class="btn btn-secondary" onclick="toggleRepresentation()">Toggle Style</button> |
|
|
<button class="btn btn-secondary" onclick="toggleSpin()">Toggle Spin</button> |
|
|
</div> |
|
|
<script> |
|
|
let stage; |
|
|
let currentRepresentation = 'cartoon'; |
|
|
let isSpinning = false; |
|
|
|
|
|
async function initViewer() {{ |
|
|
try {{ |
|
|
// Check if file exists first |
|
|
const response = await fetch("/output/{filename}"); |
|
|
if (!response.ok) {{ |
|
|
throw new Error(`File not found: ${{response.status}} ${{response.statusText}}`); |
|
|
}} |
|
|
|
|
|
stage = new NGL.Stage("viewport", {{ backgroundColor: "white" }}); |
|
|
|
|
|
const component = await stage.loadFile("/output/{filename}"); |
|
|
|
|
|
// Add cartoon representation for protein |
|
|
component.addRepresentation("cartoon", {{ |
|
|
sele: "protein", |
|
|
colorScheme: "chainname", |
|
|
opacity: 0.9 |
|
|
}}); |
|
|
|
|
|
// Add ball and stick for water molecules |
|
|
component.addRepresentation("ball+stick", {{ |
|
|
sele: "water", |
|
|
color: "cyan", |
|
|
colorScheme: "uniform", |
|
|
radius: 0.1 |
|
|
}}); |
|
|
|
|
|
// Add ball and stick for ligands |
|
|
component.addRepresentation("ball+stick", {{ |
|
|
sele: "hetero", |
|
|
color: "element", |
|
|
radius: 0.15 |
|
|
}}); |
|
|
|
|
|
stage.autoView(); |
|
|
}} catch (error) {{ |
|
|
console.error('Error loading structure:', error); |
|
|
document.getElementById('viewport').innerHTML = |
|
|
'<div style="padding: 50px; text-align: center; color: #dc3545;">' + |
|
|
'<h3>Error loading structure</h3><p>' + error.message + '</p>' + |
|
|
'<p>Make sure the file exists in the output directory.</p></div>'; |
|
|
}} |
|
|
}} |
|
|
|
|
|
function resetView() {{ |
|
|
if (stage) stage.autoView(); |
|
|
}} |
|
|
|
|
|
function toggleRepresentation() {{ |
|
|
if (!stage) return; |
|
|
const components = stage.compList; |
|
|
if (components.length === 0) return; |
|
|
|
|
|
const component = components[0]; |
|
|
component.removeAllRepresentations(); |
|
|
|
|
|
if (currentRepresentation === 'cartoon') {{ |
|
|
component.addRepresentation("ball+stick", {{ |
|
|
color: "element", |
|
|
radius: 0.15 |
|
|
}}); |
|
|
currentRepresentation = 'ball+stick'; |
|
|
}} else {{ |
|
|
component.addRepresentation("cartoon", {{ |
|
|
sele: "protein", |
|
|
colorScheme: "chainname", |
|
|
opacity: 0.9 |
|
|
}}); |
|
|
component.addRepresentation("ball+stick", {{ |
|
|
sele: "water", |
|
|
color: "cyan", |
|
|
colorScheme: "uniform", |
|
|
radius: 0.1 |
|
|
}}); |
|
|
component.addRepresentation("ball+stick", {{ |
|
|
sele: "hetero", |
|
|
color: "element", |
|
|
radius: 0.15 |
|
|
}}); |
|
|
currentRepresentation = 'cartoon'; |
|
|
}} |
|
|
}} |
|
|
|
|
|
function toggleSpin() {{ |
|
|
if (!stage) return; |
|
|
isSpinning = !isSpinning; |
|
|
stage.setSpin(isSpinning); |
|
|
}} |
|
|
|
|
|
// Initialize when page loads |
|
|
document.addEventListener('DOMContentLoaded', initViewer); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
@app.route('/output/<path:filename>') |
|
|
def serve_output(filename): |
|
|
"""Serve output files""" |
|
|
|
|
|
print(f"Requested file: {filename}") |
|
|
print(f"Full path: {OUTPUT_DIR / filename}") |
|
|
print(f"File exists: {(OUTPUT_DIR / filename).exists()}") |
|
|
print(f"Files in output dir: {list(OUTPUT_DIR.iterdir()) if OUTPUT_DIR.exists() else 'Directory not found'}") |
|
|
|
|
|
if not (OUTPUT_DIR / filename).exists(): |
|
|
abort(404) |
|
|
|
|
|
return send_from_directory(OUTPUT_DIR, filename) |
|
|
|
|
|
@app.route('/') |
|
|
def index(): |
|
|
"""Serve the main HTML page""" |
|
|
return render_template('index.html') |
|
|
|
|
|
@app.route('/<path:filename>') |
|
|
def serve_static(filename): |
|
|
"""Serve static files (CSS, JS, etc.)""" |
|
|
return send_from_directory('../', filename) |
|
|
|
|
|
@app.route('/api/prepare-structure', methods=['POST']) |
|
|
def prepare_structure_endpoint(): |
|
|
"""Prepare protein structure for AMBER""" |
|
|
try: |
|
|
data = request.get_json() |
|
|
pdb_content = data.get('pdb_content', '') |
|
|
options = data.get('options', {}) |
|
|
|
|
|
if not pdb_content: |
|
|
return jsonify({'error': 'No PDB content provided'}), 400 |
|
|
|
|
|
|
|
|
result = prepare_structure(pdb_content, options) |
|
|
|
|
|
return jsonify({ |
|
|
'success': True, |
|
|
'prepared_structure': result['prepared_structure'], |
|
|
'original_atoms': result['original_atoms'], |
|
|
'prepared_atoms': result['prepared_atoms'], |
|
|
'removed_components': result['removed_components'], |
|
|
'added_capping': result['added_capping'], |
|
|
'preserved_ligands': result['preserved_ligands'], |
|
|
'ligand_present': result.get('ligand_present', False), |
|
|
'separate_ligands': result.get('separate_ligands', False), |
|
|
'ligand_content': result.get('ligand_content', '') |
|
|
}) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error preparing structure: {str(e)}") |
|
|
return jsonify({'error': str(e)}), 500 |
|
|
|
|
|
@app.route('/api/parse-structure', methods=['POST']) |
|
|
def parse_structure_endpoint(): |
|
|
"""Parse structure information""" |
|
|
try: |
|
|
data = request.get_json() |
|
|
pdb_content = data.get('pdb_content', '') |
|
|
|
|
|
if not pdb_content: |
|
|
return jsonify({'error': 'No PDB content provided'}), 400 |
|
|
|
|
|
|
|
|
structure_info = parse_structure_info(pdb_content) |
|
|
|
|
|
return jsonify({ |
|
|
'success': True, |
|
|
'structure_info': structure_info |
|
|
}) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error parsing structure: {str(e)}") |
|
|
return jsonify({'error': str(e)}), 500 |
|
|
|
|
|
@app.route('/api/generate-ligand-ff', methods=['POST']) |
|
|
def generate_ligand_ff(): |
|
|
"""Generate force field parameters for ligand""" |
|
|
try: |
|
|
data = request.get_json() |
|
|
force_field = data.get('force_field', 'gaff2') |
|
|
|
|
|
|
|
|
s_param = 2 if force_field == 'gaff2' else 1 |
|
|
|
|
|
|
|
|
ligand_pdb = OUTPUT_DIR / "4_ligands_corrected.pdb" |
|
|
ligand_mol2 = OUTPUT_DIR / "4_ligands_corrected.mol2" |
|
|
ligand_frcmod = OUTPUT_DIR / "4_ligands_corrected.frcmod" |
|
|
|
|
|
print(f"Working directory: {os.getcwd()}") |
|
|
print(f"Output directory: {OUTPUT_DIR}") |
|
|
print(f"Ligand PDB path: {ligand_pdb}") |
|
|
print(f"Ligand MOL2 path: {ligand_mol2}") |
|
|
print(f"Ligand FRCMOD path: {ligand_frcmod}") |
|
|
|
|
|
if not ligand_pdb.exists(): |
|
|
return jsonify({'error': 'Ligand PDB file not found. Please prepare structure with ligands first.'}), 400 |
|
|
|
|
|
import re |
|
|
|
|
|
|
|
|
print("Step 1: Calculating net charge from PDB file...") |
|
|
|
|
|
awk_cmd = "awk '/^HETATM/ {if($NF ~ /[A-Z][0-9]-$/) charge--; if($NF ~ /[A-Z][0-9]\\+$/) charge++} END {print \"Net charge:\", charge+0}'" |
|
|
cmd1 = f"{awk_cmd} {ligand_pdb}" |
|
|
|
|
|
try: |
|
|
|
|
|
result = subprocess.run(cmd1, shell=True, capture_output=True, text=True) |
|
|
output = result.stdout.strip() |
|
|
print(f"Awk output: '{output}'") |
|
|
print(f"Awk stderr: '{result.stderr}'") |
|
|
|
|
|
|
|
|
net_charge_match = re.search(r'Net charge:\s*(-?\d+)', output) |
|
|
if net_charge_match: |
|
|
net_charge = int(net_charge_match.group(1)) |
|
|
print(f"Calculated net charge: {net_charge}") |
|
|
else: |
|
|
print("Could not extract net charge from awk output, using 0") |
|
|
net_charge = 0 |
|
|
except Exception as e: |
|
|
print(f"Error running awk command: {e}, using net charge 0") |
|
|
net_charge = 0 |
|
|
|
|
|
|
|
|
print(f"Step 2: Running antechamber with net charge {net_charge}...") |
|
|
|
|
|
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}" |
|
|
print(f"Running command: {cmd2}") |
|
|
result2 = subprocess.run(cmd2, shell=True, cwd=str(OUTPUT_DIR), capture_output=True, text=True) |
|
|
|
|
|
print(f"antechamber return code: {result2.returncode}") |
|
|
print(f"antechamber stdout: {result2.stdout}") |
|
|
print(f"antechamber stderr: {result2.stderr}") |
|
|
|
|
|
if result2.returncode != 0: |
|
|
return jsonify({'error': f'antechamber failed with net charge {net_charge}. Error: {result2.stderr}'}), 500 |
|
|
|
|
|
|
|
|
print("Step 3: Running parmchk2...") |
|
|
|
|
|
cmd3 = f"parmchk2 -i 4_ligands_corrected.mol2 -f mol2 -o 4_ligands_corrected.frcmod -a Y -s {s_param}" |
|
|
print(f"Running command: {cmd3}") |
|
|
result3 = subprocess.run(cmd3, shell=True, cwd=str(OUTPUT_DIR), capture_output=True, text=True) |
|
|
|
|
|
print(f"parmchk2 return code: {result3.returncode}") |
|
|
print(f"parmchk2 stdout: {result3.stdout}") |
|
|
print(f"parmchk2 stderr: {result3.stderr}") |
|
|
|
|
|
if result3.returncode != 0: |
|
|
return jsonify({'error': f'parmchk2 failed to generate force field parameters. Error: {result3.stderr}'}), 500 |
|
|
|
|
|
|
|
|
print(f"After commands - MOL2 exists: {ligand_mol2.exists()}") |
|
|
print(f"After commands - FRCMOD exists: {ligand_frcmod.exists()}") |
|
|
print(f"Output directory contents: {list(OUTPUT_DIR.glob('*'))}") |
|
|
|
|
|
if not ligand_mol2.exists() or not ligand_frcmod.exists(): |
|
|
return jsonify({'error': 'Force field generation failed - output files not created'}), 500 |
|
|
|
|
|
return jsonify({ |
|
|
'success': True, |
|
|
'message': f'Ligand force field ({force_field}) generated successfully with net charge {net_charge}', |
|
|
'net_charge': net_charge, |
|
|
'files': { |
|
|
'mol2': str(ligand_mol2), |
|
|
'frcmod': str(ligand_frcmod) |
|
|
} |
|
|
}) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error generating ligand force field: {str(e)}") |
|
|
return jsonify({'error': f'Internal server error: {str(e)}'}), 500 |
|
|
|
|
|
@app.route('/api/calculate-net-charge', methods=['POST']) |
|
|
def calculate_net_charge(): |
|
|
"""Calculate net charge of the system using tleap""" |
|
|
try: |
|
|
|
|
|
tleap_ready_file = OUTPUT_DIR / "tleap_ready.pdb" |
|
|
if not tleap_ready_file.exists(): |
|
|
return jsonify({'error': 'Structure not prepared. Please prepare structure first.'}), 400 |
|
|
|
|
|
|
|
|
ligand_pdb = OUTPUT_DIR / "4_ligands_corrected.pdb" |
|
|
ligand_present = ligand_pdb.exists() |
|
|
|
|
|
|
|
|
tleap_input = OUTPUT_DIR / "calc_charge_on_system.in" |
|
|
|
|
|
|
|
|
data = request.get_json() if request.get_json() else {} |
|
|
selected_force_field = data.get('force_field', 'ff14SB') |
|
|
|
|
|
with open(tleap_input, 'w') as f: |
|
|
f.write(f"source leaprc.protein.{selected_force_field}\n") |
|
|
f.write("source leaprc.gaff2\n\n") |
|
|
|
|
|
if ligand_present: |
|
|
|
|
|
f.write("loadamberparams 4_ligands_corrected.frcmod\n\n") |
|
|
f.write("COB = loadmol2 4_ligands_corrected.mol2\n\n") |
|
|
|
|
|
f.write("x = loadpdb tleap_ready.pdb\n\n") |
|
|
f.write("charge x\n\n") |
|
|
|
|
|
|
|
|
print("Running tleap to calculate system charge...") |
|
|
|
|
|
try: |
|
|
|
|
|
which_result = subprocess.run(['which', 'tleap'], capture_output=True, text=True) |
|
|
if which_result.returncode == 0: |
|
|
tleap_path = which_result.stdout.strip() |
|
|
else: |
|
|
|
|
|
conda_env = os.environ.get('CONDA_DEFAULT_ENV', 'MD_pipeline') |
|
|
conda_prefix = os.environ.get('CONDA_PREFIX', '') |
|
|
if conda_prefix: |
|
|
tleap_path = os.path.join(conda_prefix, 'bin', 'tleap') |
|
|
else: |
|
|
|
|
|
tleap_path = 'tleap' |
|
|
|
|
|
cmd = f"{tleap_path} -f calc_charge_on_system.in" |
|
|
result = subprocess.run(cmd, shell=True, cwd=str(OUTPUT_DIR), capture_output=True, text=True) |
|
|
except Exception as e: |
|
|
|
|
|
cmd = f"tleap -f calc_charge_on_system.in" |
|
|
result = subprocess.run(cmd, shell=True, cwd=str(OUTPUT_DIR), capture_output=True, text=True) |
|
|
|
|
|
print(f"tleap return code: {result.returncode}") |
|
|
print(f"tleap stdout: {result.stdout}") |
|
|
print(f"tleap stderr: {result.stderr}") |
|
|
|
|
|
|
|
|
|
|
|
if 'Total unperturbed charge' not in result.stdout and 'Total charge' not in result.stdout: |
|
|
return jsonify({'error': f'tleap failed to calculate charge. Error: {result.stderr}'}), 500 |
|
|
|
|
|
|
|
|
output_lines = result.stdout.split('\n') |
|
|
net_charge = None |
|
|
|
|
|
for line in output_lines: |
|
|
if 'Total unperturbed charge' in line or 'Total charge' in line: |
|
|
|
|
|
import re |
|
|
charge_match = re.search(r'charge[:\s]+(-?\d+\.?\d*)', line) |
|
|
if charge_match: |
|
|
net_charge = float(charge_match.group(1)) |
|
|
break |
|
|
|
|
|
if net_charge is None: |
|
|
return jsonify({'error': 'Could not extract net charge from tleap output'}), 500 |
|
|
|
|
|
|
|
|
if net_charge > 0: |
|
|
suggestion = f"Add {int(net_charge)} Cl- ions to neutralize the system" |
|
|
ion_type = "Cl-" |
|
|
ion_count = int(net_charge) |
|
|
elif net_charge < 0: |
|
|
suggestion = f"Add {int(abs(net_charge))} Na+ ions to neutralize the system" |
|
|
ion_type = "Na+" |
|
|
ion_count = int(abs(net_charge)) |
|
|
else: |
|
|
suggestion = "System is already neutral, no ions needed" |
|
|
ion_type = "None" |
|
|
ion_count = 0 |
|
|
|
|
|
return jsonify({ |
|
|
'success': True, |
|
|
'net_charge': net_charge, |
|
|
'suggestion': suggestion, |
|
|
'ion_type': ion_type, |
|
|
'ion_count': ion_count, |
|
|
'ligand_present': ligand_present |
|
|
}) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error calculating net charge: {str(e)}") |
|
|
return jsonify({'error': f'Internal server error: {str(e)}'}), 500 |
|
|
|
|
|
@app.route('/api/generate-all-files', methods=['POST']) |
|
|
def generate_all_files(): |
|
|
"""Generate all simulation input files based on UI parameters""" |
|
|
try: |
|
|
data = request.get_json() |
|
|
|
|
|
|
|
|
cutoff_distance = data.get('cutoff_distance', 10.0) |
|
|
temperature = data.get('temperature', 310.0) |
|
|
pressure = data.get('pressure', 1.0) |
|
|
|
|
|
|
|
|
restrained_steps = data.get('restrained_steps', 10000) |
|
|
restrained_force = data.get('restrained_force', 10.0) |
|
|
min_steps = data.get('min_steps', 20000) |
|
|
npt_heating_steps = data.get('npt_heating_steps', 50000) |
|
|
npt_equilibration_steps = data.get('npt_equilibration_steps', 100000) |
|
|
production_steps = data.get('production_steps', 1000000) |
|
|
|
|
|
dt = data.get('timestep', 0.002) |
|
|
|
|
|
|
|
|
force_field = data.get('force_field', 'ff14SB') |
|
|
water_model = data.get('water_model', 'TIP3P') |
|
|
add_ions = data.get('add_ions', 'None') |
|
|
distance = data.get('distance', 10.0) |
|
|
|
|
|
|
|
|
warnings = [] |
|
|
if restrained_steps < 5000: |
|
|
warnings.append("Restrained minimization steps should be at least 5000") |
|
|
if min_steps < 10000: |
|
|
warnings.append("Minimization steps should be at least 10000") |
|
|
|
|
|
|
|
|
tleap_ready_file = OUTPUT_DIR / "tleap_ready.pdb" |
|
|
if not tleap_ready_file.exists(): |
|
|
return jsonify({'error': 'tleap_ready.pdb not found. Please prepare structure first.'}), 400 |
|
|
|
|
|
total_residues = count_residues_in_pdb(str(tleap_ready_file)) |
|
|
|
|
|
|
|
|
generate_min_restrained_file(restrained_steps, restrained_force, total_residues, cutoff_distance) |
|
|
|
|
|
|
|
|
generate_min_file(min_steps, cutoff_distance) |
|
|
|
|
|
|
|
|
generate_heat_npt_file(npt_heating_steps, temperature, pressure, cutoff_distance, dt) |
|
|
|
|
|
|
|
|
generate_npt_equilibration_file(npt_equilibration_steps, temperature, pressure, cutoff_distance, dt) |
|
|
|
|
|
|
|
|
generate_production_file(production_steps, temperature, pressure, cutoff_distance, dt) |
|
|
|
|
|
|
|
|
ff_files_generated = [] |
|
|
try: |
|
|
generate_ff_parameters_file(force_field, water_model, add_ions, distance) |
|
|
|
|
|
|
|
|
tleap_path = None |
|
|
try: |
|
|
result = subprocess.run(['which', 'tleap'], capture_output=True, text=True) |
|
|
if result.returncode == 0: |
|
|
tleap_path = result.stdout.strip() |
|
|
except: |
|
|
pass |
|
|
|
|
|
if not tleap_path: |
|
|
conda_prefix = os.environ.get('CONDA_PREFIX') |
|
|
if conda_prefix: |
|
|
tleap_path = os.path.join(conda_prefix, 'bin', 'tleap') |
|
|
else: |
|
|
tleap_path = '/home/hn533621/.conda/envs/MD_pipeline/bin/tleap' |
|
|
|
|
|
|
|
|
cmd = f"{tleap_path} -f generate_ff_parameters.in" |
|
|
result = subprocess.run(cmd, shell=True, cwd=str(OUTPUT_DIR), |
|
|
capture_output=True, text=True, timeout=300) |
|
|
|
|
|
if result.returncode != 0: |
|
|
warnings.append(f"Force field generation failed: {result.stderr}") |
|
|
else: |
|
|
|
|
|
ff_output_files = ['protein.prmtop', 'protein.inpcrd', 'protein_solvated.pdb'] |
|
|
for ff_file in ff_output_files: |
|
|
if (OUTPUT_DIR / ff_file).exists(): |
|
|
ff_files_generated.append(ff_file) |
|
|
|
|
|
if len(ff_files_generated) == 0: |
|
|
warnings.append("Force field parameter files were not generated") |
|
|
|
|
|
except Exception as ff_error: |
|
|
warnings.append(f"Force field generation error: {str(ff_error)}") |
|
|
|
|
|
|
|
|
pbs_generated = generate_submit_pbs_file() |
|
|
|
|
|
all_files = [ |
|
|
'min_restrained.in', |
|
|
'min.in', |
|
|
'HeatNPT.in', |
|
|
'mdin_equi.in', |
|
|
'mdin_prod.in' |
|
|
] + ff_files_generated |
|
|
|
|
|
if pbs_generated: |
|
|
all_files.append('submit_jobs.pbs') |
|
|
|
|
|
return jsonify({ |
|
|
'success': True, |
|
|
'message': f'All simulation files generated successfully ({len(all_files)} files)', |
|
|
'warnings': warnings, |
|
|
'files_generated': all_files |
|
|
}) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Error generating simulation files: {str(e)}") |
|
|
return jsonify({'error': f'Internal server error: {str(e)}'}), 500 |
|
|
|
|
|
def count_residues_in_pdb(pdb_file): |
|
|
"""Count total number of residues in PDB file""" |
|
|
try: |
|
|
with open(pdb_file, 'r') as f: |
|
|
lines = f.readlines() |
|
|
|
|
|
residues = set() |
|
|
for line in lines: |
|
|
if line.startswith(('ATOM', 'HETATM')): |
|
|
|
|
|
residue_num = line[22:26].strip() |
|
|
if residue_num: |
|
|
residues.add(residue_num) |
|
|
|
|
|
return len(residues) |
|
|
except Exception as e: |
|
|
logger.error(f"Error counting residues: {str(e)}") |
|
|
return 607 |
|
|
|
|
|
def generate_min_restrained_file(steps, force_constant, total_residues, cutoff): |
|
|
"""Generate min_restrained.in file""" |
|
|
content = f"""initial minimization solvent + ions |
|
|
&cntrl |
|
|
imin = 1, |
|
|
maxcyc = {steps}, |
|
|
ncyc = {steps // 2}, |
|
|
ntb = 1, |
|
|
ntr = 1, |
|
|
ntxo = 1, |
|
|
cut = {cutoff} |
|
|
/ |
|
|
Restrain |
|
|
{force_constant} |
|
|
RES 1 {total_residues} |
|
|
END |
|
|
END |
|
|
|
|
|
""" |
|
|
|
|
|
with open(OUTPUT_DIR / "min_restrained.in", 'w') as f: |
|
|
f.write(content) |
|
|
|
|
|
def generate_min_file(steps, cutoff): |
|
|
"""Generate min.in file""" |
|
|
content = f"""Minimization |
|
|
&cntrl |
|
|
imin=1, |
|
|
maxcyc={steps}, |
|
|
ncyc={steps // 4}, |
|
|
ntb=1, |
|
|
cut={cutoff}, |
|
|
igb=0, |
|
|
ntr=0, |
|
|
/ |
|
|
|
|
|
""" |
|
|
|
|
|
with open(OUTPUT_DIR / "min.in", 'w') as f: |
|
|
f.write(content) |
|
|
|
|
|
def generate_heat_npt_file(steps, temperature, pressure, cutoff, dt=0.002): |
|
|
"""Generate HeatNPT.in file with temperature ramping""" |
|
|
|
|
|
step1 = int(steps * 0.2) |
|
|
step2 = int(steps * 0.2) |
|
|
step3 = int(steps * 0.2) |
|
|
step4 = int(steps * 0.4) |
|
|
|
|
|
|
|
|
temp1 = temperature * 0.03 |
|
|
temp2 = temperature * 0.66 |
|
|
temp3 = temperature |
|
|
temp4 = temperature |
|
|
|
|
|
content = f"""Heat |
|
|
&cntrl |
|
|
imin = 0, irest = 0, ntx = 1, |
|
|
ntb = 2, pres0 = {pressure}, ntp = 1, |
|
|
taup = 2.0, |
|
|
cut = {cutoff}, ntr = 0, |
|
|
ntc = 2, ntf = 2, |
|
|
tempi = 0, temp0 = {temperature}, |
|
|
ntt = 3, gamma_ln = 1.0, |
|
|
nstlim = {steps}, dt = {dt}, |
|
|
ntpr = 2000, ntwx = 2000, ntwr = 2000 |
|
|
/ |
|
|
&wt type='TEMP0', istep1=0, istep2={step1}, value1=0.0, value2={temp1} / |
|
|
&wt type='TEMP0', istep1={step1+1}, istep2={step1+step2}, value1={temp1}, value2={temp2} / |
|
|
&wt type='TEMP0', istep1={step1+step2+1}, istep2={step1+step2+step3}, value1={temp2}, value2={temp3} / |
|
|
&wt type='TEMP0', istep1={step1+step2+step3+1}, istep2={steps}, value1={temp3}, value2={temp4} / |
|
|
&wt type='END' / |
|
|
|
|
|
""" |
|
|
|
|
|
with open(OUTPUT_DIR / "HeatNPT.in", 'w') as f: |
|
|
f.write(content) |
|
|
|
|
|
def generate_npt_equilibration_file(steps, temperature, pressure, cutoff, dt=0.002): |
|
|
"""Generate mdin_equi.in file for NPT equilibration""" |
|
|
content = f"""NPT Equilibration |
|
|
&cntrl |
|
|
imin=0, |
|
|
ntx=1, |
|
|
irest=0, |
|
|
pres0={pressure}, |
|
|
taup=1.0, |
|
|
temp0={temperature}, |
|
|
tempi={temperature}, |
|
|
nstlim={steps}, |
|
|
dt={dt}, |
|
|
ntf=2, |
|
|
ntc=2, |
|
|
ntpr=500, |
|
|
ntwx=500, |
|
|
ntwr=500, |
|
|
cut={cutoff}, |
|
|
ntb=2, |
|
|
ntp=1, |
|
|
ntt=3, |
|
|
gamma_ln=3.0, |
|
|
ig=-1, |
|
|
iwrap=1, |
|
|
ntr=0, |
|
|
/ |
|
|
|
|
|
""" |
|
|
|
|
|
with open(OUTPUT_DIR / "mdin_equi.in", 'w') as f: |
|
|
f.write(content) |
|
|
|
|
|
def generate_production_file(steps, temperature, pressure, cutoff, dt=0.002): |
|
|
"""Generate mdin_prod.in file for production run""" |
|
|
content = f"""Production Run |
|
|
&cntrl |
|
|
imin=0, |
|
|
ntx=1, |
|
|
irest=0, |
|
|
pres0={pressure}, |
|
|
taup=1.0, |
|
|
temp0={temperature}, |
|
|
tempi={temperature}, |
|
|
nstlim={steps}, |
|
|
dt={dt}, |
|
|
ntf=2, |
|
|
ntc=2, |
|
|
ntpr=1000, |
|
|
ntwx=1000, |
|
|
ntwr=1000, |
|
|
cut={cutoff}, |
|
|
ntb=2, |
|
|
ntp=1, |
|
|
ntt=3, |
|
|
gamma_ln=3.0, |
|
|
ig=-1, |
|
|
iwrap=1, |
|
|
ntr=0, |
|
|
/ |
|
|
|
|
|
""" |
|
|
|
|
|
with open(OUTPUT_DIR / "mdin_prod.in", 'w') as f: |
|
|
f.write(content) |
|
|
|
|
|
def generate_submit_pbs_file(): |
|
|
"""Copy submit_jobs.pbs template into output folder""" |
|
|
try: |
|
|
templates_dir = Path("templates") |
|
|
template_path = templates_dir / "submit_jobs.pbs" |
|
|
if not template_path.exists(): |
|
|
logger.warning("submit_jobs.pbs template not found; skipping PBS generation") |
|
|
return False |
|
|
with open(template_path, 'r') as tf: |
|
|
content = tf.read() |
|
|
with open(OUTPUT_DIR / "submit_jobs.pbs", 'w') as outf: |
|
|
outf.write(content) |
|
|
return True |
|
|
except Exception as e: |
|
|
logger.error(f"Error generating submit_jobs.pbs: {e}") |
|
|
return False |
|
|
|
|
|
@app.route('/api/health', methods=['GET']) |
|
|
def health_check(): |
|
|
"""Health check endpoint""" |
|
|
return jsonify({'status': 'healthy', 'message': 'MD Simulation Pipeline API is running'}) |
|
|
|
|
|
@app.route('/api/clean-output', methods=['POST']) |
|
|
def clean_output(): |
|
|
"""Clean output folder endpoint""" |
|
|
try: |
|
|
print("DEBUG: clean-output endpoint called") |
|
|
if clean_and_create_output_folder(): |
|
|
return jsonify({'success': True, 'message': 'Output folder cleaned successfully'}) |
|
|
else: |
|
|
return jsonify({'success': False, 'error': 'Failed to clean output folder'}), 500 |
|
|
except Exception as e: |
|
|
print(f"DEBUG: Error in clean-output: {str(e)}") |
|
|
return jsonify({'success': False, 'error': str(e)}), 500 |
|
|
|
|
|
@app.route('/api/download-output-zip', methods=['GET']) |
|
|
def download_output_zip(): |
|
|
"""Create a ZIP of the output folder and return it for download""" |
|
|
try: |
|
|
if not OUTPUT_DIR.exists(): |
|
|
return jsonify({'error': 'Output directory not found'}), 404 |
|
|
|
|
|
import tempfile |
|
|
import shutil |
|
|
|
|
|
|
|
|
tmp_dir = tempfile.mkdtemp() |
|
|
zip_base = os.path.join(tmp_dir, 'output') |
|
|
zip_path = shutil.make_archive(zip_base, 'zip', root_dir=str(OUTPUT_DIR)) |
|
|
|
|
|
|
|
|
return send_file(zip_path, as_attachment=True, download_name='output.zip') |
|
|
except Exception as e: |
|
|
logger.error(f"Error creating output ZIP: {str(e)}") |
|
|
return jsonify({'error': f'Failed to create ZIP: {str(e)}'}), 500 |
|
|
|
|
|
@app.route('/api/get-generated-files', methods=['GET']) |
|
|
def get_generated_files(): |
|
|
"""Return contents of known generated input files for preview""" |
|
|
try: |
|
|
files_to_read = [ |
|
|
'min_restrained.in', |
|
|
'min.in', |
|
|
'HeatNPT.in', |
|
|
'mdin_equi.in', |
|
|
'mdin_prod.in' |
|
|
] |
|
|
|
|
|
|
|
|
result = {} |
|
|
for name in files_to_read: |
|
|
path = OUTPUT_DIR / name |
|
|
if path.exists(): |
|
|
try: |
|
|
with open(path, 'r') as f: |
|
|
result[name] = f.read() |
|
|
except Exception as fe: |
|
|
result[name] = f"<error reading file: {fe}>" |
|
|
else: |
|
|
result[name] = "<file not found>" |
|
|
return jsonify({'success': True, 'files': result}) |
|
|
except Exception as e: |
|
|
logger.error(f"Error reading generated files: {str(e)}") |
|
|
return jsonify({'error': f'Failed to read files: {str(e)}'}), 500 |
|
|
|
|
|
def get_ligand_residue_name(): |
|
|
"""Extract ligand residue name from tleap_ready.pdb""" |
|
|
try: |
|
|
with open(OUTPUT_DIR / "tleap_ready.pdb", 'r') as f: |
|
|
for line in f: |
|
|
if line.startswith('HETATM'): |
|
|
|
|
|
residue_name = line[17:20].strip() |
|
|
if residue_name and residue_name not in ['HOH', 'WAT', 'TIP', 'SPC']: |
|
|
return residue_name |
|
|
return "LIG" |
|
|
except: |
|
|
return "LIG" |
|
|
|
|
|
def generate_ff_parameters_file(force_field, water_model, add_ions, distance): |
|
|
"""Generate the final force field parameters file with dynamic values""" |
|
|
|
|
|
print(f"DEBUG: force_field={force_field}, water_model={water_model}, add_ions={add_ions}, distance={distance}") |
|
|
|
|
|
|
|
|
ligand_present = (OUTPUT_DIR / "4_ligands_corrected.mol2").exists() |
|
|
|
|
|
|
|
|
ligand_name = get_ligand_residue_name() |
|
|
|
|
|
|
|
|
content = f"source leaprc.protein.{force_field}\n" |
|
|
|
|
|
|
|
|
print(f"DEBUG: water_model={water_model}") |
|
|
if water_model.lower() == "tip3p": |
|
|
content += "source leaprc.water.tip3p\n" |
|
|
elif water_model == "spce": |
|
|
content += "source leaprc.water.spce\n" |
|
|
|
|
|
|
|
|
if ligand_present: |
|
|
content += "source leaprc.gaff2\n\n" |
|
|
content += "loadamberparams 4_ligands_corrected.frcmod\n\n" |
|
|
content += f"{ligand_name} = loadmol2 4_ligands_corrected.mol2\n\n" |
|
|
else: |
|
|
content += "\n" |
|
|
|
|
|
content += "x = loadpdb tleap_ready.pdb\n\n" |
|
|
content += "charge x\n\n" |
|
|
|
|
|
|
|
|
if add_ions == "Na+": |
|
|
content += "addions x Na+ 0.0\n\n" |
|
|
elif add_ions == "Cl-": |
|
|
content += "addions x Cl- 0.0\n\n" |
|
|
|
|
|
|
|
|
|
|
|
if water_model.lower() == "tip3p": |
|
|
content += f"solvateBox x TIP3PBOX {distance}\n\n" |
|
|
elif water_model.lower() == "spce": |
|
|
content += f"solvateBox x SPCBOX {distance}\n\n" |
|
|
|
|
|
content += "saveamberparm x protein.prmtop protein.inpcrd\n\n" |
|
|
content += "savepdb x protein_solvated.pdb\n\n" |
|
|
content += "quit\n" |
|
|
|
|
|
|
|
|
print("DEBUG: Generated content:") |
|
|
print(content) |
|
|
|
|
|
|
|
|
with open(OUTPUT_DIR / "generate_ff_parameters.in", 'w') as f: |
|
|
f.write(content) |
|
|
|
|
|
@app.route('/api/generate-ff-parameters', methods=['POST']) |
|
|
def generate_ff_parameters(): |
|
|
"""Generate final force field parameters using tleap""" |
|
|
try: |
|
|
data = request.get_json() |
|
|
force_field = data.get('force_field', 'ff14SB') |
|
|
water_model = data.get('water_model', 'TIP3P') |
|
|
add_ions = data.get('add_ions', 'None') |
|
|
distance = data.get('distance', 10.0) |
|
|
|
|
|
|
|
|
generate_ff_parameters_file(force_field, water_model, add_ions, distance) |
|
|
|
|
|
|
|
|
tleap_path = None |
|
|
try: |
|
|
result = subprocess.run(['which', 'tleap'], capture_output=True, text=True) |
|
|
if result.returncode == 0: |
|
|
tleap_path = result.stdout.strip() |
|
|
except: |
|
|
pass |
|
|
|
|
|
if not tleap_path: |
|
|
conda_prefix = os.environ.get('CONDA_PREFIX') |
|
|
if conda_prefix: |
|
|
tleap_path = os.path.join(conda_prefix, 'bin', 'tleap') |
|
|
else: |
|
|
tleap_path = '/home/hn533621/.conda/envs/MD_pipeline/bin/tleap' |
|
|
|
|
|
|
|
|
cmd = f"{tleap_path} -f generate_ff_parameters.in" |
|
|
result = subprocess.run(cmd, shell=True, cwd=str(OUTPUT_DIR), |
|
|
capture_output=True, text=True, timeout=300) |
|
|
|
|
|
if result.returncode != 0: |
|
|
logger.error(f"tleap failed: {result.stderr}") |
|
|
return jsonify({ |
|
|
'success': False, |
|
|
'error': f'tleap failed: {result.stderr}' |
|
|
}), 500 |
|
|
|
|
|
|
|
|
output_files = ['protein.prmtop', 'protein.inpcrd', 'protein_solvated.pdb'] |
|
|
missing_files = [f for f in output_files if not (OUTPUT_DIR / f).exists()] |
|
|
|
|
|
if missing_files: |
|
|
return jsonify({ |
|
|
'success': False, |
|
|
'error': f'Missing output files: {", ".join(missing_files)}' |
|
|
}), 500 |
|
|
|
|
|
return jsonify({ |
|
|
'success': True, |
|
|
'message': 'Force field parameters generated successfully', |
|
|
'files_generated': output_files |
|
|
}) |
|
|
|
|
|
except subprocess.TimeoutExpired: |
|
|
return jsonify({ |
|
|
'success': False, |
|
|
'error': 'tleap command timed out after 5 minutes' |
|
|
}), 500 |
|
|
except Exception as e: |
|
|
logger.error(f"Error generating FF parameters: {str(e)}") |
|
|
return jsonify({ |
|
|
'success': False, |
|
|
'error': f'Failed to generate force field parameters: {str(e)}' |
|
|
}), 500 |
|
|
|
|
|
if __name__ == '__main__': |
|
|
print("🧬 MD Simulation Pipeline") |
|
|
print("=========================") |
|
|
print("🌐 Starting Flask server...") |
|
|
print("📡 Backend API: http://localhost:5000") |
|
|
print("🔗 Web Interface: http://localhost:5000") |
|
|
print("") |
|
|
print("Press Ctrl+C to stop the server") |
|
|
print("") |
|
|
|
|
|
|
|
|
print("🧹 Cleaning output folder...") |
|
|
clean_and_create_output_folder() |
|
|
print("✅ Output folder ready!") |
|
|
print("") |
|
|
|
|
|
app.run(debug=False, host='0.0.0.0', port=5000) |
|
|
|