UNPKG

@peterspackman/occjs

Version:

JavaScript/WebAssembly bindings for OCC - a quantum chemistry and crystallography library

947 lines (835 loc) 66.1 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Wavefunction Calculator with Web Worker - Quantum Chemistry</title> <script src="https://cdn.tailwindcss.com"></script> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> <style> body { font-family: 'Inter', sans-serif; } .code-block { font-family: 'Courier New', monospace; } .log-output { background: #f8f9fa; color: #333; font-family: 'Courier New', monospace; font-size: 13px; line-height: 1.3; white-space: pre-wrap; overflow-y: auto; max-height: 500px; padding: 16px; border-radius: 6px; border: 1px solid #dee2e6; } .log-debug { color: #6c757d; } .log-info { color: #0f5132; } .log-warn { color: #664d03; } .log-error { color: #842029; } .matrix-display { font-family: 'Courier New', monospace; font-size: 11px; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px; padding: 0; overflow: auto; max-height: 500px; } .matrix-table { border-collapse: collapse; margin: 0; font-family: 'Courier New', monospace; font-size: 10px; } .matrix-table td { padding: 2px 6px; text-align: right; border: 1px solid #e0e0e0; white-space: nowrap; min-width: 80px; } .matrix-table th { padding: 2px 6px; text-align: center; border: 1px solid #ccc; background: #f0f0f0; font-weight: bold; font-size: 9px; } .matrix-table .row-header { background: #f8f8f8; font-weight: bold; text-align: center; min-width: 40px; } @keyframes spin { to { transform: rotate(360deg); } } .spinner { display: inline-block; width: 20px; height: 20px; border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; } </style> </head> <body class="bg-gray-50 min-h-screen"> <div class="min-h-screen bg-gray-50"> <!-- Header --> <div class="bg-white border-b border-gray-200 px-6 py-4"> <div class="flex items-center justify-between"> <div> <h1 class="text-2xl font-bold text-gray-900">Wavefunction Calculator (Web Worker)</h1> <p class="text-sm text-gray-600">Quantum Chemistry Calculations from XYZ Files - Non-blocking UI</p> </div> <div id="moleculeHeaderInfo" class="text-right text-sm text-gray-600 hidden"> <div id="moleculeName" class="font-medium text-gray-900"></div> <div id="moleculeFormula" class="text-gray-600"></div> </div> </div> </div> <!-- Main Layout --> <div class="flex h-screen"> <!-- Left Sidebar --> <div class="w-1/4 bg-white border-r border-gray-200 p-6 space-y-6 overflow-y-auto"> <!-- Worker Status --> <div id="workerStatus" class="bg-blue-50 border border-blue-200 rounded-lg p-3 text-sm"> <div class="flex items-center"> <div class="spinner mr-2"></div> <span>Initializing Web Worker...</span> </div> </div> <!-- File Upload Section --> <div> <h3 class="text-lg font-semibold text-gray-900 mb-3">Upload Molecule</h3> <div id="dropZone" class="border-2 border-dashed border-gray-300 rounded-lg p-4 text-center cursor-pointer hover:border-gray-400 transition-colors"> <svg class="mx-auto h-8 w-8 text-gray-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path> </svg> <p class="text-gray-600 text-sm mb-1">Drop XYZ file or click to browse</p> <input type="file" id="fileInput" class="hidden" accept=".xyz" /> </div> <div id="fileInfo" class="mt-3 hidden"> <p class="text-sm text-gray-600">File: <span id="fileName" class="font-medium"></span></p> <button id="clearFile" class="mt-1 text-xs text-red-600 hover:text-red-700">Clear</button> </div> <div class="mt-3"> <label class="block text-sm font-medium text-gray-700 mb-2">Or paste XYZ coordinates:</label> <textarea id="xyzInput" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm font-mono" rows="8" placeholder="3&#10;Water molecule&#10;O 0.000000 0.000000 0.000000&#10;H 0.757000 0.586000 0.000000&#10;H -0.757000 0.586000 0.000000"></textarea> </div> </div> <!-- Method Settings --> <div id="methodPanel" class="hidden"> <h3 class="text-lg font-semibold text-gray-900 mb-3">Calculation Method</h3> <div class="space-y-3"> <div> <label class="block text-sm font-medium text-gray-700 mb-1">Theory Level</label> <select id="method" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> <option value="hf">Hartree-Fock (HF)</option> <option value="dft-b3lyp">DFT (B3LYP)</option> <option value="dft-pbe">DFT (PBE)</option> <option value="dft-blyp">DFT (BLYP)</option> <option value="dft-wb97x">DFT (ωB97X)</option> <option value="dft-wb97m">DFT (ωB97M-V)</option> <option value="dft-m06-2x">DFT (M06-2X)</option> <option value="dft-pbe0">DFT (PBE0)</option> </select> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">Basis Set</label> <select id="basisSet" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> <option value="sto-3g">STO-3G</option> <option value="3-21g">3-21G</option> <option value="6-31g">6-31G</option> <option value="6-31g(d,p)" selected>6-31G(d,p)</option> <option value="def2-svp">def2-SVP</option> <option value="def2-tzvp">def2-TZVP</option> <option value="cc-pvdz">cc-pVDZ</option> <option value="cc-pvtz">cc-pVTZ</option> </select> </div> </div> </div> <!-- SCF Settings --> <div id="scfPanel" class="hidden"> <h3 class="text-lg font-semibold text-gray-900 mb-3">SCF Settings</h3> <div class="space-y-3"> <div> <label class="block text-sm font-medium text-gray-700 mb-1">Max Iterations</label> <input type="number" id="maxIterations" min="10" max="500" value="100" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">Energy Tolerance</label> <select id="energyTolerance" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> <option value="1e-6">1e-6</option> <option value="1e-7">1e-7</option> <option value="1e-8" selected>1e-8</option> <option value="1e-9">1e-9</option> <option value="1e-10">1e-10</option> </select> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">Log Level</label> <select id="logLevel" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> <option value="0">Trace</option> <option value="1">Debug</option> <option value="2" selected>Info</option> <option value="3">Warning</option> <option value="4">Error</option> <option value="5">Critical</option> </select> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">Number of Threads</label> <input type="number" id="numThreads" min="1" max="8" value="1" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> <p class="text-xs text-gray-500 mt-1">Note: Threading >1 requires CORS headers for SharedArrayBuffer</p> </div> </div> </div> <!-- Run Button --> <div id="runPanel" class="hidden"> <button id="calculateBtn" class="w-full bg-blue-600 text-white py-3 px-4 rounded-md hover:bg-blue-700 transition-colors font-medium disabled:bg-gray-400 disabled:cursor-not-allowed"> Run Calculation </button> <button id="cancelBtn" class="w-full mt-2 bg-red-600 text-white py-2 px-3 rounded-md hover:bg-red-700 transition-colors font-medium hidden"> Cancel Calculation </button> <div id="statusContent" class="mt-3 text-sm text-gray-600"> Upload a molecule to begin. </div> </div> </div> <!-- Main Content Area --> <div class="flex-1 bg-gray-50 overflow-hidden"> <!-- Tab Navigation --> <div id="tabNavigation" class="bg-white border-b border-gray-200 px-6 py-3 hidden"> <nav class="flex space-x-6"> <button id="outputTab" class="py-2 px-1 border-b-2 border-blue-500 text-blue-600 text-sm font-medium"> Output </button> <button id="resultsTab" class="py-2 px-1 border-b-2 border-transparent text-gray-500 hover:text-gray-700 text-sm font-medium"> Results </button> <button id="propertiesTab" class="py-2 px-1 border-b-2 border-transparent text-gray-500 hover:text-gray-700 text-sm font-medium"> Properties </button> </nav> </div> <!-- Tab Content --> <div class="h-full p-6"> <!-- Output Tab --> <div id="outputTabContent" class="h-full"> <div class="bg-white rounded-lg shadow h-full flex flex-col"> <div class="flex justify-between items-center p-4 border-b border-gray-200"> <h3 class="text-lg font-semibold text-gray-900">Calculation Output</h3> <div class="flex space-x-2"> <button id="clearLogBtn" class="text-sm bg-gray-500 text-white px-3 py-1 rounded hover:bg-gray-600">Clear</button> <button id="scrollLogBtn" class="text-sm bg-blue-500 text-white px-3 py-1 rounded hover:bg-blue-600">Auto-scroll</button> </div> </div> <div id="logOutput" class="log-output flex-1 m-4"></div> </div> </div> <!-- Results Tab --> <div id="resultsTabContent" class="h-full hidden"> <div class="bg-white rounded-lg shadow h-full p-6"> <h3 class="text-lg font-semibold text-gray-900 mb-4">Calculation Results</h3> <!-- Energy Results --> <div id="energyResults" class="mb-6 p-4 bg-green-50 rounded-lg hidden"> <h4 class="text-md font-medium text-green-900 mb-2">Energies</h4> <div id="energyDetails" class="text-sm text-green-800 space-y-1"></div> </div> <!-- Timing Info --> <div id="timingInfo" class="mb-4 text-sm text-gray-600"></div> <!-- Properties from Worker --> <div id="workerProperties" class="mb-6 p-4 bg-blue-50 rounded-lg hidden"> <h4 class="text-md font-medium text-blue-900 mb-2">Molecular Properties</h4> <div id="propertyDetails" class="text-sm text-blue-800 space-y-1"></div> </div> <!-- Wavefunction Export --> <div id="wavefunctionExport" class="mb-6 p-4 bg-gray-50 rounded-lg hidden"> <h4 class="text-md font-medium text-gray-900 mb-3">Export Wavefunction</h4> <div class="flex space-x-3 mb-3"> <select id="exportFormat" class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> <option value="fchk">Formatted Checkpoint (.fchk)</option> <option value="json">JSON Format (.json)</option> </select> <button id="downloadWavefunction" class="bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 transition-colors font-medium"> Download Wavefunction </button> </div> <p class="text-sm text-gray-600">Export the calculated wavefunction for use in other quantum chemistry programs or analysis tools.</p> </div> <!-- Cube File Generation --> <div id="cubeGeneration" class="mb-6 p-4 bg-purple-50 rounded-lg hidden"> <h4 class="text-md font-medium text-purple-900 mb-3">Generate Cube Files</h4> <div class="grid grid-cols-2 gap-4 mb-3"> <div> <label class="block text-sm font-medium text-gray-700 mb-1">Property</label> <select id="cubeProperty" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> <option value="density">Electron Density</option> <option value="mo">Molecular Orbital</option> <option value="mo_alpha">Alpha Molecular Orbital</option> <option value="mo_beta">Beta Molecular Orbital</option> <option value="esp">Electrostatic Potential</option> </select> </div> <div id="moIndexContainer" class="hidden"> <label class="block text-sm font-medium text-gray-700 mb-1">Orbital Index</label> <input type="number" id="moIndex" min="0" value="0" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> <p id="moIndexHint" class="text-xs text-gray-500 mt-1"></p> </div> </div> <div class="grid grid-cols-3 gap-2 mb-3"> <div> <label class="block text-sm font-medium text-gray-700 mb-1">Grid X</label> <input type="number" id="cubeNx" min="10" max="200" value="80" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">Grid Y</label> <input type="number" id="cubeNy" min="10" max="200" value="80" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> </div> <div> <label class="block text-sm font-medium text-gray-700 mb-1">Grid Z</label> <input type="number" id="cubeNz" min="10" max="200" value="80" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"> </div> </div> <button id="generateCubeBtn" class="bg-purple-600 text-white py-2 px-4 rounded-md hover:bg-purple-700 transition-colors font-medium"> Generate Cube File </button> <p class="text-sm text-gray-600 mt-2">Generate cube files for visualization in molecular viewers like NGL, VMD, or ChimeraX.</p> </div> <!-- Info Panel --> <div class="mt-6 p-4 bg-blue-50 rounded-lg"> <h4 class="text-md font-medium text-blue-900 mb-2">About This Calculation</h4> <p class="text-sm text-blue-800 mb-2">This quantum chemistry calculation was performed entirely in your browser using WebAssembly (WASM), with no data sent to any server. The calculation ran in a Web Worker to keep the UI responsive.</p> <p class="text-sm text-blue-800">Powered by <a href="https://github.com/peterspackman/occ" target="_blank" class="font-medium underline hover:text-blue-600">OCC (Open Computational Chemistry)</a> - an open-source quantum chemistry library compiled to WASM.</p> </div> </div> </div> <!-- Properties Tab --> <div id="propertiesTabContent" class="h-full hidden"> <div class="bg-white rounded-lg shadow h-full p-6"> <h3 class="text-lg font-semibold text-gray-900 mb-4">Molecular Properties</h3> <!-- Matrix Properties --> <div id="matrixSection" class="mb-6 hidden"> <h4 class="text-md font-medium text-gray-900 mb-3">Matrix Properties</h4> <div class="mb-3"> <select id="matrixSelect" class="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> <option value="overlap">Overlap Matrix (S)</option> <option value="kinetic">Kinetic Energy Matrix (T)</option> <option value="nuclear">Nuclear Attraction Matrix (V)</option> <option value="fock">Fock Matrix (F)</option> <option value="density">Density Matrix (P)</option> <option value="coefficients">MO Coefficients (C)</option> </select> </div> <div id="matrixInfo" class="mb-3 p-3 bg-gray-50 rounded"> <div id="matrixStats" class="text-sm text-gray-700"></div> </div> <div id="matrixDisplay" class="matrix-display mb-3"> Select a matrix to display its values. </div> <button id="exportMatrixBtn" class="bg-green-600 text-white py-2 px-4 rounded-md hover:bg-green-700 transition-colors text-sm hidden"> Export Matrix as CSV </button> </div> <!-- Orbital Properties --> <div id="orbitalSection" class="hidden"> <h4 class="text-md font-medium text-gray-900 mb-3">Orbital Properties</h4> <div id="energyList" class="text-sm space-y-1 max-h-60 overflow-y-auto"></div> </div> <!-- Worker Info --> <div class="mt-6 p-4 bg-blue-50 rounded-lg"> <h4 class="text-md font-medium text-blue-900 mb-2">Matrix Data from Worker</h4> <p class="text-sm text-blue-800">Matrix data has been computed in the Web Worker and transferred back for display.</p> </div> </div> </div> </div> </div> </div> </div> <!-- Error Modal --> <div id="errorModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50"> <div class="bg-white rounded-lg p-6 max-w-md"> <h3 class="text-lg font-semibold text-red-600 mb-2">Error</h3> <p id="errorMessage" class="text-gray-700 mb-4"></p> <button id="closeError" class="bg-gray-600 text-white px-4 py-2 rounded hover:bg-gray-700">Close</button> </div> </div> <script type="module"> let worker = null; let currentMolecule = null; let currentXYZData = null; let currentResults = null; let autoScroll = true; let isCalculating = false; // Initialize Web Worker async function initializeWorker() { try { worker = new Worker('./wavefunction-worker.js', { type: 'module' }); // Set up worker message handler worker.onmessage = handleWorkerMessage; worker.onerror = (error) => { console.error('Worker error:', error); showError('Worker error: ' + error.message); updateWorkerStatus('error', 'Worker error occurred'); }; // Determine if we're using npm package or local build let isNpmPackage = false; let modulePath = '../dist/index.browser.js'; try { // Try to detect if we can load npm package await import('@peterspackman/occjs'); isNpmPackage = true; } catch (e) { console.log('Using local build'); } // Initialize OCC in worker worker.postMessage({ type: 'init', data: { isNpmPackage: isNpmPackage, modulePath: modulePath, wasmPath: isNpmPackage ? undefined : '../dist/occjs.wasm', dataPath: isNpmPackage ? undefined : '../dist/occjs.data' } }); } catch (error) { console.error('Failed to initialize worker:', error); showError('Failed to initialize Web Worker: ' + error.message); updateWorkerStatus('error', 'Failed to initialize worker'); } } // Handle messages from worker function handleWorkerMessage(e) { const { type, ...data } = e.data; switch(type) { case 'initialized': if (data.success) { updateWorkerStatus('ready', 'Web Worker ready'); updateStatus('Worker initialized. Ready to accept molecules.'); } else { updateWorkerStatus('error', 'Worker initialization failed'); showError('Worker initialization failed: ' + data.error); } break; case 'log': addLogMessage(data.message, mapLogLevel(data.level)); break; case 'progress': updateStatus(data.message); if (data.stage === 'complete') { isCalculating = false; document.getElementById('calculateBtn').disabled = false; document.getElementById('calculateBtn').textContent = 'Run Calculation'; document.getElementById('cancelBtn').classList.add('hidden'); } break; case 'result': handleCalculationResult(data); break; case 'error': console.error('Worker error:', data.error); showError('Calculation error: ' + data.error); isCalculating = false; document.getElementById('calculateBtn').disabled = false; document.getElementById('calculateBtn').textContent = 'Run Calculation'; document.getElementById('cancelBtn').classList.add('hidden'); break; case 'cubeResult': handleCubeResult(data); break; } } // Map numeric log levels to CSS classes function mapLogLevel(level) { switch(level) { case 0: // TRACE case 1: // DEBUG return 'debug'; case 2: // INFO return 'info'; case 3: // WARN return 'warn'; case 4: // ERROR case 5: // CRITICAL return 'error'; default: return 'info'; } } // Update worker status display function updateWorkerStatus(status, message) { const statusDiv = document.getElementById('workerStatus'); statusDiv.className = 'rounded-lg p-3 text-sm'; switch(status) { case 'initializing': statusDiv.className += ' bg-blue-50 border border-blue-200'; statusDiv.innerHTML = '<div class="flex items-center"><div class="spinner mr-2"></div><span>' + message + '</span></div>'; break; case 'ready': statusDiv.className += ' bg-green-50 border border-green-200'; statusDiv.innerHTML = '<div class="flex items-center"><svg class="w-5 h-5 text-green-600 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg><span>' + message + '</span></div>'; break; case 'error': statusDiv.className += ' bg-red-50 border border-red-200'; statusDiv.innerHTML = '<div class="flex items-center"><svg class="w-5 h-5 text-red-600 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path></svg><span>' + message + '</span></div>'; break; } } // Handle calculation results from worker function handleCalculationResult(data) { isCalculating = false; document.getElementById('calculateBtn').disabled = false; document.getElementById('calculateBtn').textContent = 'Run Calculation'; document.getElementById('cancelBtn').classList.add('hidden'); if (data.success) { currentResults = data.results; displayResults(data.results); switchTab('results'); } else { showError('Calculation failed: ' + data.error); updateStatus('Calculation failed.'); } } // Add log message to output function addLogMessage(message, level = 'info') { const timestamp = new Date().toLocaleTimeString(); const div = document.createElement('div'); div.className = `log-${level}`; div.style.marginBottom = '2px'; div.textContent = `[${timestamp}] ${message}`; const logOutput = document.getElementById('logOutput'); logOutput.appendChild(div); if (autoScroll) { logOutput.scrollTop = logOutput.scrollHeight; } } // File handling const dropZone = document.getElementById('dropZone'); const fileInput = document.getElementById('fileInput'); const fileInfo = document.getElementById('fileInfo'); const fileName = document.getElementById('fileName'); const clearFile = document.getElementById('clearFile'); const xyzInput = document.getElementById('xyzInput'); dropZone.addEventListener('click', () => fileInput.click()); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('border-blue-500', 'bg-blue-50'); dropZone.classList.remove('border-gray-300'); }); dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('border-blue-500', 'bg-blue-50'); dropZone.classList.add('border-gray-300'); }); dropZone.addEventListener('drop', async (e) => { e.preventDefault(); dropZone.classList.remove('border-blue-500', 'bg-blue-50'); dropZone.classList.add('border-gray-300'); const file = e.dataTransfer.files[0]; if (file) await handleFile(file); }); fileInput.addEventListener('change', async (e) => { const file = e.target.files[0]; if (file) await handleFile(file); }); // Handle XYZ input changes xyzInput.addEventListener('input', () => { const xyzText = xyzInput.value.trim(); if (xyzText) { loadMoleculeFromXYZ(xyzText); } }); clearFile.addEventListener('click', () => { currentMolecule = null; currentXYZData = null; currentResults = null; fileInput.value = ''; xyzInput.value = ''; fileInfo.classList.add('hidden'); updateStatus('Upload an XYZ file or paste coordinates to begin.'); // Hide all panels document.getElementById('methodPanel').classList.add('hidden'); document.getElementById('scfPanel').classList.add('hidden'); document.getElementById('runPanel').classList.add('hidden'); document.getElementById('tabNavigation').classList.add('hidden'); document.getElementById('moleculeHeaderInfo').classList.add('hidden'); // Clear results document.getElementById('energyResults').classList.add('hidden'); document.getElementById('wavefunctionExport').classList.add('hidden'); document.getElementById('workerProperties').classList.add('hidden'); document.getElementById('cubeGeneration').classList.add('hidden'); }); async function handleFile(file) { const extension = file.name.split('.').pop().toLowerCase(); if (extension !== 'xyz') { showError('Please upload an XYZ file.'); return; } fileName.textContent = file.name; fileInfo.classList.remove('hidden'); updateStatus('Reading XYZ file...'); try { const xyzContent = await file.text(); xyzInput.value = xyzContent; await loadMoleculeFromXYZ(xyzContent); } catch (error) { console.error('Error loading XYZ file:', error); showError('Failed to load XYZ file: ' + error.message); } } async function loadMoleculeFromXYZ(xyzContent) { try { updateStatus('Validating XYZ data...'); currentXYZData = xyzContent; // Parse XYZ to get basic info for display const lines = xyzContent.trim().split('\n'); const numAtoms = parseInt(lines[0]); const moleculeName = lines[1] || 'Loaded Molecule'; // Count atoms by element const elementCounts = new Map(); for (let i = 2; i < 2 + numAtoms; i++) { const parts = lines[i].trim().split(/\s+/); const element = parts[0]; elementCounts.set(element, (elementCounts.get(element) || 0) + 1); } // Update display const formula = Array.from(elementCounts.entries()) .map(([elem, count]) => count > 1 ? `${elem}${count}` : elem) .join(''); document.getElementById('moleculeName').textContent = moleculeName; document.getElementById('moleculeFormula').textContent = `${formula} (${numAtoms} atoms)`; document.getElementById('moleculeHeaderInfo').classList.remove('hidden'); updateStatus(`Molecule loaded: ${numAtoms} atoms.`); // Show panels document.getElementById('methodPanel').classList.remove('hidden'); document.getElementById('scfPanel').classList.remove('hidden'); document.getElementById('runPanel').classList.remove('hidden'); document.getElementById('tabNavigation').classList.remove('hidden'); currentMolecule = { numAtoms, name: moleculeName }; } catch (error) { console.error('Error parsing XYZ:', error); showError('Failed to parse XYZ data: ' + error.message); currentMolecule = null; currentXYZData = null; } } // Calculate button document.getElementById('calculateBtn').addEventListener('click', async () => { if (!currentXYZData) { showError('Please load a molecule first.'); return; } if (!worker) { showError('Web Worker not initialized.'); return; } // Clear previous results document.getElementById('energyResults').classList.add('hidden'); document.getElementById('wavefunctionExport').classList.add('hidden'); document.getElementById('workerProperties').classList.add('hidden'); // Clear log and switch to output tab document.getElementById('logOutput').innerHTML = ''; switchTab('output'); // Update UI state isCalculating = true; document.getElementById('calculateBtn').disabled = true; document.getElementById('calculateBtn').textContent = 'Calculating...'; document.getElementById('cancelBtn').classList.remove('hidden'); // Set log level const logLevel = parseInt(document.getElementById('logLevel').value); worker.postMessage({ type: 'setLogLevel', data: { level: logLevel } }); // Set number of threads const numThreads = parseInt(document.getElementById('numThreads').value); worker.postMessage({ type: 'setNumThreads', data: { numThreads: numThreads } }); // Get calculation parameters const params = { xyzData: currentXYZData, method: document.getElementById('method').value, basisSet: document.getElementById('basisSet').value, maxIterations: parseInt(document.getElementById('maxIterations').value), energyTolerance: parseFloat(document.getElementById('energyTolerance').value), numThreads: numThreads }; updateStatus('Sending calculation to Web Worker...'); // Send calculation request to worker worker.postMessage({ type: 'calculate', data: params }); }); // Cancel button document.getElementById('cancelBtn').addEventListener('click', () => { if (worker && isCalculating) { // Terminate and restart worker worker.terminate(); updateStatus('Calculation cancelled. Restarting worker...'); initializeWorker(); isCalculating = false; document.getElementById('calculateBtn').disabled = false; document.getElementById('calculateBtn').textContent = 'Run Calculation'; document.getElementById('cancelBtn').classList.add('hidden'); } }); function displayResults(results) { const energyDetails = document.getElementById('energyDetails'); const timingInfo = document.getElementById('timingInfo'); const propertyDetails = document.getElementById('propertyDetails'); try { // Display energy results let energyHTML = `<div><strong>Total Energy:</strong> ${results.energy.toFixed(8)} Eh</div>`; energyHTML += `<div><strong>Total Energy:</strong> ${results.energyInEV.toFixed(6)} eV</div>`; energyDetails.innerHTML = energyHTML; // Format timing information const seconds = Math.floor(results.elapsedMs / 1000); const milliseconds = Math.round(results.elapsedMs % 1000); timingInfo.innerHTML = `<p>Calculation completed in ${seconds}s ${milliseconds}ms</p>`; // Show energy results document.getElementById('energyResults').classList.remove('hidden'); // Display properties if available if (results.properties) { let propHTML = ''; if (results.properties.homo !== undefined) { propHTML += `<div><strong>HOMO:</strong> ${results.properties.homo.toFixed(6)} Eh (${(results.properties.homo * 27.2114).toFixed(3)} eV)</div>`; } if (results.properties.lumo !== undefined) { propHTML += `<div><strong>LUMO:</strong> ${results.properties.lumo.toFixed(6)} Eh (${(results.properties.lumo * 27.2114).toFixed(3)} eV)</div>`; } if (results.properties.gap !== undefined) { propHTML += `<div><strong>HOMO-LUMO Gap:</strong> ${results.properties.gap.toFixed(6)} Eh (${(results.properties.gap * 27.2114).toFixed(3)} eV)</div>`; } if (propHTML) { propertyDetails.innerHTML = propHTML; document.getElementById('workerProperties').classList.remove('hidden'); } } // Show wavefunction export if data available if (results.wavefunctionData) { document.getElementById('wavefunctionExport').classList.remove('hidden'); document.getElementById('cubeGeneration').classList.remove('hidden'); // Update MO index hint based on results if (results.wavefunctionData) { const nAlpha = results.wavefunctionData.numAlphaElectrons; const nBeta = results.wavefunctionData.numBetaElectrons; // For closed-shell, both are equal and HOMO is the highest occupied orbital // For open-shell, we need to consider both alpha and beta const homoAlphaIndex = nAlpha - 1; const homoBetaIndex = nBeta - 1; const lumoAlphaIndex = nAlpha; const lumoBetaIndex = nBeta; if (nAlpha === nBeta) { // Closed shell document.getElementById('moIndexHint').textContent = `HOMO: ${homoAlphaIndex}, LUMO: ${lumoAlphaIndex}`; document.getElementById('moIndex').value = homoAlphaIndex; } else { // Open shell document.getElementById('moIndexHint').textContent = `HOMO α: ${homoAlphaIndex}, HOMO β: ${homoBetaIndex}, LUMO α: ${lumoAlphaIndex}, LUMO β: ${lumoBetaIndex}`; document.getElementById('moIndex').value = homoAlphaIndex; } } } // Show matrix section if matrices available if (results.matrices) { document.getElementById('matrixSection').classList.remove('hidden'); document.getElementById('orbitalSection').classList.remove('hidden'); updateMatrixDisplay(); updateOrbitalDisplay(); } } catch (error) { console.error('Error displaying results:', error); showError('Error displaying results: ' + error.message); } } function updateMatrixDisplay() { const matrixSelect = document.getElementById('matrixSelect'); const currentMatrix = matrixSelect.value; displayMatrix(currentMatrix); } function displayMatrix(matrixType) { if (!currentResults || !currentResults.matrices) { document.getElementById('matrixDisplay').textContent = 'No matrix data available. Run a calculation first.'; return; } try { const matrixDisplay = document.getElementById('matrixDisplay'); const matrixStats = document.getElementById('matrixStats'); const exportBtn = document.getElementById('exportMatrixBtn'); let matrixData; let matrixName; // Get matrix data from results switch(matrixType) { case 'overlap': matrixName = 'Overlap Matrix (S)'; matrixData = currentResults.matrices.overlap; break; case 'kinetic': matrixName = 'Kinetic Energy Matrix (T)'; matrixData = currentResults.matrices.kinetic; break; case 'nuclear': matrixName = 'Nuclear Attraction Matrix (V)'; matrixData = currentResults.matrices.nuclear; break; case 'fock': matrixName = 'Fock Matrix (F)'; matrixData = currentResults.matrices.fock; break; case 'density': matrixName = 'Density Matrix (P)'; matrixData = currentResults.matrices.density; break; case 'coefficients': matrixName = 'MO Coefficients (C)'; matrixData = currentResults.matrices.coefficients; break; default: matrixDisplay.textContent = 'Unknown matrix type'; return; } if (!matrixData) { matrixDisplay.textContent = `${matrixName} not available`; exportBtn.classList.add('hidden'); return; } const rows = matrixData.rows; const cols = matrixData.cols; // Update stats matrixStats.innerHTML = `<strong>${matrixName}</strong><br>Dimensions: ${rows} × ${cols}`; // Create scrollable table display let matrixHTML = `<div style="padding: 8px; font-size: 12px; color: #666; border-bottom: 1px solid #dee2e6;">Matrix size: ${rows}×${cols} (scroll to see all elements)</div>`; // Start building HTML table matrixHTML += '<div style="padding: 8px;"><table class="matrix-table">'; // Add column headers matrixHTML += '<thead><tr><th>Row\\Col</th>'; for (let j = 0; j < cols; j++) { matrixHTML += `<th>${j}</th>`; } matrixHTML += '</tr></thead><tbody>'; // Add all data rows for (let i = 0; i < rows; i++) { matrixHTML += '<tr>'; matrixHTML += `<th class="row-header">${i}</th>`; for (let j = 0; j < cols; j++) { const value = matrixData.data[i][j]; const formattedValue = Math.abs(value) < 1e-10 ? '0.000000' : value.toFixed(6); matrixHTML += `<td>${formattedValue}</td>`; } matrixHTML += '</tr>'; } matrixHTML += '</tbody></table></div>'; matrixDisplay.innerHTML = matrixHTML; exportBtn.classList.remove('hidden'); // Store current matrix data for export e