// MD Simulation Pipeline JavaScript console.log('Script loading...'); // Debug log class MDSimulationPipeline { constructor() { this.currentProtein = null; this.preparedProtein = null; this.simulationParams = {}; this.generatedFiles = {}; this.nglStage = null; this.preparedNglStage = null; this.currentRepresentation = 'cartoon'; this.preparedRepresentation = 'cartoon'; this.isSpinning = false; this.preparedIsSpinning = false; this.currentTabIndex = 0; this.tabOrder = ['protein-loading', 'structure-prep', 'simulation-params', 'simulation-steps', 'file-generation']; this.init(); this.initializeTooltips(); } init() { this.setupEventListeners(); this.initializeTabs(); this.initializeStepToggles(); this.loadDefaultParams(); this.updateNavigationState(); } initializeTooltips() { // Initialize Bootstrap tooltips using vanilla JavaScript // Note: This requires Bootstrap to be loaded if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) { const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-toggle="tooltip"]')); tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl); }); } else { console.log('Bootstrap not loaded, tooltips will not work'); } } setupEventListeners() { // Tab navigation document.querySelectorAll('.tab-button').forEach(button => { button.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab)); }); // File upload const fileInput = document.getElementById('pdb-file'); const fileUploadArea = document.getElementById('file-upload-area'); const chooseFileBtn = document.getElementById('choose-file-btn'); console.log('File input element:', fileInput); console.log('File upload area:', fileUploadArea); console.log('Choose file button:', chooseFileBtn); if (!fileInput) { console.error('File input element not found!'); return; } fileInput.addEventListener('change', (e) => this.handleFileUpload(e)); // Handle click on upload area (but not on the button) fileUploadArea.addEventListener('click', (e) => { // Only trigger if not clicking on the button if (e.target !== chooseFileBtn && !chooseFileBtn.contains(e.target)) { console.log('Upload area clicked, triggering file input'); fileInput.click(); } }); // Handle click on choose file button chooseFileBtn.addEventListener('click', (e) => { e.stopPropagation(); // Prevent triggering the upload area click console.log('Choose file button clicked, triggering file input'); fileInput.click(); }); fileUploadArea.addEventListener('dragover', (e) => this.handleDragOver(e)); fileUploadArea.addEventListener('drop', (e) => this.handleDrop(e)); // PDB fetch document.getElementById('fetch-pdb').addEventListener('click', () => this.fetchPDB()); // File generation document.getElementById('generate-files').addEventListener('click', () => this.generateAllFiles()); document.getElementById('preview-files').addEventListener('click', () => this.previewFiles()); document.getElementById('preview-solvated').addEventListener('click', () => this.previewSolvatedProtein()); document.getElementById('download-zip').addEventListener('click', () => this.downloadZip()); // Structure preparation document.getElementById('prepare-structure').addEventListener('click', () => this.prepareStructure()); document.getElementById('preview-prepared').addEventListener('click', () => this.previewPreparedStructure()); document.getElementById('download-prepared').addEventListener('click', () => this.downloadPreparedStructure()); // Ligand download button const downloadLigandBtn = document.getElementById('download-ligand'); if (downloadLigandBtn) { downloadLigandBtn.addEventListener('click', (e) => { e.preventDefault(); this.downloadLigandFile(); }); } // Navigation buttons document.getElementById('prev-tab').addEventListener('click', () => this.previousTab()); document.getElementById('next-tab').addEventListener('click', () => this.nextTab()); // Parameter changes document.querySelectorAll('input, select').forEach(input => { input.addEventListener('change', () => this.updateSimulationParams()); }); // Render chain and ligand choices when structure tab becomes visible document.querySelector('[data-tab="structure-prep"]').addEventListener('click', () => { this.renderChainAndLigandSelections(); }); // Separate ligands checkbox change document.getElementById('separate-ligands').addEventListener('change', (e) => { const downloadBtn = document.getElementById('download-ligand'); if (e.target.checked && this.preparedProtein && this.preparedProtein.ligand_present && this.preparedProtein.ligand_content) { downloadBtn.disabled = false; downloadBtn.classList.remove('btn-outline-secondary'); downloadBtn.classList.add('btn-outline-primary'); } else { downloadBtn.disabled = true; downloadBtn.classList.remove('btn-outline-primary'); downloadBtn.classList.add('btn-outline-secondary'); } }); // Preserve ligands checkbox change document.getElementById('preserve-ligands').addEventListener('change', (e) => { this.toggleLigandForceFieldGroup(e.target.checked); }); } initializeTabs() { const tabs = document.querySelectorAll('.tab-content'); tabs.forEach(tab => { if (!tab.classList.contains('active')) { tab.style.display = 'none'; } }); } initializeStepToggles() { document.querySelectorAll('.step-header').forEach(header => { header.addEventListener('click', () => { const stepItem = header.parentElement; const content = stepItem.querySelector('.step-content'); const isActive = content.classList.contains('active'); // Close all other step contents document.querySelectorAll('.step-content').forEach(c => c.classList.remove('active')); // Toggle current step if (!isActive) { content.classList.add('active'); } }); }); } loadDefaultParams() { this.simulationParams = { boxType: 'cubic', boxSize: 1.0, boxMargin: 1.0, forceField: 'amber99sb-ildn', waterModel: 'tip3p', ionConcentration: 150, temperature: 300, pressure: 1.0, couplingType: 'berendsen', timestep: 0.002, cutoff: 1.0, pmeOrder: 4, steps: { restrainedMin: { enabled: true, steps: 1000, force: 1000 }, minimization: { enabled: true, steps: 5000, algorithm: 'steep' }, nvt: { enabled: true, steps: 50000, temperature: 300 }, npt: { enabled: true, steps: 100000, temperature: 300, pressure: 1.0 }, production: { enabled: true, steps: 1000000, temperature: 300, pressure: 1.0 } } }; } switchTab(tabName) { // Hide all tab contents document.querySelectorAll('.tab-content').forEach(tab => { tab.classList.remove('active'); tab.style.display = 'none'; }); // Remove active class from all tab buttons document.querySelectorAll('.tab-button').forEach(button => { button.classList.remove('active'); }); // Show selected tab document.getElementById(tabName).classList.add('active'); document.getElementById(tabName).style.display = 'block'; // Add active class to clicked button document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); // Update current tab index and navigation state this.currentTabIndex = this.tabOrder.indexOf(tabName); this.updateNavigationState(); } previousTab() { if (this.currentTabIndex > 0) { const prevTab = this.tabOrder[this.currentTabIndex - 1]; this.switchTab(prevTab); } } nextTab() { if (this.currentTabIndex < this.tabOrder.length - 1) { const nextTab = this.tabOrder[this.currentTabIndex + 1]; this.switchTab(nextTab); } } updateNavigationState() { const prevBtn = document.getElementById('prev-tab'); const nextBtn = document.getElementById('next-tab'); const currentStepSpan = document.getElementById('current-step'); const totalStepsSpan = document.getElementById('total-steps'); // Update button states prevBtn.disabled = this.currentTabIndex === 0; nextBtn.disabled = this.currentTabIndex === this.tabOrder.length - 1; // Update step indicator if (currentStepSpan) { currentStepSpan.textContent = this.currentTabIndex + 1; } if (totalStepsSpan) { totalStepsSpan.textContent = this.tabOrder.length; } // Update next button text based on current tab if (this.currentTabIndex === this.tabOrder.length - 1) { nextBtn.innerHTML = 'Complete '; } else { nextBtn.innerHTML = 'Next '; } } handleDragOver(e) { e.preventDefault(); e.currentTarget.style.background = '#e3f2fd'; } handleDrop(e) { e.preventDefault(); e.currentTarget.style.background = '#f8f9fa'; const files = e.dataTransfer.files; if (files.length > 0) { this.processFile(files[0]); } } handleFileUpload(e) { console.log('File upload triggered'); console.log('Files:', e.target.files); const file = e.target.files[0]; if (file) { console.log('File selected:', file.name, file.size, file.type); this.processFile(file); } else { console.log('No file selected'); } } processFile(file) { console.log('Processing file:', file.name, file.size, file.type); if (!file.name.toLowerCase().endsWith('.pdb') && !file.name.toLowerCase().endsWith('.ent')) { console.log('Invalid file type:', file.name); this.showStatus('error', 'Please upload a valid PDB file (.pdb or .ent)'); return; } console.log('File validation passed, reading file...'); const reader = new FileReader(); reader.onload = (e) => { console.log('File read successfully, content length:', e.target.result.length); const content = e.target.result; this.parsePDBFile(content, file.name); }; reader.onerror = (e) => { console.error('Error reading file:', e); this.showStatus('error', 'Error reading file'); }; reader.readAsText(file); } async parsePDBFile(content, filename) { try { // Clean output folder when new PDB is loaded try { await fetch('/api/clean-output', { method: 'POST' }); } catch (error) { console.log('Could not clean output folder:', error); } const lines = content.split('\n'); let atomCount = 0; let chains = new Set(); let residues = new Set(); let waterMolecules = 0; let ions = 0; let ligands = new Set(); let ligandDetails = []; let ligandGroups = new Map(); // Group by RESN-CHAIN let hetatoms = 0; let structureId = filename.replace(/\.(pdb|ent)$/i, '').toUpperCase(); // Common water molecule names const waterNames = new Set(['HOH', 'WAT', 'TIP3', 'TIP4', 'SPC', 'SPCE']); // Common ion names (expanded to include more ions) const ionNames = new Set(['NA', 'CL', 'K', 'MG', 'CA', 'ZN', 'FE', 'MN', 'CU', 'NI', 'CO', 'CD', 'HG', 'PB', 'SR', 'BA', 'RB', 'CS', 'LI', 'F', 'BR', 'I', 'SO4', 'PO4', 'CO3', 'NO3', 'NH4']); // Track ligand entities (separated by TER or different chains) let ligandEntities = new Map(); // Map to store ligand entities let currentLigandEntity = null; let currentChain = null; let currentResidue = null; // Track unique water molecules by residue const uniqueWaterResidues = new Set(); lines.forEach(line => { if (line.startsWith('ATOM')) { atomCount++; const chainId = line.substring(21, 22).trim(); if (chainId) chains.add(chainId); const resName = line.substring(17, 20).trim(); const resNum = line.substring(22, 26).trim(); residues.add(`${resName}${resNum}`); } else if (line.startsWith('HETATM')) { hetatoms++; const resName = line.substring(17, 20).trim(); const resNum = line.substring(22, 26).trim(); const chainId = line.substring(21, 22).trim(); const entityKey = `${resName}_${resNum}_${chainId}`; if (waterNames.has(resName)) { waterMolecules++; uniqueWaterResidues.add(entityKey); } else if (ionNames.has(resName)) { ions++; } else { // Everything else is treated as ligand (FALLBACK LOGIC) ligands.add(resName); ligandDetails.push({ resn: resName, chain: chainId, resi: resNum }); // Group by RESN-CHAIN for UI display const groupKey = `${resName}-${chainId}`; if (!ligandGroups.has(groupKey)) { ligandGroups.set(groupKey, { resn: resName, chain: chainId }); } // Track ligand entities (separated by TER or different chains) if (currentChain !== chainId || currentResidue !== resName) { // New ligand entity detected currentLigandEntity = `${resName}_${chainId}`; currentChain = chainId; currentResidue = resName; if (!ligandEntities.has(currentLigandEntity)) { ligandEntities.set(currentLigandEntity, { name: resName, chain: chainId, residueNum: resNum, atomCount: 0 }); } } // Increment atom count for this ligand entity if (ligandEntities.has(currentLigandEntity)) { ligandEntities.get(currentLigandEntity).atomCount++; } } } else if (line.startsWith('TER')) { // TER record separates entities, reset current ligand tracking currentLigandEntity = null; currentChain = null; currentResidue = null; } }); // Count unique water molecules const uniqueWaterCount = uniqueWaterResidues.size; // Count ligand entities (different ligands or same ligand in different chains) const ligandEntityCount = ligandEntities.size; // Get unique ligand names const uniqueLigandNames = Array.from(ligands); // Create ligand info string let ligandInfo = 'None'; if (uniqueLigandNames.length > 0) { if (ligandEntityCount > 1) { // Multiple entities - show count and names ligandInfo = `${ligandEntityCount} entities: ${uniqueLigandNames.join(', ')}`; } else { // Single entity ligandInfo = uniqueLigandNames.join(', '); } } this.currentProtein = { filename: filename, structureId: structureId, atomCount: atomCount, chains: Array.from(chains), residueCount: residues.size, waterMolecules: uniqueWaterCount, ions: ions, ligands: uniqueLigandNames, ligandDetails: ligandDetails, ligandGroups: Array.from(ligandGroups.values()), // RESN-CHAIN groups for UI ligandEntities: ligandEntityCount, ligandInfo: ligandInfo, hetatoms: hetatoms, content: content }; this.displayProteinInfo(); this.showStatus('success', `Successfully loaded ${filename}`); } catch (error) { this.showStatus('error', 'Error parsing PDB file: ' + error.message); } } displayProteinInfo() { if (!this.currentProtein) return; document.getElementById('structure-id').textContent = this.currentProtein.structureId; document.getElementById('atom-count').textContent = this.currentProtein.atomCount.toLocaleString(); document.getElementById('chain-info').textContent = this.currentProtein.chains.join(', '); document.getElementById('residue-count').textContent = this.currentProtein.residueCount.toLocaleString(); document.getElementById('water-count').textContent = this.currentProtein.waterMolecules.toLocaleString(); document.getElementById('ion-count').textContent = this.currentProtein.ions.toLocaleString(); document.getElementById('ligand-info').textContent = this.currentProtein.ligandInfo; document.getElementById('hetatm-count').textContent = this.currentProtein.hetatoms.toLocaleString(); document.getElementById('protein-preview').style.display = 'block'; // Load 3D visualization this.load3DVisualization(); // Also refresh chain/ligand lists when protein info is displayed this.renderChainAndLigandSelections(); } async fetchPDB() { const pdbId = document.getElementById('pdb-id').value.trim().toUpperCase(); if (!pdbId) { this.showStatus('error', 'Please enter a PDB ID'); return; } if (!/^[0-9A-Z]{4}$/.test(pdbId)) { this.showStatus('error', 'Please enter a valid 4-character PDB ID'); return; } this.showStatus('info', 'Fetching PDB structure...'); try { const response = await fetch(`https://files.rcsb.org/download/${pdbId}.pdb`); if (!response.ok) { throw new Error(`PDB ID ${pdbId} not found`); } const content = await response.text(); this.parsePDBFile(content, `${pdbId}.pdb`); this.showStatus('success', `Successfully fetched PDB structure ${pdbId}`); } catch (error) { this.showStatus('error', `Error fetching PDB: ${error.message}`); } } showStatus(type, message) { const statusDiv = document.getElementById('pdb-status'); statusDiv.className = `status-message ${type}`; statusDiv.textContent = message; statusDiv.style.display = 'block'; // Auto-hide after 5 seconds for success messages if (type === 'success') { setTimeout(() => { statusDiv.style.display = 'none'; }, 5000); } } updateSimulationParams() { // Update basic parameters this.simulationParams.boxType = document.getElementById('box-type').value; this.simulationParams.boxSize = parseFloat(document.getElementById('box-size').value); this.simulationParams.forceField = document.getElementById('force-field').value; this.simulationParams.waterModel = document.getElementById('water-model').value; this.simulationParams.addIons = document.getElementById('add-ions').value; this.simulationParams.temperature = parseInt(document.getElementById('temperature').value); this.simulationParams.pressure = parseFloat(document.getElementById('pressure').value); this.simulationParams.couplingType = document.getElementById('coupling-type').value; this.simulationParams.timestep = parseFloat(document.getElementById('timestep').value); this.simulationParams.cutoff = parseFloat(document.getElementById('cutoff').value); this.simulationParams.electrostatic = document.getElementById('electrostatic').value; this.simulationParams.ligandForceField = document.getElementById('ligand-forcefield').value; // Update step parameters this.simulationParams.steps.restrainedMin = { enabled: document.getElementById('enable-restrained-min').checked, steps: parseInt(document.getElementById('restrained-steps').value), force: parseInt(document.getElementById('restrained-force').value) }; this.simulationParams.steps.minimization = { enabled: document.getElementById('enable-minimization').checked, steps: parseInt(document.getElementById('min-steps').value), algorithm: document.getElementById('min-algorithm').value }; this.simulationParams.steps.nvt = { enabled: document.getElementById('enable-nvt').checked, steps: parseInt(document.getElementById('nvt-steps').value), temperature: parseInt(document.getElementById('nvt-temp').value) }; this.simulationParams.steps.npt = { enabled: document.getElementById('enable-npt').checked, steps: parseInt(document.getElementById('npt-steps').value), temperature: parseInt(document.getElementById('npt-temp').value), pressure: parseFloat(document.getElementById('npt-pressure').value) }; this.simulationParams.steps.production = { enabled: document.getElementById('enable-production').checked, steps: parseInt(document.getElementById('prod-steps').value), temperature: parseInt(document.getElementById('prod-temp').value), pressure: parseFloat(document.getElementById('prod-pressure').value) }; } toggleLigandForceFieldGroup(show) { const section = document.getElementById('ligand-forcefield-section'); if (show) { section.style.display = 'block'; section.classList.remove('disabled'); } else { section.style.display = 'none'; section.classList.add('disabled'); } } async calculateNetCharge(event) { console.log('calculateNetCharge called'); // Debug log if (!this.preparedProtein) { alert('Please prepare structure first before calculating net charge.'); return; } // Show loading state const button = event ? event.target : document.querySelector('button[onclick*="calculateNetCharge"]'); const originalText = button.innerHTML; button.innerHTML = ' Calculating...'; button.disabled = true; try { // Get the selected force field const selectedForceField = document.getElementById('force-field').value; const response = await fetch('/api/calculate-net-charge', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ force_field: selectedForceField }) }); const result = await response.json(); if (result.success) { // Update the Add Ions dropdown based on suggestion const addIonsSelect = document.getElementById('add-ions'); if (result.ion_type === 'Cl-') { addIonsSelect.value = 'Cl-'; } else if (result.ion_type === 'Na+') { addIonsSelect.value = 'Na+'; } else { addIonsSelect.value = 'None'; } // Show detailed results alert(`✅ System Charge Analysis Complete!\n\n` + `Net Charge: ${result.net_charge}\n` + `Recommendation: ${result.suggestion}\n` + `Ligand Present: ${result.ligand_present ? 'Yes' : 'No'}`); } else { alert(`❌ Error: ${result.error}`); } } catch (error) { console.error('Error calculating net charge:', error); alert(`❌ Error: Failed to calculate net charge. ${error.message}`); } finally { // Restore button state button.innerHTML = originalText; button.disabled = false; } } async generateLigandFF(event) { console.log('generateLigandFF called'); // Debug log if (!this.preparedProtein || !this.preparedProtein.ligand_present) { alert('No ligand found. Please ensure ligands are preserved during structure preparation.'); return; } const selectedFF = document.getElementById('ligand-forcefield').value; // Show loading state const button = event ? event.target : document.querySelector('button[onclick*="generateLigandFF"]'); const originalText = button.innerHTML; button.innerHTML = ' Generating...'; button.disabled = true; try { const response = await fetch('/api/generate-ligand-ff', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ force_field: selectedFF }) }); const result = await response.json(); if (result.success) { alert(`✅ ${result.message}\n\nNet charge: ${result.net_charge}\n\nGenerated files:\n- ${result.files.mol2}\n- ${result.files.frcmod}`); } else { alert(`❌ Error: ${result.error}`); } } catch (error) { console.error('Error generating ligand force field:', error); alert(`❌ Error: Failed to generate force field parameters. ${error.message}`); } finally { // Restore button state button.innerHTML = originalText; button.disabled = false; } } countAtomsInPDB(pdbContent) { const lines = pdbContent.split('\n'); return lines.filter(line => line.startsWith('ATOM') || line.startsWith('HETATM')).length; } async generateAllFiles() { if (!this.preparedProtein) { alert('Please prepare structure first'); return; } // Show loading state const button = document.getElementById('generate-files'); const originalText = button.innerHTML; button.innerHTML = ' Generating...'; button.disabled = true; try { // Collect all simulation parameters const params = { cutoff_distance: parseFloat(document.getElementById('cutoff').value), temperature: parseFloat(document.getElementById('temperature').value), pressure: parseFloat(document.getElementById('pressure').value), restrained_steps: parseInt(document.getElementById('restrained-steps').value), restrained_force: parseFloat(document.getElementById('restrained-force').value), min_steps: parseInt(document.getElementById('min-steps').value), npt_heating_steps: parseInt(document.getElementById('nvt-steps').value), npt_equilibration_steps: parseInt(document.getElementById('npt-steps').value), production_steps: parseInt(document.getElementById('prod-steps').value), timestep: parseFloat(document.getElementById('timestep').value), // Force field parameters force_field: document.getElementById('force-field').value, water_model: document.getElementById('water-model').value, add_ions: document.getElementById('add-ions').value, distance: parseFloat(document.getElementById('box-size').value) }; const response = await fetch('/api/generate-all-files', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(params) }); const result = await response.json(); if (result.success) { let message = `✅ ${result.message}\n\nGenerated files:\n`; result.files_generated.forEach(file => { message += `- ${file}\n`; }); if (result.warnings && result.warnings.length > 0) { message += `\n⚠️ Warnings:\n`; result.warnings.forEach(warning => { message += `- ${warning}\n`; }); } alert(message); // Reveal the download section const downloadSection = document.getElementById('download-section'); if (downloadSection) { downloadSection.style.display = 'block'; } } else { alert(`❌ Error: ${result.error}`); } } catch (error) { console.error('Error generating files:', error); alert(`❌ Error: Failed to generate simulation files. ${error.message}`); } finally { // Restore button state button.innerHTML = originalText; button.disabled = false; } } createSimulationFiles() { const files = {}; const proteinName = this.currentProtein.structureId.toLowerCase(); // Generate GROMACS input files files[`${proteinName}.mdp`] = this.generateMDPFile(); files[`${proteinName}_restrained.mdp`] = this.generateRestrainedMDPFile(); files[`${proteinName}_min.mdp`] = this.generateMinimizationMDPFile(); files[`${proteinName}_nvt.mdp`] = this.generateNVTMDPFile(); files[`${proteinName}_npt.mdp`] = this.generateNPTMDPFile(); files[`${proteinName}_prod.mdp`] = this.generateProductionMDPFile(); // Generate PBS script files[`${proteinName}_simulation.pbs`] = this.generatePBSScript(); // Generate setup script files[`setup_${proteinName}.sh`] = this.generateSetupScript(); // Generate analysis script files[`analyze_${proteinName}.sh`] = this.generateAnalysisScript(); return files; } generateMDPFile() { const params = this.simulationParams; return `; 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 `; } generateRestrainedMDPFile() { const params = this.simulationParams; return `; 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.pme_order} ; Van der Waals vdwtype = Cut-off rvdw = ${params.cutoff} `; } generateMinimizationMDPFile() { const params = this.simulationParams; return `; 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.pme_order} ; Van der Waals vdwtype = Cut-off rvdw = ${params.cutoff} `; } generateNVTMDPFile() { const params = this.simulationParams; return `; 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.pme_order} ; 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 `; } generateNPTMDPFile() { const params = this.simulationParams; return `; 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.pme_order} ; 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 `; } generateProductionMDPFile() { return this.generateMDPFile(); // Same as main MDP file } generatePBSScript() { const proteinName = this.currentProtein.structureId.toLowerCase(); const totalSteps = this.simulationParams.steps.production.steps; const timeInNs = (totalSteps * this.simulationParams.timestep) / 1000; return `#!/bin/bash #PBS -N ${proteinName}_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=${proteinName} STEPS=${totalSteps} TIME_NS=${timeInNs.toFixed(2)} 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" `; } generateSetupScript() { const proteinName = this.currentProtein.structureId.toLowerCase(); return `#!/bin/bash # Setup script for ${proteinName} MD simulation # Generated by MD Simulation Pipeline set -e PROTEIN=${proteinName} FORCE_FIELD=${this.simulationParams.forceField} WATER_MODEL=${this.simulationParams.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 ${this.simulationParams.boxMargin} -bt ${this.simulationParams.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" `; } generateAnalysisScript() { const proteinName = this.currentProtein.structureId.toLowerCase(); return `#!/bin/bash # Analysis script for ${proteinName} MD simulation # Generated by MD Simulation Pipeline PROTEIN=${proteinName} 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" `; } displayGeneratedFiles() { const filesList = document.getElementById('files-list'); filesList.innerHTML = ''; Object.entries(this.generatedFiles).forEach(([filename, content]) => { const fileItem = document.createElement('div'); fileItem.className = 'file-item'; const fileType = this.getFileType(filename); const fileSize = this.formatFileSize(content.length); fileItem.innerHTML = `
Type: ${fileType}
Size: ${fileSize}
`; filesList.appendChild(fileItem); }); } getFileType(filename) { const extension = filename.split('.').pop().toLowerCase(); const types = { 'mdp': 'GROMACS MDP', 'pbs': 'PBS Script', 'sh': 'Shell Script', 'gro': 'GROMACS Structure', 'top': 'GROMACS Topology', 'xvg': 'GROMACS Data' }; return types[extension] || 'Text File'; } getFileIcon(filename) { const extension = filename.split('.').pop().toLowerCase(); const icons = { 'mdp': 'fa-cogs', 'pbs': 'fa-tasks', 'sh': 'fa-terminal', 'gro': 'fa-cube', 'top': 'fa-sitemap', 'xvg': 'fa-chart-line' }; return icons[extension] || 'fa-file'; } formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } previewFile(filename) { const content = this.generatedFiles[filename]; const previewWindow = window.open('', '_blank', 'width=800,height=600'); previewWindow.document.write(`${content}
`);
}
downloadFile(filename) {
const content = this.generatedFiles[filename];
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
async previewFiles() {
try {
const resp = await fetch('/api/get-generated-files');
const data = await resp.json();
if (!data.success) {
alert('❌ Error: ' + (data.error || 'Unable to load files'));
return;
}
const filesList = document.getElementById('files-list');
if (!filesList) return;
filesList.innerHTML = '';
// Store file contents for modal display
this.fileContents = data.files;
Object.entries(data.files).forEach(([name, content]) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.style.cssText = 'padding: 10px; margin: 5px 0; border: 1px solid #ddd; border-radius: 5px; cursor: pointer; background: #f9f9f9;';
fileItem.innerHTML = `${name}`;
fileItem.onclick = () => this.showFileContent(name, content);
filesList.appendChild(fileItem);
});
// Reveal preview and download areas
const preview = document.getElementById('files-preview');
if (preview) preview.style.display = 'block';
const dl = document.getElementById('download-section');
if (dl) dl.style.display = 'block';
this.switchTab('file-generation');
} catch (e) {
console.error('Preview error:', e);
alert('❌ Failed to preview files: ' + e.message);
}
}
showFileContent(filename, content) {
// Create modal if it doesn't exist
let modal = document.getElementById('file-content-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'file-content-modal';
modal.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.5); z-index: 1000; display: none;
`;
modal.innerHTML = `
Structure ID: ${protein.structureId}
Atoms: ${protein.atomCount.toLocaleString()}
Chains: ${protein.chains.join(', ')}
Residues: ${protein.residueCount.toLocaleString()}
Water molecules: ${protein.waterMolecules.toLocaleString()}
Ions: ${protein.ions.toLocaleString()}
Ligands: ${protein.ligands.length > 0 ? protein.ligands.join(', ') : 'None'}
HETATM entries: ${protein.hetatoms.toLocaleString()}
Type: ${params.boxType}
Size: ${params.boxSize} nm
Margin: ${params.boxMargin} nm
Force Field: ${params.forceField}
Water Model: ${params.waterModel}
Ion Conc.: ${params.ionConcentration} mM
Temperature: ${params.temperature} K
Pressure: ${params.pressure} bar
Time Step: ${params.timestep} ps
Total Time: ${totalTime.toFixed(2)} ns
Steps: ${params.steps.production.steps.toLocaleString()}
Output Freq: Every 5 ps
MDP Files: 6
Scripts: 3
Total Size: ${this.formatFileSize(Object.values(this.generatedFiles).join('').length)}
Preparing structure...
`; try { // Call Python backend const response = await fetch('/api/prepare-structure', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ pdb_content: this.currentProtein.content, options: options }) }); const result = await response.json(); if (result.success) { // Store prepared structure this.preparedProtein = { content: 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.ligand_present, separate_ligands: result.separate_ligands, ligand_content: result.ligand_content || '' }; // Format removed components const removedText = result.removed_components ? Object.entries(result.removed_components) .filter(([key, value]) => value > 0) .map(([key, value]) => `${key}: ${value}`) .join(', ') || 'None' : 'None'; // Format added capping const addedText = result.added_capping ? Object.entries(result.added_capping) .filter(([key, value]) => value > 0) .map(([key, value]) => `${key}: ${value}`) .join(', ') || 'None' : 'None'; // Update status document.getElementById('prep-status-content').innerHTML = `Structure preparation completed!
Original atoms: ${result.original_atoms.toLocaleString()}
Prepared atoms: ${result.prepared_atoms.toLocaleString()}
Removed: ${removedText}
Added: ${addedText}
Ligands: ${result.preserved_ligands}
Ready for AMBER force field generation!
`; // Enable preview and download buttons document.getElementById('preview-prepared').disabled = false; document.getElementById('download-prepared').disabled = false; // Enable ligand download button if ligands are present and separate ligands is checked const separateLigandsChecked = document.getElementById('separate-ligands').checked; const downloadLigandBtn = document.getElementById('download-ligand'); if (result.ligand_present && separateLigandsChecked && result.ligand_content) { downloadLigandBtn.disabled = false; downloadLigandBtn.classList.remove('btn-outline-secondary'); downloadLigandBtn.classList.add('btn-outline-primary'); } else { downloadLigandBtn.disabled = true; downloadLigandBtn.classList.remove('btn-outline-primary'); downloadLigandBtn.classList.add('btn-outline-secondary'); } // Show ligand force field group if preserve ligands is checked const preserveLigandsChecked = document.getElementById('preserve-ligands').checked; if (preserveLigandsChecked && result.ligand_present) { this.toggleLigandForceFieldGroup(true); } } else { throw new Error(result.error || 'Structure preparation failed'); } } catch (error) { console.error('Error preparing structure:', error); document.getElementById('prep-status-content').innerHTML = `Error preparing structure
${error.message}
`; } } renderChainAndLigandSelections() { if (!this.currentProtein) return; // Render chains const chainContainer = document.getElementById('chain-selection'); if (chainContainer) { chainContainer.innerHTML = ''; this.currentProtein.chains.forEach(chainId => { const id = `chain-${chainId}`; const wrapper = document.createElement('div'); wrapper.className = 'checkbox-inline'; wrapper.innerHTML = ` `; chainContainer.appendChild(wrapper); }); } // Render ligands (RESN-CHAIN groups) const ligandContainer = document.getElementById('ligand-selection'); if (ligandContainer) { ligandContainer.innerHTML = ''; if (Array.isArray(this.currentProtein.ligandGroups) && this.currentProtein.ligandGroups.length > 0) { this.currentProtein.ligandGroups.forEach(l => { const key = `${l.resn}-${l.chain}`; const id = `lig-${key}`; const wrapper = document.createElement('div'); wrapper.className = 'checkbox-inline'; wrapper.innerHTML = ` `; ligandContainer.appendChild(wrapper); }); } else { // Fallback: show unique ligand names if detailed positions not parsed if (Array.isArray(this.currentProtein.ligands) && this.currentProtein.ligands.length > 0) { this.currentProtein.ligands.forEach(resn => { const id = `lig-${resn}`; const wrapper = document.createElement('div'); wrapper.className = 'checkbox-inline'; wrapper.innerHTML = ` `; ligandContainer.appendChild(wrapper); }); } else { ligandContainer.innerHTML = 'No ligands detected'; } } } } getSelectedChains() { const container = document.getElementById('chain-selection'); if (!container) return []; return Array.from(container.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.getAttribute('data-chain')); } getSelectedLigands() { const container = document.getElementById('ligand-selection'); if (!container) return []; return Array.from(container.querySelectorAll('input[type="checkbox"]:checked')).map(cb => ({ resn: cb.getAttribute('data-resn') || '', chain: cb.getAttribute('data-chain') || '' })); } previewPreparedStructure() { if (!this.preparedProtein) { alert('Please prepare a protein structure first'); return; } // Show prepared structure preview document.getElementById('prepared-structure-preview').style.display = 'block'; // Format removed components const removedText = this.preparedProtein.removed_components ? Object.entries(this.preparedProtein.removed_components) .filter(([key, value]) => value > 0) .map(([key, value]) => `${key}: ${value}`) .join(', ') || 'None' : 'None'; // Format added capping const addedText = this.preparedProtein.added_capping ? Object.entries(this.preparedProtein.added_capping) .filter(([key, value]) => value > 0) .map(([key, value]) => `${key}: ${value}`) .join(', ') || 'None' : 'None'; // Update structure info document.getElementById('original-atoms').textContent = this.preparedProtein.original_atoms.toLocaleString(); document.getElementById('prepared-atoms').textContent = this.preparedProtein.prepared_atoms.toLocaleString(); document.getElementById('removed-components').textContent = removedText; document.getElementById('added-capping').textContent = addedText; document.getElementById('preserved-ligands').textContent = this.preparedProtein.preserved_ligands; // Load 3D visualization of prepared structure this.loadPrepared3DVisualization(); } downloadPreparedStructure() { if (!this.preparedProtein) { alert('Please prepare a structure first'); return; } // Download prepared structure const blob = new Blob([this.preparedProtein.content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `tleap_ready.pdb`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } downloadLigandFile() { if (!this.preparedProtein || !this.preparedProtein.ligand_present || !this.preparedProtein.ligand_content) { alert('No ligand file available. Please prepare structure with separate ligands enabled.'); return; } // Download ligand file const ligandBlob = new Blob([this.preparedProtein.ligand_content], { type: 'text/plain' }); const ligandUrl = URL.createObjectURL(ligandBlob); const ligandA = document.createElement('a'); ligandA.href = ligandUrl; ligandA.download = `4_ligands_corrected.pdb`; document.body.appendChild(ligandA); ligandA.click(); document.body.removeChild(ligandA); URL.revokeObjectURL(ligandUrl); } // 3D Visualization for prepared structure async loadPrepared3DVisualization() { if (!this.preparedProtein) return; try { // Initialize NGL stage for prepared structure if not already done if (!this.preparedNglStage) { this.preparedNglStage = new NGL.Stage("prepared-ngl-viewer", { backgroundColor: "white", quality: "medium" }); } // Clear existing components this.preparedNglStage.removeAllComponents(); // Create a blob from prepared PDB content const blob = new Blob([this.preparedProtein.content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); // Load the prepared structure const component = await this.preparedNglStage.loadFile(url, { ext: "pdb", defaultRepresentation: false }); // Add cartoon representation for protein with chain-based colors component.addRepresentation("cartoon", { sele: "protein", colorScheme: "chainname", opacity: 0.9 }); // Add ball and stick for ligands (if any) - check for HETATM records component.addRepresentation("ball+stick", { sele: "hetero", color: "element", radius: 0.2, opacity: 0.8 }); // Auto-fit the view this.preparedNglStage.autoView(); // Show controls document.getElementById('prepared-viewer-controls').style.display = 'flex'; // Clean up the blob URL URL.revokeObjectURL(url); } catch (error) { console.error('Error loading prepared 3D visualization:', error); } } resetPreparedView() { if (this.preparedNglStage) { this.preparedNglStage.autoView(); } } togglePreparedRepresentation() { if (!this.preparedNglStage) return; const components = this.preparedNglStage.compList; if (components.length === 0) return; const component = components[0]; component.removeAllRepresentations(); if (this.preparedRepresentation === 'cartoon') { // Switch to ball and stick component.addRepresentation("ball+stick", { color: "element", radius: 0.15 }); this.preparedRepresentation = 'ball+stick'; document.getElementById('prepared-style-text').textContent = 'Ball & Stick'; } else if (this.preparedRepresentation === 'ball+stick') { // Switch to surface component.addRepresentation("surface", { sele: "protein", colorScheme: "chainname", opacity: 0.7 }); this.preparedRepresentation = 'surface'; document.getElementById('prepared-style-text').textContent = 'Surface'; } else { // Switch back to cartoon component.addRepresentation("cartoon", { sele: "protein", colorScheme: "chainname", opacity: 0.8 }); // Add ball and stick for ligands if (this.preparedProtein.preserved_ligands !== 'None') { component.addRepresentation("ball+stick", { sele: "hetero", color: "element", radius: 0.15 }); } this.preparedRepresentation = 'cartoon'; document.getElementById('prepared-style-text').textContent = 'Mixed View'; } } togglePreparedSpin() { if (!this.preparedNglStage) return; this.preparedIsSpinning = !this.preparedIsSpinning; this.preparedNglStage.setSpin(this.preparedIsSpinning); } } // Initialize the application when the page loads function initializeApp() { console.log('Initializing mdPipeline...'); // Debug log window.mdPipeline = new MDSimulationPipeline(); console.log('mdPipeline initialized:', window.mdPipeline); // Debug log } // Try to initialize immediately if DOM is already loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeApp); } else { // DOM is already loaded initializeApp(); } // Add some utility functions for better UX function formatNumber(num) { return num.toLocaleString(); } function formatTime(seconds) { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const secs = seconds % 60; return `${hours}h ${minutes}m ${secs}s`; }