@peterspackman/occjs
Version:
JavaScript/WebAssembly bindings for OCC - a quantum chemistry and crystallography library
337 lines (296 loc) • 10.3 kB
JavaScript
/**
* Core quantum chemistry classes and utilities
*/
/**
* SCF convergence settings
*/
export class SCFSettings {
constructor() {
this.maxIterations = 100;
this.energyTolerance = 1e-8;
this.densityTolerance = 1e-6;
this.initialGuess = 'core';
this.diis = true;
this.diisSize = 8;
}
setMaxIterations(max) {
this.maxIterations = max;
return this;
}
setEnergyTolerance(tol) {
this.energyTolerance = tol;
return this;
}
setDensityTolerance(tol) {
this.densityTolerance = tol;
return this;
}
setInitialGuess(guess) {
this.initialGuess = guess;
return this;
}
setDIIS(enabled, size = 8) {
this.diis = enabled;
this.diisSize = size;
return this;
}
}
/**
* Quantum chemistry calculation wrapper
*/
export class QMCalculation {
constructor(molecule, basis, module) {
this.molecule = molecule;
this.basis = basis;
this.module = module;
this.wavefunction = null;
this.energy = null;
this.method = null;
this.properties = new Map();
// Store C++ objects to prevent premature garbage collection
this._cppProcedure = null; // HartreeFock or DFT object
this._cppScf = null; // SCF object
}
/**
* Run Hartree-Fock SCF calculation
* @param {SCFSettings|Object} settings - SCF settings
* @returns {Promise<number>} SCF energy
*/
async runHF(settings = {}) {
const scfSettings = settings instanceof SCFSettings ? settings : new SCFSettings();
if (!(settings instanceof SCFSettings)) {
Object.assign(scfSettings, settings);
}
const hf = new this.module.HartreeFock(this.basis);
// Configure precision if specified
if (settings.precision) {
hf.setPrecision(settings.precision);
}
// Create SCF procedure
const spinKind = settings.unrestricted ?
this.module.SpinorbitalKind.Unrestricted :
this.module.SpinorbitalKind.Restricted;
const scf = new this.module.HartreeFockSCF(hf, spinKind);
// Set charge and multiplicity
scf.setChargeMultiplicity(this.molecule.charge(), this.molecule.multiplicity());
// Configure SCF convergence settings
const convergenceSettings = scf.convergenceSettings;
if (scfSettings.energyTolerance) {
convergenceSettings.energyThreshold = scfSettings.energyTolerance;
}
if (scfSettings.commutatorTolerance) {
convergenceSettings.commutatorThreshold = scfSettings.commutatorTolerance;
}
this.energy = scf.run();
this.wavefunction = scf.wavefunction();
this.method = 'HF';
return this.energy;
}
/**
* Run DFT calculation
* @param {string} functional - DFT functional name
* @param {Object} options - DFT options including SCF settings
* @returns {Promise<number>} DFT energy
*/
async runDFT(functional, options = {}) {
// Let C++ handle functional validation - just pass the name directly
const dft = new this.module.DFT(functional, this.basis);
// Configure precision if specified
if (options.precision) {
dft.setPrecision(options.precision);
}
// Create SCF procedure
const spinKind = options.unrestricted ?
this.module.SpinorbitalKind.Unrestricted :
this.module.SpinorbitalKind.Restricted;
const scf = new this.module.KohnShamSCF(dft, spinKind);
// Set charge and multiplicity
scf.setChargeMultiplicity(this.molecule.charge(), this.molecule.multiplicity());
// Configure SCF convergence settings
if (options.scfSettings) {
const convergenceSettings = scf.convergenceSettings;
if (options.scfSettings.energyTolerance) {
convergenceSettings.energyThreshold = options.scfSettings.energyTolerance;
}
}
this.energy = scf.run();
this.wavefunction = scf.wavefunction();
this.method = `DFT/${functional}`; // Use the functional name as provided
return this.energy;
}
/**
* Run a post-HF correlation calculation (MP2 / CCSD / CCSD(T)) on the
* converged SCF wavefunction. Backend and auxiliary basis are resolved
* exactly like the occ CLI.
* @param {string} method - e.g. 'mp2', 'ri-mp2', 'ccsd', 'ccsd(t)'
* @param {Object} options - optional overrides: backend, auxBasis,
* spinScaling, nFrozen, maxMemoryGb, maxCycle, tol
* @returns {Promise<Object>} correlation result (totalEnergy,
* correlationEnergy, triplesCorrection, ...)
*/
async runCorrelation(method = 'mp2', options = {}) {
if (!this.wavefunction) {
throw new Error('Correlation methods require a reference wavefunction. Run HF or DFT first.');
}
const opts = new this.module.CorrelationOptions();
opts.method = method;
if (options.backend !== undefined) {
opts.backend = options.backend;
}
if (options.auxBasis !== undefined) {
opts.auxBasis = options.auxBasis;
}
if (options.spinScaling !== undefined) {
opts.spinScaling = options.spinScaling;
}
if (options.nFrozen !== undefined) {
opts.nFrozen = options.nFrozen;
}
if (options.maxMemoryGb !== undefined) {
opts.maxMemoryGb = options.maxMemoryGb;
}
if (options.maxCycle !== undefined) {
opts.maxCycle = options.maxCycle;
}
if (options.tol !== undefined) {
opts.tol = options.tol;
}
const result = this.module.runCorrelationWithOptions(this.wavefunction, opts);
this.energy = result.totalEnergy;
this.method = result.method;
return {
method: result.method,
scfEnergy: result.scfEnergy,
correlationEnergy: result.correlationEnergy,
totalEnergy: result.totalEnergy,
sameSpin: result.sameSpin,
oppositeSpin: result.oppositeSpin,
scaledCorrelation: result.scaledCorrelation,
ccsdCorrelation: result.ccsdCorrelation,
triplesCorrection: result.triplesCorrection,
iterations: result.iterations,
converged: result.converged,
nFrozen: result.nFrozen,
};
}
/**
* Run MP2 calculation
* @param {Object} options - MP2 options (backend, auxBasis, spinScaling, nFrozen, ...)
* @returns {Promise<number>} MP2 total energy
*/
async runMP2(options = {}) {
const result = await this.runCorrelation(options.method || 'mp2', options);
return result.totalEnergy;
}
/**
* Run CCSD or CCSD(T)
* @param {Object} options - options (triples: true for CCSD(T), backend, nFrozen, ...)
* @returns {Promise<Object>} correlation result
*/
async runCCSD(options = {}) {
const method = options.triples ? 'ccsd(t)' : 'ccsd';
return this.runCorrelation(method, options);
}
/**
* Calculate molecular properties
* @param {Array<string>} properties - List of properties to calculate
* @returns {Promise<Object>} Calculated properties
*/
async calculateProperties(properties) {
if (!this.wavefunction) {
throw new Error('Properties calculation requires a wavefunction. Run SCF first.');
}
const results = {};
for (const prop of properties) {
switch (prop.toLowerCase()) {
case 'mulliken':
results.mulliken = this.wavefunction.mullikenCharges();
this.properties.set('mulliken', results.mulliken);
break;
case 'energy':
results.energy = this.energy;
break;
case 'orbitals':
results.orbitals = {
coefficients: this.wavefunction.coefficients(),
energies: this.wavefunction.orbitalEnergies(),
occupations: this.wavefunction.occupations()
};
this.properties.set('orbitals', results.orbitals);
break;
case 'homo':
results.homo = this.wavefunction.homoEnergy();
break;
case 'lumo':
results.lumo = this.wavefunction.lumoEnergy();
break;
case 'gap':
results.gap = this.wavefunction.lumoEnergy() - this.wavefunction.homoEnergy();
break;
default:
console.warn(`Unknown property: ${prop}`);
}
}
return results;
}
/**
* Export wavefunction to various formats
* @param {string} format - Export format ('json', 'molden')
* @returns {string} Exported wavefunction data
*/
exportWavefunction(format = 'molden') {
if (!this.wavefunction) {
throw new Error('No wavefunction to export. Run SCF calculation first.');
}
// Use the unified exportToString method that takes format as parameter
return this.wavefunction.exportToString(format.toLowerCase());
}
/**
* Get calculation summary
* @returns {Object} Summary of calculation results
*/
getSummary() {
return {
method: this.method,
energy: this.energy,
molecule: {
formula: this.molecule.name(),
natoms: this.molecule.size(),
charge: this.molecule.charge(),
multiplicity: this.molecule.multiplicity()
},
basis: this.basis ? this.basis.name() : 'Unknown',
converged: this.energy !== null,
properties: Object.fromEntries(this.properties)
};
}
}
/**
* Load basis set for a molecule
* @param {Object} module - OCC module
* @param {Object} molecule - Molecule object
* @param {string} basisName - Basis set name
* @param {Object} options - Loading options
* @returns {Object} Loaded basis set
*/
export function loadBasisSet(module, molecule, basisName, options = {}) {
// If JSON basis data is provided, use fromJson method
if (options.json) {
const jsonString = typeof options.json === 'string' ? options.json : JSON.stringify(options.json);
return module.AOBasis.fromJson(molecule.atoms(), jsonString);
}
// Otherwise, load by name
return module.AOBasis.load(molecule.atoms(), basisName);
}
/**
* Create a QM calculation object
* @param {Object} molecule - Molecule object
* @param {string} basisName - Basis set name
* @param {Object} options - Creation options
* @param {Object} module - OCC module
* @returns {Promise<QMCalculation>} QM calculation object
*/
export function createQMCalculation(molecule, basisName, options = {}, module) {
const basis = loadBasisSet(module, molecule, basisName, options);
return new QMCalculation(molecule, basis, module);
}