@peterspackman/occjs
Version:
JavaScript/WebAssembly bindings for OCC - a quantum chemistry and crystallography library
344 lines (296 loc) • 10.2 kB
JavaScript
/**
* High-level optimization utilities for OCC.js
*
* This module provides convenient functions for molecular geometry optimization
* and vibrational analysis, similar to the Python interface.
*/
/**
* Perform geometry optimization using Hartree-Fock method
* @param {Object} module - The loaded OCC.js module
* @param {Object} molecule - Initial molecule geometry
* @param {string} basisName - Basis set name (e.g., "3-21G", "STO-3G")
* @param {Object} options - Optimization options
* @param {Object} options.criteria - Convergence criteria
* @param {number} options.maxSteps - Maximum optimization steps
* @param {function} options.onStep - Callback for each step
* @returns {Promise<Object>} Optimization result
*/
export async function optimizeHF(module, molecule, basisName = "3-21G", options = {}) {
const {
criteria = null,
maxSteps = 25,
onStep = null
} = options;
// Set up default convergence criteria if not provided
const optCriteria = criteria || (() => {
const c = new module.ConvergenceCriteria();
c.gradientMax = 1e-4;
c.gradientRms = 1e-5;
c.stepMax = 1e-3;
c.stepRms = 1e-4;
return c;
})();
// Create optimizer
const optimizer = new module.BernyOptimizer(molecule, optCriteria);
// Storage for optimization trajectory
const trajectory = {
energies: [],
gradientNorms: [],
geometries: [],
converged: false,
steps: 0,
finalEnergy: null,
finalMolecule: null
};
let converged = false;
for (let step = 0; step < maxSteps; step++) {
// Get current geometry
const currentMol = optimizer.getNextGeometry();
trajectory.geometries.push(currentMol);
// Create calculation for current geometry
const basis = module.AOBasis.load(currentMol.atoms(), basisName);
const hf = new module.HartreeFock(basis);
// Run SCF calculation
const scf = new module.HartreeFockSCF(hf);
scf.setChargeMultiplicity(0, 1); // Neutral singlet
const energy = await scf.run();
// Compute gradient
const wfn = scf.wavefunction();
const gradient = hf.computeGradient(wfn.molecularOrbitals);
// Store progress
trajectory.energies.push(energy);
const gradNorm = Math.sqrt(gradient.squaredNorm());
trajectory.gradientNorms.push(gradNorm);
// Update optimizer
optimizer.update(energy, gradient);
// Call user callback if provided
if (onStep) {
onStep({
step: step + 1,
energy,
gradientNorm: gradNorm,
molecule: currentMol,
optimizer
});
}
// Check convergence
if (optimizer.step()) {
converged = true;
trajectory.converged = true;
trajectory.steps = step + 1;
break;
}
}
// Get final results
trajectory.finalMolecule = optimizer.getNextGeometry();
trajectory.finalEnergy = optimizer.currentEnergy();
trajectory.converged = converged;
trajectory.steps = converged ? trajectory.steps : maxSteps;
return trajectory;
}
/**
* Perform geometry optimization using DFT method
* @param {Object} module - The loaded OCC.js module
* @param {Object} molecule - Initial molecule geometry
* @param {string} functional - DFT functional (e.g., "b3lyp", "pbe")
* @param {string} basisName - Basis set name
* @param {Object} options - Optimization options
* @returns {Promise<Object>} Optimization result
*/
export async function optimizeDFT(module, molecule, functional = "b3lyp", basisName = "3-21G", options = {}) {
const {
criteria = null,
maxSteps = 25,
onStep = null
} = options;
// Set up default convergence criteria if not provided
const optCriteria = criteria || (() => {
const c = new module.ConvergenceCriteria();
c.gradientMax = 1e-4;
c.gradientRms = 1e-5;
c.stepMax = 1e-3;
c.stepRms = 1e-4;
return c;
})();
// Create optimizer
const optimizer = new module.BernyOptimizer(molecule, optCriteria);
// Storage for optimization trajectory
const trajectory = {
energies: [],
gradientNorms: [],
geometries: [],
converged: false,
steps: 0,
finalEnergy: null,
finalMolecule: null
};
let converged = false;
for (let step = 0; step < maxSteps; step++) {
// Get current geometry
const currentMol = optimizer.getNextGeometry();
trajectory.geometries.push(currentMol);
// Create calculation for current geometry
const basis = module.AOBasis.load(currentMol.atoms(), basisName);
const dft = new module.DFT(functional, basis);
// Run SCF calculation
const scf = new module.KohnShamSCF(dft);
scf.setChargeMultiplicity(0, 1); // Neutral singlet
const energy = await scf.run();
// Compute gradient
const wfn = scf.wavefunction();
const gradient = dft.computeGradient(wfn.molecularOrbitals);
// Store progress
trajectory.energies.push(energy);
const gradNorm = Math.sqrt(gradient.squaredNorm());
trajectory.gradientNorms.push(gradNorm);
// Update optimizer
optimizer.update(energy, gradient);
// Call user callback if provided
if (onStep) {
onStep({
step: step + 1,
energy,
gradientNorm: gradNorm,
molecule: currentMol,
optimizer
});
}
// Check convergence
if (optimizer.step()) {
converged = true;
trajectory.converged = true;
trajectory.steps = step + 1;
break;
}
}
// Get final results
trajectory.finalMolecule = optimizer.getNextGeometry();
trajectory.finalEnergy = optimizer.currentEnergy();
trajectory.converged = converged;
trajectory.steps = converged ? trajectory.steps : maxSteps;
return trajectory;
}
/**
* Compute vibrational frequencies at optimized geometry
* @param {Object} module - The loaded OCC.js module
* @param {Object} molecule - Optimized molecule
* @param {string} method - Method type ("HF" or "DFT")
* @param {string} functional - DFT functional (ignored for HF)
* @param {string} basisName - Basis set name
* @param {Object} options - Frequency calculation options
* @returns {Promise<Object>} Vibrational analysis result
*/
export async function computeFrequencies(module, molecule, method = "HF", functional = "b3lyp", basisName = "3-21G", options = {}) {
const {
stepSize = 0.005,
useAcousticSumRule = true,
projectTransRot = true
} = options;
// Set up calculation at optimized geometry
const basis = module.AOBasis.load(molecule.atoms(), basisName);
let calculator, scf, hessEvaluator;
let scfEnergy;
if (method.toUpperCase() === "HF") {
calculator = new module.HartreeFock(basis);
scf = new module.HartreeFockSCF(calculator);
scf.setChargeMultiplicity(0, 1);
scfEnergy = await scf.run();
hessEvaluator = calculator.hessianEvaluator();
} else if (method.toUpperCase() === "DFT") {
calculator = new module.DFT(functional, basis);
scf = new module.KohnShamSCF(calculator);
scf.setChargeMultiplicity(0, 1);
scfEnergy = await scf.run();
hessEvaluator = calculator.hessianEvaluator();
} else {
throw new Error(`Unknown method: ${method}. Use "HF" or "DFT"`);
}
// Configure Hessian evaluator
hessEvaluator.setStepSize(stepSize);
hessEvaluator.setUseAcousticSumRule(useAcousticSumRule);
// Compute Hessian
const wfn = scf.wavefunction();
const hessian = hessEvaluator.compute(wfn.molecularOrbitals);
// Compute vibrational modes
const vibrationalModes = module.computeVibrationalModesFromMolecule(hessian, molecule, projectTransRot);
// Extract frequency data
const frequencies = vibrationalModes.getAllFrequencies();
const freqArray = [];
for (let i = 0; i < frequencies.size(); i++) {
freqArray.push(frequencies.get(i));
}
return {
modes: vibrationalModes,
frequencies: freqArray,
nModes: vibrationalModes.nModes(),
nAtoms: vibrationalModes.nAtoms(),
summary: vibrationalModes.summaryString(),
frequenciesString: vibrationalModes.frequenciesString(),
scfEnergy: scfEnergy
};
}
/**
* Complete workflow: optimize geometry and compute frequencies
* @param {Object} module - The loaded OCC.js module
* @param {Object} molecule - Initial molecule geometry
* @param {Object} options - Combined optimization and frequency options
* @returns {Promise<Object>} Complete result with optimization and frequencies
*/
export async function optimizeAndAnalyze(module, molecule, options = {}) {
const {
method = "HF",
functional = "b3lyp",
basisName = "3-21G",
optimization = {},
frequencies = {}
} = options;
// First perform optimization
let optimizationResult;
if (method.toUpperCase() === "HF") {
optimizationResult = await optimizeHF(module, molecule, basisName, optimization);
} else if (method.toUpperCase() === "DFT") {
optimizationResult = await optimizeDFT(module, molecule, functional, basisName, optimization);
} else {
throw new Error(`Unknown method: ${method}. Use "HF" or "DFT"`);
}
if (!optimizationResult.converged) {
console.warn("Optimization did not converge - frequencies may not be meaningful");
}
// Then compute frequencies at optimized geometry
const frequencyResult = await computeFrequencies(
module,
optimizationResult.finalMolecule,
method,
functional,
basisName,
frequencies
);
return {
optimization: optimizationResult,
frequencies: frequencyResult,
finalMolecule: optimizationResult.finalMolecule,
finalEnergy: optimizationResult.finalEnergy,
converged: optimizationResult.converged
};
}
/**
* Export molecule to XYZ format with optional comment
* @param {Object} module - The loaded OCC.js module
* @param {Object} molecule - Molecule to export
* @param {string} comment - Optional comment line
* @returns {string} XYZ format string
*/
export function moleculeToXYZ(module, molecule, comment = "") {
if (comment) {
return module.moleculeToXYZWithComment(molecule, comment);
} else {
return module.moleculeToXYZ(molecule);
}
}
export default {
optimizeHF,
optimizeDFT,
computeFrequencies,
optimizeAndAnalyze,
moleculeToXYZ
};