Skip to content

IR Spectroscopy — Numerical Hessian

sci-form computes infrared spectra from first principles via a numerical second-derivative (Hessian) approach. The resulting normal-mode frequencies and dipole derivatives directly yield IR intensities.

Pipeline

Theory

Numerical Hessian via Central Finite Differences

For a molecule with N atoms, the Cartesian Hessian Hij is computed using central finite differences with step size δ:

Hij=2EqiqjE(q+δe^i+δe^j)E(q+δe^iδe^j)E(qδe^i+δe^j)+E(qδe^iδe^j)4δ2

where qi runs over the 3N Cartesian degrees of freedom and e^i are unit displacement vectors. This requires 2×(3N)2 energy evaluations for the full Hessian, reduced to 6N evaluations using the mixed-partial shortcut:

Hij=E(q+δi+δj)E(q+δiδj)E(qδi+δj)+E(qδiδj)4δ2

Default step size: δ=0.01 Å.

Mass-Weighted Hessian

To account for atomic masses in Newton's equations of motion, the Hessian is transformed to mass-weighted coordinates:

H~ij=Hijmimj

where mi is the mass of the atom associated with degree of freedom i.

Normal Mode Diagonalization

The mass-weighted Hessian is diagonalized to obtain eigenvalues λk (in units of eV/(amu·Å²)) and eigenvectors L (normal coordinates):

H~L=ΛL

Vibrational frequencies in cm⁻¹ are obtained from the eigenvalues:

ν~k=12πcFλk1amuÅ2=9.6485×1027λk2πc

where c=2.998×1010 cm/s. Negative eigenvalues (imaginary frequencies) indicate structural instabilities; they are reported with a negative sign by convention.

Translation/Rotation Separation

The 3N modes include 3 translations and 3 (or 2 for linear molecules) rotations. These appear as near-zero frequency modes (|ν| < 10 cm⁻¹) and are excluded from the IR spectrum. The number of real vibrational modes is:

  • 3N6 for non-linear molecules
  • 3N5 for linear molecules

IR Intensities — Dipole Derivatives

The IR intensity of mode k is proportional to the squared gradient of the dipole moment along the normal coordinate:

Ik|μQk|2

Numerically, this is estimated via finite differences of the electronic dipole along each displacement, then projected onto the normal mode:

μαQkiLikμα(q+δi)μα(qδi)2δ

Intensities are reported in km/mol (standard IUPAC unit: 1 km/mol = 10 L mol⁻¹ cm⁻²).

Zero-Point Vibrational Energy (ZPVE)

The ZPVE is summed over real modes:

EZPVE=12khcν~k=12kωk

In eV, using 1 cm⁻¹ = 1.2399 × 10⁻⁴ eV:

EZPVE[eV]=12kν~k×1.2399×104

Lorentzian Broadened Spectrum

The discrete IR peaks are broadened using Lorentzian line shapes:

A(ν~)=kIkγ/π(ν~ν~k)2+γ2

where γ is the half-width at half-maximum (HWHM) in cm⁻¹.

Parameters

compute_vibrational_analysis

ParameterTypeDefaultDescription
elements&[u8]Atomic numbers
positions&[[f64;3]]Cartesian coordinates (Å)
method&str"eht"Semiempirical method: "eht", "pm3", or "xtb"
step_sizeOption<f64>None (→ 0.01 Å)Finite-difference step Δ in Å

compute_ir_spectrum

ParameterTypeDefaultDescription
analysis&VibrationalAnalysisResult from compute_vibrational_analysis
gammaf64Lorentzian HWHM in cm⁻¹ (typical: 10–30)
wn_minf64Low-wavenumber limit (cm⁻¹)
wn_maxf64High-wavenumber limit (cm⁻¹)
n_pointsusizeGrid resolution

Output Types

VibrationalAnalysis

FieldTypeDescription
n_atomsusizeNumber of atoms
modesVec<VibrationalMode>All 3N modes sorted by frequency
n_real_modesusizeReal modes (excluding translation/rotation)
zpve_evf64Zero-point vibrational energy (eV)
methodStringMethod used for Hessian
notesVec<String>Caveats

