|
|
|
|
|
console.log('Script loading...'); |
|
|
|
|
|
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() { |
|
|
|
|
|
|
|
|
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() { |
|
|
|
|
|
document.querySelectorAll('.tab-button').forEach(button => { |
|
|
button.addEventListener('click', (e) => this.switchTab(e.target.dataset.tab)); |
|
|
}); |
|
|
|
|
|
|
|
|
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)); |
|
|
|
|
|
|
|
|
fileUploadArea.addEventListener('click', (e) => { |
|
|
|
|
|
if (e.target !== chooseFileBtn && !chooseFileBtn.contains(e.target)) { |
|
|
console.log('Upload area clicked, triggering file input'); |
|
|
fileInput.click(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
chooseFileBtn.addEventListener('click', (e) => { |
|
|
e.stopPropagation(); |
|
|
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)); |
|
|
|
|
|
|
|
|
document.getElementById('fetch-pdb').addEventListener('click', () => this.fetchPDB()); |
|
|
|
|
|
|
|
|
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()); |
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('prepare-structure').addEventListener('click', () => this.prepareStructure()); |
|
|
document.getElementById('preview-prepared').addEventListener('click', () => this.previewPreparedStructure()); |
|
|
document.getElementById('download-prepared').addEventListener('click', () => this.downloadPreparedStructure()); |
|
|
|
|
|
|
|
|
const downloadLigandBtn = document.getElementById('download-ligand'); |
|
|
if (downloadLigandBtn) { |
|
|
downloadLigandBtn.addEventListener('click', (e) => { |
|
|
e.preventDefault(); |
|
|
this.downloadLigandFile(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('prev-tab').addEventListener('click', () => this.previousTab()); |
|
|
document.getElementById('next-tab').addEventListener('click', () => this.nextTab()); |
|
|
|
|
|
|
|
|
document.querySelectorAll('input, select').forEach(input => { |
|
|
input.addEventListener('change', () => this.updateSimulationParams()); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelector('[data-tab="structure-prep"]').addEventListener('click', () => { |
|
|
this.renderChainAndLigandSelections(); |
|
|
}); |
|
|
|
|
|
|
|
|
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'); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
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'); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.step-content').forEach(c => c.classList.remove('active')); |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
document.querySelectorAll('.tab-content').forEach(tab => { |
|
|
tab.classList.remove('active'); |
|
|
tab.style.display = 'none'; |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.tab-button').forEach(button => { |
|
|
button.classList.remove('active'); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById(tabName).classList.add('active'); |
|
|
document.getElementById(tabName).style.display = 'block'; |
|
|
|
|
|
|
|
|
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); |
|
|
|
|
|
|
|
|
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'); |
|
|
|
|
|
|
|
|
prevBtn.disabled = this.currentTabIndex === 0; |
|
|
nextBtn.disabled = this.currentTabIndex === this.tabOrder.length - 1; |
|
|
|
|
|
|
|
|
if (currentStepSpan) { |
|
|
currentStepSpan.textContent = this.currentTabIndex + 1; |
|
|
} |
|
|
if (totalStepsSpan) { |
|
|
totalStepsSpan.textContent = this.tabOrder.length; |
|
|
} |
|
|
|
|
|
|
|
|
if (this.currentTabIndex === this.tabOrder.length - 1) { |
|
|
nextBtn.innerHTML = 'Complete <i class="fas fa-check"></i>'; |
|
|
} else { |
|
|
nextBtn.innerHTML = 'Next <i class="fas fa-chevron-right"></i>'; |
|
|
} |
|
|
} |
|
|
|
|
|
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 { |
|
|
|
|
|
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(); |
|
|
let hetatoms = 0; |
|
|
let structureId = filename.replace(/\.(pdb|ent)$/i, '').toUpperCase(); |
|
|
|
|
|
|
|
|
const waterNames = new Set(['HOH', 'WAT', 'TIP3', 'TIP4', 'SPC', 'SPCE']); |
|
|
|
|
|
|
|
|
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']); |
|
|
|
|
|
|
|
|
let ligandEntities = new Map(); |
|
|
let currentLigandEntity = null; |
|
|
let currentChain = null; |
|
|
let currentResidue = null; |
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
ligands.add(resName); |
|
|
ligandDetails.push({ resn: resName, chain: chainId, resi: resNum }); |
|
|
|
|
|
|
|
|
const groupKey = `${resName}-${chainId}`; |
|
|
if (!ligandGroups.has(groupKey)) { |
|
|
ligandGroups.set(groupKey, { resn: resName, chain: chainId }); |
|
|
} |
|
|
|
|
|
|
|
|
if (currentChain !== chainId || currentResidue !== resName) { |
|
|
|
|
|
currentLigandEntity = `${resName}_${chainId}`; |
|
|
currentChain = chainId; |
|
|
currentResidue = resName; |
|
|
|
|
|
if (!ligandEntities.has(currentLigandEntity)) { |
|
|
ligandEntities.set(currentLigandEntity, { |
|
|
name: resName, |
|
|
chain: chainId, |
|
|
residueNum: resNum, |
|
|
atomCount: 0 |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (ligandEntities.has(currentLigandEntity)) { |
|
|
ligandEntities.get(currentLigandEntity).atomCount++; |
|
|
} |
|
|
} |
|
|
} else if (line.startsWith('TER')) { |
|
|
|
|
|
currentLigandEntity = null; |
|
|
currentChain = null; |
|
|
currentResidue = null; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const uniqueWaterCount = uniqueWaterResidues.size; |
|
|
|
|
|
|
|
|
const ligandEntityCount = ligandEntities.size; |
|
|
|
|
|
|
|
|
const uniqueLigandNames = Array.from(ligands); |
|
|
|
|
|
|
|
|
let ligandInfo = 'None'; |
|
|
if (uniqueLigandNames.length > 0) { |
|
|
if (ligandEntityCount > 1) { |
|
|
|
|
|
ligandInfo = `${ligandEntityCount} entities: ${uniqueLigandNames.join(', ')}`; |
|
|
} else { |
|
|
|
|
|
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()), |
|
|
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'; |
|
|
|
|
|
|
|
|
this.load3DVisualization(); |
|
|
|
|
|
|
|
|
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'; |
|
|
|
|
|
|
|
|
if (type === 'success') { |
|
|
setTimeout(() => { |
|
|
statusDiv.style.display = 'none'; |
|
|
}, 5000); |
|
|
} |
|
|
} |
|
|
|
|
|
updateSimulationParams() { |
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
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'); |
|
|
if (!this.preparedProtein) { |
|
|
alert('Please prepare structure first before calculating net charge.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const button = event ? event.target : document.querySelector('button[onclick*="calculateNetCharge"]'); |
|
|
const originalText = button.innerHTML; |
|
|
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Calculating...'; |
|
|
button.disabled = true; |
|
|
|
|
|
try { |
|
|
|
|
|
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) { |
|
|
|
|
|
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'; |
|
|
} |
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
button.innerHTML = originalText; |
|
|
button.disabled = false; |
|
|
} |
|
|
} |
|
|
|
|
|
async generateLigandFF(event) { |
|
|
console.log('generateLigandFF called'); |
|
|
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; |
|
|
|
|
|
|
|
|
const button = event ? event.target : document.querySelector('button[onclick*="generateLigandFF"]'); |
|
|
const originalText = button.innerHTML; |
|
|
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 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 { |
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
const button = document.getElementById('generate-files'); |
|
|
const originalText = button.innerHTML; |
|
|
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating...'; |
|
|
button.disabled = true; |
|
|
|
|
|
try { |
|
|
|
|
|
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: 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); |
|
|
|
|
|
|
|
|
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 { |
|
|
|
|
|
button.innerHTML = originalText; |
|
|
button.disabled = false; |
|
|
} |
|
|
} |
|
|
|
|
|
createSimulationFiles() { |
|
|
const files = {}; |
|
|
const proteinName = this.currentProtein.structureId.toLowerCase(); |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
|
|
|
files[`${proteinName}_simulation.pbs`] = this.generatePBSScript(); |
|
|
|
|
|
|
|
|
files[`setup_${proteinName}.sh`] = this.generateSetupScript(); |
|
|
|
|
|
|
|
|
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(); |
|
|
} |
|
|
|
|
|
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 = ` |
|
|
<h4><i class="fas ${this.getFileIcon(filename)}"></i> ${filename}</h4> |
|
|
<p><strong>Type:</strong> ${fileType}</p> |
|
|
<p><strong>Size:</strong> ${fileSize}</p> |
|
|
<button class="btn btn-secondary btn-sm" onclick="mdPipeline.previewFile('${filename}')"> |
|
|
<i class="fas fa-eye"></i> Preview |
|
|
</button> |
|
|
<button class="btn btn-primary btn-sm" onclick="mdPipeline.downloadFile('${filename}')"> |
|
|
<i class="fas fa-download"></i> Download |
|
|
</button> |
|
|
`; |
|
|
|
|
|
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(` |
|
|
<html> |
|
|
<head> |
|
|
<title>Preview: ${filename}</title> |
|
|
<style> |
|
|
body { font-family: monospace; margin: 20px; background: #f5f5f5; } |
|
|
pre { background: white; padding: 20px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } |
|
|
h1 { color: #333; } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<h1>${filename}</h1> |
|
|
<pre>${content}</pre> |
|
|
</body> |
|
|
</html> |
|
|
`); |
|
|
} |
|
|
|
|
|
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 = ''; |
|
|
|
|
|
|
|
|
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 = `<strong>${name}</strong>`; |
|
|
fileItem.onclick = () => this.showFileContent(name, content); |
|
|
filesList.appendChild(fileItem); |
|
|
}); |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
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 = ` |
|
|
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); |
|
|
background: white; border-radius: 10px; padding: 20px; max-width: 80%; max-height: 80%; |
|
|
overflow: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3);"> |
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;"> |
|
|
<h3 id="modal-filename" style="margin: 0; color: #333;"></h3> |
|
|
<button id="close-modal" style="background: #dc3545; color: white; border: none; |
|
|
border-radius: 5px; padding: 8px 15px; cursor: pointer;">Close</button> |
|
|
</div> |
|
|
<pre id="modal-content" style="background: #f8f9fa; padding: 15px; border-radius: 5px; |
|
|
overflow: auto; max-height: 60vh; white-space: pre-wrap; font-family: monospace;"></pre> |
|
|
</div> |
|
|
`; |
|
|
document.body.appendChild(modal); |
|
|
|
|
|
|
|
|
document.getElementById('close-modal').onclick = () => modal.style.display = 'none'; |
|
|
modal.onclick = (e) => { |
|
|
if (e.target === modal) modal.style.display = 'none'; |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('modal-filename').textContent = filename; |
|
|
document.getElementById('modal-content').textContent = content; |
|
|
modal.style.display = 'block'; |
|
|
} |
|
|
|
|
|
async downloadZip() { |
|
|
try { |
|
|
const resp = await fetch('/api/download-output-zip'); |
|
|
if (!resp.ok) { |
|
|
const text = await resp.text(); |
|
|
throw new Error(text || 'Failed to create ZIP'); |
|
|
} |
|
|
const blob = await resp.blob(); |
|
|
const url = window.URL.createObjectURL(blob); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = 'output.zip'; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
a.remove(); |
|
|
window.URL.revokeObjectURL(url); |
|
|
} catch (e) { |
|
|
console.error('Download error:', e); |
|
|
alert('❌ Failed to download ZIP: ' + e.message); |
|
|
} |
|
|
} |
|
|
|
|
|
async previewSolvatedProtein() { |
|
|
try { |
|
|
|
|
|
const button = document.getElementById('preview-solvated'); |
|
|
const originalText = button.innerHTML; |
|
|
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Loading...'; |
|
|
button.disabled = true; |
|
|
|
|
|
|
|
|
const response = await fetch('/api/get-viewer-pdb'); |
|
|
if (!response.ok) { |
|
|
throw new Error('Viewer PDB not available. Please generate files first.'); |
|
|
} |
|
|
|
|
|
const data = await response.json(); |
|
|
if (!data.success) { |
|
|
throw new Error(data.error || 'Failed to load viewer PDB'); |
|
|
} |
|
|
|
|
|
|
|
|
window.open('/viewer/viewer_protein_with_ligand.pdb', '_blank'); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('Error previewing solvated protein:', error); |
|
|
alert('❌ Error: ' + error.message); |
|
|
} finally { |
|
|
|
|
|
const button = document.getElementById('preview-solvated'); |
|
|
button.innerHTML = '<i class="fas fa-tint"></i> Preview Solvated Protein'; |
|
|
button.disabled = false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
displaySimulationSummary() { |
|
|
const summaryContent = document.getElementById('summary-content'); |
|
|
const params = this.simulationParams; |
|
|
const protein = this.currentProtein; |
|
|
|
|
|
const totalTime = (params.steps.production.steps * params.timestep) / 1000; |
|
|
|
|
|
summaryContent.innerHTML = ` |
|
|
<div class="summary-item"> |
|
|
<h4>Protein Information</h4> |
|
|
<p><strong>Structure ID:</strong> ${protein.structureId}</p> |
|
|
<p><strong>Atoms:</strong> ${protein.atomCount.toLocaleString()}</p> |
|
|
<p><strong>Chains:</strong> ${protein.chains.join(', ')}</p> |
|
|
<p><strong>Residues:</strong> ${protein.residueCount.toLocaleString()}</p> |
|
|
</div> |
|
|
<div class="summary-item"> |
|
|
<h4>System Components</h4> |
|
|
<p><strong>Water molecules:</strong> ${protein.waterMolecules.toLocaleString()}</p> |
|
|
<p><strong>Ions:</strong> ${protein.ions.toLocaleString()}</p> |
|
|
<p><strong>Ligands:</strong> ${protein.ligands.length > 0 ? protein.ligands.join(', ') : 'None'}</p> |
|
|
<p><strong>HETATM entries:</strong> ${protein.hetatoms.toLocaleString()}</p> |
|
|
</div> |
|
|
<div class="summary-item"> |
|
|
<h4>Simulation Box</h4> |
|
|
<p><strong>Type:</strong> ${params.boxType}</p> |
|
|
<p><strong>Size:</strong> ${params.boxSize} nm</p> |
|
|
<p><strong>Margin:</strong> ${params.boxMargin} nm</p> |
|
|
</div> |
|
|
<div class="summary-item"> |
|
|
<h4>Force Field & Water</h4> |
|
|
<p><strong>Force Field:</strong> ${params.forceField}</p> |
|
|
<p><strong>Water Model:</strong> ${params.waterModel}</p> |
|
|
<p><strong>Ion Conc.:</strong> ${params.ionConcentration} mM</p> |
|
|
</div> |
|
|
<div class="summary-item"> |
|
|
<h4>Simulation Parameters</h4> |
|
|
<p><strong>Temperature:</strong> ${params.temperature} K</p> |
|
|
<p><strong>Pressure:</strong> ${params.pressure} bar</p> |
|
|
<p><strong>Time Step:</strong> ${params.timestep} ps</p> |
|
|
</div> |
|
|
<div class="summary-item"> |
|
|
<h4>Simulation Time</h4> |
|
|
<p><strong>Total Time:</strong> ${totalTime.toFixed(2)} ns</p> |
|
|
<p><strong>Steps:</strong> ${params.steps.production.steps.toLocaleString()}</p> |
|
|
<p><strong>Output Freq:</strong> Every 5 ps</p> |
|
|
</div> |
|
|
<div class="summary-item"> |
|
|
<h4>Generated Files</h4> |
|
|
<p><strong>MDP Files:</strong> 6</p> |
|
|
<p><strong>Scripts:</strong> 3</p> |
|
|
<p><strong>Total Size:</strong> ${this.formatFileSize(Object.values(this.generatedFiles).join('').length)}</p> |
|
|
</div> |
|
|
`; |
|
|
} |
|
|
|
|
|
|
|
|
async load3DVisualization() { |
|
|
if (!this.currentProtein) return; |
|
|
|
|
|
try { |
|
|
|
|
|
if (!this.nglStage) { |
|
|
this.nglStage = new NGL.Stage("ngl-viewer", { |
|
|
backgroundColor: "white", |
|
|
quality: "medium" |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
this.nglStage.removeAllComponents(); |
|
|
|
|
|
|
|
|
const blob = new Blob([this.currentProtein.content], { type: 'text/plain' }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
|
|
|
|
|
|
const component = await this.nglStage.loadFile(url, { |
|
|
ext: "pdb", |
|
|
defaultRepresentation: false |
|
|
}); |
|
|
|
|
|
|
|
|
component.addRepresentation("cartoon", { |
|
|
sele: "protein", |
|
|
colorScheme: "chainname", |
|
|
opacity: 0.9 |
|
|
}); |
|
|
|
|
|
|
|
|
if (this.currentProtein.waterMolecules > 0) { |
|
|
component.addRepresentation("ball+stick", { |
|
|
sele: "water", |
|
|
color: "cyan", |
|
|
colorScheme: "uniform", |
|
|
radius: 0.1 |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (this.currentProtein.ions > 0) { |
|
|
component.addRepresentation("ball+stick", { |
|
|
sele: "ion", |
|
|
color: "element", |
|
|
radius: 0.2 |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (this.currentProtein.ligands.length > 0) { |
|
|
component.addRepresentation("ball+stick", { |
|
|
sele: "hetero", |
|
|
color: "element", |
|
|
radius: 0.15 |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
this.nglStage.autoView(); |
|
|
|
|
|
|
|
|
document.getElementById('viewer-controls').style.display = 'flex'; |
|
|
|
|
|
|
|
|
URL.revokeObjectURL(url); |
|
|
|
|
|
} catch (error) { |
|
|
console.error('Error loading 3D visualization:', error); |
|
|
this.showStatus('error', 'Error loading 3D visualization: ' + error.message); |
|
|
} |
|
|
} |
|
|
|
|
|
resetView() { |
|
|
if (this.nglStage) { |
|
|
this.nglStage.autoView(); |
|
|
} |
|
|
} |
|
|
|
|
|
toggleRepresentation() { |
|
|
if (!this.nglStage) return; |
|
|
|
|
|
const components = this.nglStage.compList; |
|
|
if (components.length === 0) return; |
|
|
|
|
|
const component = components[0]; |
|
|
component.removeAllRepresentations(); |
|
|
|
|
|
if (this.currentRepresentation === 'cartoon') { |
|
|
|
|
|
component.addRepresentation("ball+stick", { |
|
|
color: "element", |
|
|
radius: 0.15 |
|
|
}); |
|
|
this.currentRepresentation = 'ball+stick'; |
|
|
document.getElementById('style-text').textContent = 'Ball & Stick'; |
|
|
} else if (this.currentRepresentation === 'ball+stick') { |
|
|
|
|
|
component.addRepresentation("surface", { |
|
|
sele: "protein", |
|
|
colorScheme: "chainname", |
|
|
opacity: 0.7 |
|
|
}); |
|
|
|
|
|
|
|
|
if (this.currentProtein.waterMolecules > 0) { |
|
|
component.addRepresentation("ball+stick", { |
|
|
sele: "water", |
|
|
color: "cyan", |
|
|
colorScheme: "uniform", |
|
|
radius: 0.1 |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (this.currentProtein.ions > 0) { |
|
|
component.addRepresentation("ball+stick", { |
|
|
sele: "ion", |
|
|
color: "element", |
|
|
radius: 0.2 |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (this.currentProtein.ligands.length > 0) { |
|
|
component.addRepresentation("ball+stick", { |
|
|
sele: "hetero", |
|
|
color: "element", |
|
|
radius: 0.15 |
|
|
}); |
|
|
} |
|
|
|
|
|
this.currentRepresentation = 'surface'; |
|
|
document.getElementById('style-text').textContent = 'Surface'; |
|
|
} else { |
|
|
|
|
|
component.addRepresentation("cartoon", { |
|
|
sele: "protein", |
|
|
colorScheme: "chainname", |
|
|
opacity: 0.8 |
|
|
}); |
|
|
|
|
|
|
|
|
if (this.currentProtein.waterMolecules > 0) { |
|
|
component.addRepresentation("ball+stick", { |
|
|
sele: "water", |
|
|
color: "cyan", |
|
|
colorScheme: "uniform", |
|
|
radius: 0.1 |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (this.currentProtein.ions > 0) { |
|
|
component.addRepresentation("ball+stick", { |
|
|
sele: "ion", |
|
|
color: "element", |
|
|
radius: 0.2 |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
if (this.currentProtein.ligands.length > 0) { |
|
|
component.addRepresentation("ball+stick", { |
|
|
sele: "hetero", |
|
|
color: "element", |
|
|
radius: 0.15 |
|
|
}); |
|
|
} |
|
|
|
|
|
this.currentRepresentation = 'cartoon'; |
|
|
document.getElementById('style-text').textContent = 'Mixed View'; |
|
|
} |
|
|
} |
|
|
|
|
|
toggleSpin() { |
|
|
if (!this.nglStage) return; |
|
|
|
|
|
this.isSpinning = !this.isSpinning; |
|
|
this.nglStage.setSpin(this.isSpinning); |
|
|
} |
|
|
|
|
|
|
|
|
async prepareStructure() { |
|
|
if (!this.currentProtein) { |
|
|
alert('Please load a protein structure first'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const options = { |
|
|
remove_water: document.getElementById('remove-water').checked, |
|
|
remove_ions: document.getElementById('remove-ions').checked, |
|
|
remove_hydrogens: document.getElementById('remove-hydrogens').checked, |
|
|
add_nme: document.getElementById('add-nme').checked, |
|
|
add_ace: document.getElementById('add-ace').checked, |
|
|
preserve_ligands: document.getElementById('preserve-ligands').checked, |
|
|
separate_ligands: document.getElementById('separate-ligands').checked, |
|
|
selected_chains: this.getSelectedChains(), |
|
|
selected_ligands: this.getSelectedLigands() |
|
|
}; |
|
|
|
|
|
|
|
|
document.getElementById('prep-status').style.display = 'block'; |
|
|
document.getElementById('prep-status-content').innerHTML = ` |
|
|
<p><i class="fas fa-spinner fa-spin"></i> Preparing structure...</p> |
|
|
`; |
|
|
|
|
|
try { |
|
|
|
|
|
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) { |
|
|
|
|
|
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 || '' |
|
|
}; |
|
|
|
|
|
|
|
|
const removedText = result.removed_components ? |
|
|
Object.entries(result.removed_components) |
|
|
.filter(([key, value]) => value > 0) |
|
|
.map(([key, value]) => `${key}: ${value}`) |
|
|
.join(', ') || 'None' : 'None'; |
|
|
|
|
|
|
|
|
const addedText = result.added_capping ? |
|
|
Object.entries(result.added_capping) |
|
|
.filter(([key, value]) => value > 0) |
|
|
.map(([key, value]) => `${key}: ${value}`) |
|
|
.join(', ') || 'None' : 'None'; |
|
|
|
|
|
|
|
|
document.getElementById('prep-status-content').innerHTML = ` |
|
|
<p><i class="fas fa-check-circle"></i> Structure preparation completed!</p> |
|
|
<p><strong>Original atoms:</strong> ${result.original_atoms.toLocaleString()}</p> |
|
|
<p><strong>Prepared atoms:</strong> ${result.prepared_atoms.toLocaleString()}</p> |
|
|
<p><strong>Removed:</strong> ${removedText}</p> |
|
|
<p><strong>Added:</strong> ${addedText}</p> |
|
|
<p><strong>Ligands:</strong> ${result.preserved_ligands}</p> |
|
|
<p>Ready for AMBER force field generation!</p> |
|
|
`; |
|
|
|
|
|
|
|
|
document.getElementById('preview-prepared').disabled = false; |
|
|
document.getElementById('download-prepared').disabled = false; |
|
|
|
|
|
|
|
|
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'); |
|
|
} |
|
|
|
|
|
|
|
|
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 = ` |
|
|
<p><i class="fas fa-exclamation-triangle"></i> Error preparing structure</p> |
|
|
<p>${error.message}</p> |
|
|
`; |
|
|
} |
|
|
} |
|
|
|
|
|
renderChainAndLigandSelections() { |
|
|
if (!this.currentProtein) return; |
|
|
|
|
|
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 = ` |
|
|
<label class="checkbox-container"> |
|
|
<input type="checkbox" id="${id}" data-chain="${chainId}"> |
|
|
<span class="checkmark"></span> |
|
|
Chain ${chainId} |
|
|
</label>`; |
|
|
chainContainer.appendChild(wrapper); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
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 = ` |
|
|
<label class="checkbox-container"> |
|
|
<input type="checkbox" id="${id}" data-resn="${l.resn}" data-chain="${l.chain}"> |
|
|
<span class="checkmark"></span> |
|
|
${key} |
|
|
</label>`; |
|
|
ligandContainer.appendChild(wrapper); |
|
|
}); |
|
|
} else { |
|
|
|
|
|
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 = ` |
|
|
<label class="checkbox-container"> |
|
|
<input type="checkbox" id="${id}" data-resn="${resn}"> |
|
|
<span class="checkmark"></span> |
|
|
${resn} |
|
|
</label>`; |
|
|
ligandContainer.appendChild(wrapper); |
|
|
}); |
|
|
} else { |
|
|
ligandContainer.innerHTML = '<small>No ligands detected</small>'; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('prepared-structure-preview').style.display = 'block'; |
|
|
|
|
|
|
|
|
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'; |
|
|
|
|
|
|
|
|
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'; |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
this.loadPrepared3DVisualization(); |
|
|
} |
|
|
|
|
|
downloadPreparedStructure() { |
|
|
if (!this.preparedProtein) { |
|
|
alert('Please prepare a structure first'); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
|
|
|
async loadPrepared3DVisualization() { |
|
|
if (!this.preparedProtein) return; |
|
|
|
|
|
try { |
|
|
|
|
|
if (!this.preparedNglStage) { |
|
|
this.preparedNglStage = new NGL.Stage("prepared-ngl-viewer", { |
|
|
backgroundColor: "white", |
|
|
quality: "medium" |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
this.preparedNglStage.removeAllComponents(); |
|
|
|
|
|
|
|
|
const blob = new Blob([this.preparedProtein.content], { type: 'text/plain' }); |
|
|
const url = URL.createObjectURL(blob); |
|
|
|
|
|
|
|
|
const component = await this.preparedNglStage.loadFile(url, { |
|
|
ext: "pdb", |
|
|
defaultRepresentation: false |
|
|
}); |
|
|
|
|
|
|
|
|
component.addRepresentation("cartoon", { |
|
|
sele: "protein", |
|
|
colorScheme: "chainname", |
|
|
opacity: 0.9 |
|
|
}); |
|
|
|
|
|
|
|
|
component.addRepresentation("ball+stick", { |
|
|
sele: "hetero", |
|
|
color: "element", |
|
|
radius: 0.2, |
|
|
opacity: 0.8 |
|
|
}); |
|
|
|
|
|
|
|
|
this.preparedNglStage.autoView(); |
|
|
|
|
|
|
|
|
document.getElementById('prepared-viewer-controls').style.display = 'flex'; |
|
|
|
|
|
|
|
|
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') { |
|
|
|
|
|
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') { |
|
|
|
|
|
component.addRepresentation("surface", { |
|
|
sele: "protein", |
|
|
colorScheme: "chainname", |
|
|
opacity: 0.7 |
|
|
}); |
|
|
this.preparedRepresentation = 'surface'; |
|
|
document.getElementById('prepared-style-text').textContent = 'Surface'; |
|
|
} else { |
|
|
|
|
|
component.addRepresentation("cartoon", { |
|
|
sele: "protein", |
|
|
colorScheme: "chainname", |
|
|
opacity: 0.8 |
|
|
}); |
|
|
|
|
|
|
|
|
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); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function initializeApp() { |
|
|
console.log('Initializing mdPipeline...'); |
|
|
window.mdPipeline = new MDSimulationPipeline(); |
|
|
console.log('mdPipeline initialized:', window.mdPipeline); |
|
|
} |
|
|
|
|
|
|
|
|
if (document.readyState === 'loading') { |
|
|
document.addEventListener('DOMContentLoaded', initializeApp); |
|
|
} else { |
|
|
|
|
|
initializeApp(); |
|
|
} |
|
|
|
|
|
|
|
|
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`; |
|
|
} |
|
|
|