@peterspackman/occjs
Version:
JavaScript/WebAssembly bindings for OCC - a quantum chemistry and crystallography library
519 lines (442 loc) • 25.7 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DMA Calculator - Distributed Multipole Analysis</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; }
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8 max-w-6xl">
<!-- Header -->
<div class="text-center mb-8">
<h1 class="text-4xl font-bold text-gray-800 mb-2">DMA Calculator</h1>
<p class="text-gray-600">Distributed Multipole Analysis from Wavefunction Files</p>
</div>
<!-- Main Content -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Left Column: Upload and Settings -->
<div class="space-y-6">
<!-- File Upload -->
<div class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">Upload Wavefunction File</h2>
<div id="dropZone" class="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer hover:border-gray-400 transition-colors">
<svg class="mx-auto h-12 w-12 text-gray-400 mb-3" 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 mb-2">Drop your wavefunction file here or click to browse</p>
<p class="text-sm text-gray-500">Supported formats: .fchk, .molden, .json (OCC format)</p>
<p class="text-xs text-red-500 mt-2">Limit: 250 basis functions</p>
<input type="file" id="fileInput" class="hidden" accept=".fchk,.molden,.json" />
</div>
<div id="fileInfo" class="mt-4 hidden">
<p class="text-sm text-gray-600">Selected file: <span id="fileName" class="font-medium"></span></p>
<button id="clearFile" class="mt-2 text-sm text-red-600 hover:text-red-700">Clear file</button>
</div>
</div>
<!-- Settings (Hidden until file is loaded) -->
<div id="settingsPanel" class="bg-white rounded-lg shadow-md p-6 hidden">
<h2 class="text-xl font-semibold mb-4">DMA Settings</h2>
<!-- Calculate Button at the top -->
<button id="calculateBtn" class="mb-6 w-full bg-blue-600 text-white py-3 px-4 rounded-md hover:bg-blue-700 transition-colors font-medium text-lg">
Calculate DMA
</button>
<!-- Molecule Info -->
<div id="moleculeInfo" class="mb-6 p-4 bg-blue-50 rounded-lg">
<h3 class="text-sm font-semibold text-blue-900 mb-2">Molecule Information</h3>
<div id="moleculeDetails" class="text-sm text-blue-800"></div>
</div>
<!-- General Settings -->
<div class="mb-6">
<h3 class="text-lg font-medium mb-3">General Settings</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Maximum Rank</label>
<input type="number" id="maxRank" min="0" max="10" value="4" 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">Higher ranks capture more detailed multipole moments</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Big Exponent</label>
<input type="number" id="bigExponent" min="0" max="10" step="0.1" value="4.0" 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">Switch parameter for multipole expansion</p>
</div>
<div>
<label class="flex items-center">
<input type="checkbox" id="includeNuclei" checked class="mr-2 h-4 w-4 text-blue-600 rounded">
<span class="text-sm font-medium text-gray-700">Include Nuclear Contributions</span>
</label>
</div>
</div>
</div>
<!-- Atom-specific Settings -->
<div>
<h3 class="text-lg font-medium mb-3">Atom-specific Settings</h3>
<p class="text-sm text-gray-600 mb-4">Configure multipole settings for each atom type in your molecule</p>
<div id="atomSettings" class="space-y-4">
<!-- Dynamically populated based on molecule -->
</div>
</div>
</div>
</div>
<!-- Right Column: Results -->
<div class="space-y-6">
<!-- Status/Info -->
<div id="statusPanel" class="bg-white rounded-lg shadow-md p-6">
<h2 class="text-xl font-semibold mb-4">Status</h2>
<div id="statusContent" class="text-gray-600">
<p>No wavefunction loaded. Please upload a file to begin.</p>
</div>
</div>
<!-- Results -->
<div id="resultsPanel" class="bg-white rounded-lg shadow-md p-6 hidden">
<!-- Loading State -->
<div id="loadingState" class="hidden">
<div class="flex items-center justify-center py-12">
<div class="text-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p class="text-gray-700">Calculating DMA...</p>
</div>
</div>
</div>
<!-- Results Content -->
<div id="resultsContent" class="hidden">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-semibold">DMA Results</h2>
<button id="downloadBtn" class="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 transition-colors text-sm">
Download Punch File
</button>
</div>
<!-- Summary -->
<div id="resultsSummary" class="mb-4 p-4 bg-gray-50 rounded">
<p class="text-sm text-gray-700"></p>
</div>
<!-- Timing Info -->
<div id="timingInfo" class="mb-4 text-sm text-gray-600"></div>
<!-- Punch File Content -->
<div>
<h3 class="font-medium mb-2">Punch File Content</h3>
<div class="relative">
<pre id="punchContent" class="code-block text-xs bg-gray-900 text-gray-100 p-4 rounded overflow-x-auto max-h-96"></pre>
<button id="copyBtn" class="absolute top-2 right-2 bg-gray-700 text-gray-300 px-3 py-1 rounded text-xs hover:bg-gray-600 transition-colors">
Copy
</button>
</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">
// Try to load from npm package first, fallback to local build
let OCC = null;
async function loadOCCModule() {
try {
// First try to load from npm package (e.g., via CDN or bundler)
const module = await import('@peterspackman/occjs');
console.log('Loaded OCC from npm package');
return module;
} catch (e) {
console.log('Failed to load from npm, trying local build...');
try {
// Fallback to local build
const module = await import('../dist/index.browser.js');
console.log('Loaded OCC from local build');
return module;
} catch (e2) {
console.error('Failed to load OCC module from both npm and local build');
throw new Error('Could not load OCC module. Please ensure either the npm package is installed or you are running from the OCC repository.');
}
}
}
let currentWavefunction = null;
let currentFileContent = null;
let currentFileType = null;
// Initialize OCC module
async function init() {
console.log('Initializing OCC...');
try {
OCC = await loadOCCModule();
// Determine the correct paths based on how the module was loaded
const isNpmPackage = OCC.default ? true : false;
const wasmPath = isNpmPackage ? undefined : '../dist/occjs.wasm';
const dataPath = isNpmPackage ? undefined : '../dist/occjs.data';
await OCC.loadOCC({
wasmPath: wasmPath,
dataPath: dataPath
});
console.log('OCC loaded successfully');
updateStatus('OCC module loaded. Ready to accept wavefunction files.');
} catch (error) {
console.error('Failed to load OCC module:', error);
showError('Failed to load OCC module: ' + error.message);
}
}
// 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');
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);
});
clearFile.addEventListener('click', () => {
currentWavefunction = null;
currentFileContent = null;
currentFileType = null;
fileInput.value = '';
fileInfo.classList.add('hidden');
updateStatus('No wavefunction loaded. Please upload a file to begin.');
document.getElementById('resultsPanel').classList.add('hidden');
document.getElementById('settingsPanel').classList.add('hidden');
// Show the upload dropzone again
document.getElementById('dropZone').style.display = 'block';
});
async function handleFile(file) {
const extension = file.name.split('.').pop().toLowerCase();
if (!['fchk', 'molden', 'json'].includes(extension)) {
showError('Unsupported file format. Please upload a .fchk, .molden, or .json file.');
return;
}
fileName.textContent = file.name;
fileInfo.classList.remove('hidden');
updateStatus('Reading file...');
try {
currentFileContent = await file.text();
currentFileType = extension === 'json' ? 'json' : extension;
updateStatus('Loading wavefunction...');
currentWavefunction = await OCC.wavefunctionFromString(currentFileContent, currentFileType);
// Check basis function limit
const nbf = currentWavefunction.basis.nbf();
if (nbf > 250) {
showError(`File has ${nbf} basis functions, which exceeds the limit of 250.`);
currentWavefunction = null;
return;
}
updateStatus(`Wavefunction loaded successfully. ${nbf} basis functions, ${currentWavefunction.atoms.size()} atoms.`);
// Hide the upload dropzone
document.getElementById('dropZone').style.display = 'none';
// Update atom settings based on loaded molecule
updateAtomSettings();
// Show settings panel
document.getElementById('settingsPanel').classList.remove('hidden');
} catch (error) {
console.error('Error loading wavefunction:', error);
showError('Failed to load wavefunction: ' + error.message);
currentWavefunction = null;
}
}
function updateAtomSettings() {
if (!currentWavefunction) return;
// Count atoms by element
const elementCounts = new Map();
const elementInfo = new Map();
for (let i = 0; i < currentWavefunction.atoms.size(); i++) {
const atom = currentWavefunction.atoms.get(i);
const symbol = getElementSymbol(atom.atomicNumber);
elementCounts.set(symbol, (elementCounts.get(symbol) || 0) + 1);
elementInfo.set(symbol, atom.atomicNumber);
}
// Update molecule info
const moleculeDetails = document.getElementById('moleculeDetails');
const totalAtoms = currentWavefunction.atoms.size();
const nbf = currentWavefunction.basis.nbf();
let infoHTML = `<div class="grid grid-cols-2 gap-2">`;
infoHTML += `<div><strong>Total atoms:</strong> ${totalAtoms}</div>`;
infoHTML += `<div><strong>Basis functions:</strong> ${nbf}</div>`;
infoHTML += `<div><strong>Composition:</strong> `;
const composition = Array.from(elementCounts.entries())
.map(([elem, count]) => count > 1 ? `${elem}<sub>${count}</sub>` : elem)
.join(' ');
infoHTML += composition + '</div>';
infoHTML += `</div>`;
moleculeDetails.innerHTML = infoHTML;
// Clear existing atom settings
const atomSettings = document.getElementById('atomSettings');
atomSettings.innerHTML = '';
// Default settings for common elements
const defaultSettings = {
'H': { radius: 0.35, maxRank: 1 },
'C': { radius: 0.65, maxRank: 4 },
'N': { radius: 0.60, maxRank: 4 },
'O': { radius: 0.60, maxRank: 4 },
'F': { radius: 0.55, maxRank: 4 },
'S': { radius: 0.70, maxRank: 4 },
'Cl': { radius: 0.70, maxRank: 4 }
};
// Add settings for each unique element
Array.from(elementCounts.entries()).sort().forEach(([element, count]) => {
const defaults = defaultSettings[element] || { radius: 0.65, maxRank: 4 };
const div = document.createElement('div');
div.className = 'atom-setting bg-gray-50 p-4 rounded-lg';
div.dataset.element = element;
div.innerHTML = `
<div class="flex items-center justify-between mb-3">
<div class="flex items-center space-x-3">
<span class="text-lg font-semibold">${element}</span>
<span class="text-sm text-gray-600">(${count} atom${count > 1 ? 's' : ''})</span>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Radius (Å)</label>
<input type="number" class="radius-input w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
min="0" max="2" step="0.05" value="${defaults.radius}">
<p class="text-xs text-gray-500 mt-1">Site expansion radius</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Max Rank</label>
<input type="number" class="limit-input w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
min="0" max="10" value="${defaults.maxRank}">
<p class="text-xs text-gray-500 mt-1">Maximum multipole order</p>
</div>
</div>
`;
atomSettings.appendChild(div);
});
}
function getElementSymbol(atomicNumber) {
const symbols = ['H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne',
'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar', 'K', 'Ca'];
return symbols[atomicNumber - 1] || `Z${atomicNumber}`;
}
// Calculate DMA
document.getElementById('calculateBtn').addEventListener('click', async () => {
if (!currentWavefunction) {
showError('Please upload a wavefunction file first.');
return;
}
// Show results panel with loading state
const resultsPanel = document.getElementById('resultsPanel');
const loadingState = document.getElementById('loadingState');
const resultsContent = document.getElementById('resultsContent');
resultsPanel.classList.remove('hidden');
loadingState.classList.remove('hidden');
resultsContent.classList.add('hidden');
// Start timing
const startTime = performance.now();
try {
// Gather settings
const options = {
maxRank: parseInt(document.getElementById('maxRank').value),
bigExponent: parseFloat(document.getElementById('bigExponent').value),
includeNuclei: document.getElementById('includeNuclei').checked,
atomRadii: {},
atomLimits: {}
};
// Collect atom-specific settings
document.querySelectorAll('.atom-setting').forEach(row => {
const element = row.dataset.element;
const radius = parseFloat(row.querySelector('.radius-input').value);
const limit = parseInt(row.querySelector('.limit-input').value);
options.atomRadii[element] = radius;
options.atomLimits[element] = limit;
});
console.log('Starting DMA calculation with options:', options);
// Use the imported calculateDMA function
const result = await OCC.calculateDMA(currentWavefunction, options);
// Calculate elapsed time
const endTime = performance.now();
const elapsedMs = endTime - startTime;
console.log('DMA calculation completed');
// Display results with timing
await displayResults(result, elapsedMs);
} catch (error) {
console.error('DMA calculation error:', error);
showError('DMA calculation failed: ' + error.message);
resultsPanel.classList.add('hidden');
}
});
async function displayResults(dmaResult, elapsedMs) {
const loadingState = document.getElementById('loadingState');
const resultsContent = document.getElementById('resultsContent');
const resultsSummary = document.getElementById('resultsSummary');
const punchContent = document.getElementById('punchContent');
const timingInfo = document.getElementById('timingInfo');
// Generate summary
const numSites = dmaResult.sites.size();
const maxRank = dmaResult.result.max_rank;
resultsSummary.innerHTML = `
<p class="text-sm text-gray-700">
<strong>Sites:</strong> ${numSites} |
<strong>Max Rank:</strong> ${maxRank} |
<strong>Total Components:</strong> ${numSites * ((maxRank + 1) * (maxRank + 2) / 2)}
</p>
`;
// Format timing information
const seconds = Math.floor(elapsedMs / 1000);
const milliseconds = Math.round(elapsedMs % 1000);
timingInfo.innerHTML = `<p>Calculation completed in ${seconds}s ${milliseconds}ms</p>`;
// Generate and display punch file
const punchFile = await dmaResult.toPunchFile();
punchContent.textContent = punchFile;
// Hide loading, show results
loadingState.classList.add('hidden');
resultsContent.classList.remove('hidden');
// Setup download button
document.getElementById('downloadBtn').onclick = () => {
const blob = new Blob([punchFile], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'dma.punch';
a.click();
URL.revokeObjectURL(url);
};
// Setup copy button
document.getElementById('copyBtn').onclick = () => {
navigator.clipboard.writeText(punchFile).then(() => {
const btn = document.getElementById('copyBtn');
btn.textContent = 'Copied!';
setTimeout(() => { btn.textContent = 'Copy'; }, 2000);
});
};
}
// UI Helper functions
function updateStatus(message) {
document.getElementById('statusContent').innerHTML = `<p>${message}</p>`;
}
function showError(message) {
document.getElementById('errorMessage').textContent = message;
document.getElementById('errorModal').classList.remove('hidden');
}
document.getElementById('closeError').addEventListener('click', () => {
document.getElementById('errorModal').classList.add('hidden');
});
// Initialize on load
init().catch(console.error);
</script>
</body>
</html>