@peterspackman/occjs
Version:
JavaScript/WebAssembly bindings for OCC - a quantum chemistry and crystallography library
953 lines (838 loc) • 65.3 kB
HTML
<!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 Water molecule O 0.000000 0.000000 0.000000 H 0.757000 0.586000 0.000000 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>
</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 }
});
// 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)
};
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
exportBtn.onclick = () => exportMatrix(matrixData, matrixName);
} catch (error) {
console.error('Error displaying matrix:', error);
document.getElementById('matrixDisplay').textContent = 'Error loading matrix: ' + error.message;
}
}
function updateOrbitalDisplay() {
if (!currentResults) {
document.getElementById('energyList').innerHTML = 'No results available.';
return;
}
try {
const energyList = document.getElementById('energyList');
let energyHTML = '';
// Display HOMO/LUMO info if available
if (currentResults.properties) {