VibrationalMode

FieldTypeDescription
frequency_cm1f64Frequency (cm⁻¹); negative = imaginary
ir_intensityf64IR intensity (km/mol)
displacementVec<f64>3N displacement vector
is_realboolTrue if real vibrational mode

IrSpectrum

FieldTypeDescription
wavenumbersVec<f64>Wavenumber grid (cm⁻¹)
intensitiesVec<f64>Broadened absorption (km/mol)
peaksVec<IrPeak>Discrete peaks
gammaf64Broadening HWHM (cm⁻¹)
notesVec<String>Caveats

IrPeak

FieldTypeDescription
frequency_cm1f64Peak centre (cm⁻¹)
ir_intensityf64Raw intensity (km/mol)
mode_indexusizeIndex into modes

API

Rust

rust
use sci_form::{embed, compute_vibrational_analysis, compute_ir_spectrum};

let conf = embed("CCO", 42);
let pos: Vec<[f64;3]> = conf.coords.chunks(3).map(|c| [c[0],c[1],c[2]]).collect();

// ~6N energy evaluations (may take a few seconds for large molecules)
let vib = compute_vibrational_analysis(&conf.elements, &pos, "xtb", None).unwrap();
println!("ZPVE: {:.4} eV, {} real modes", vib.zpve_ev, vib.n_real_modes);

// Dominant IR mode
if let Some(peak) = vib.modes.iter()
    .filter(|m| m.is_real)
    .max_by(|a,b| a.ir_intensity.partial_cmp(&b.ir_intensity).unwrap())
{
    println!("Strongest band: {:.1} cm⁻¹  ({:.1} km/mol)", peak.frequency_cm1, peak.ir_intensity);
}

// Broaden into a 600–4000 cm⁻¹ spectrum
let spec = compute_ir_spectrum(&vib, 15.0, 600.0, 4000.0, 1000);
println!("{} wavenumber points, γ = {} cm⁻¹", spec.wavenumbers.len(), spec.gamma);

Python

python
from sci_form import embed, vibrational_analysis, ir_spectrum

conf = embed("CCO", seed=42)
vib  = vibrational_analysis(conf.elements, conf.coords, method="xtb")
spec = ir_spectrum(vib, gamma=15.0, wn_min=600.0, wn_max=4000.0, n_points=1000)

print(f"ZPVE: {vib.zpve_ev:.4f} eV, {vib.n_real_modes} real modes")

import matplotlib.pyplot as plt
plt.plot(spec.wavenumbers, spec.intensities)
plt.xlabel("Wavenumber (cm⁻¹)")
plt.ylabel("Absorbance (km/mol)")
plt.title("IR Spectrum of Ethanol (GFN-xTB)")
plt.gca().invert_xaxis()
plt.show()

TypeScript/WASM

typescript
import init, { embed, compute_vibrational_analysis, compute_ir_spectrum } from 'sci-form-wasm';
await init();

const r = JSON.parse(embed('CCO', 42));
const elements = JSON.stringify(r.elements);
const coords   = JSON.stringify(r.coords);

const vib = JSON.parse(compute_vibrational_analysis(elements, coords, 'xtb', null));
const spec = JSON.parse(compute_ir_spectrum(JSON.stringify(vib), 15.0, 600.0, 4000.0, 1000));

console.log(`ZPVE: ${vib.zpve_ev.toFixed(4)} eV`);
const maxI = Math.max(...spec.intensities);
const wn   = spec.wavenumbers[spec.intensities.indexOf(maxI)];
console.log(`Dominant band at ${wn.toFixed(1)} cm⁻¹`);

Performance Notes

  • The Hessian requires 6N energy evaluations for central differences, where N is the number of atoms.
  • For EHT: ~0.3 ms/eval → ~1 s for 50 atoms.
  • For PM3: ~5 ms/eval → ~15 s for 50 atoms.
  • For GFN-xTB: ~20 ms/eval → ~60 s for 50 atoms.
  • Use method = "eht" for rapid screening; "xtb" for higher fidelity.

Released under the MIT License.