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
where
Default step size:
Mass-Weighted Hessian
To account for atomic masses in Newton's equations of motion, the Hessian is transformed to mass-weighted coordinates:
where
Normal Mode Diagonalization
The mass-weighted Hessian is diagonalized to obtain eigenvalues
Vibrational frequencies in cm⁻¹ are obtained from the eigenvalues:
where
Translation/Rotation Separation
The
for non-linear molecules for linear molecules
IR Intensities — Dipole Derivatives
The IR intensity of mode
Numerically, this is estimated via finite differences of the electronic dipole along each displacement, then projected onto the normal mode:
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:
In eV, using 1 cm⁻¹ = 1.2399 × 10⁻⁴ eV:
Lorentzian Broadened Spectrum
The discrete IR peaks are broadened using Lorentzian line shapes:
where
Parameters
compute_vibrational_analysis
| Parameter | Type | Default | Description |
|---|---|---|---|
elements | &[u8] | — | Atomic numbers |
positions | &[[f64;3]] | — | Cartesian coordinates (Å) |
method | &str | "eht" | Semiempirical method: "eht", "pm3", or "xtb" |
step_size | Option<f64> | None (→ 0.01 Å) | Finite-difference step Δ in Å |
compute_ir_spectrum
| Parameter | Type | Default | Description |
|---|---|---|---|
analysis | &VibrationalAnalysis | — | Result from compute_vibrational_analysis |
gamma | f64 | — | Lorentzian HWHM in cm⁻¹ (typical: 10–30) |
wn_min | f64 | — | Low-wavenumber limit (cm⁻¹) |
wn_max | f64 | — | High-wavenumber limit (cm⁻¹) |
n_points | usize | — | Grid resolution |
Output Types
VibrationalAnalysis
| Field | Type | Description |
|---|---|---|
n_atoms | usize | Number of atoms |
modes | Vec<VibrationalMode> | All |
n_real_modes | usize | Real modes (excluding translation/rotation) |
zpve_ev | f64 | Zero-point vibrational energy (eV) |
method | String | Method used for Hessian |
notes | Vec<String> | Caveats |
VibrationalMode
| Field | Type | Description |
|---|---|---|
frequency_cm1 | f64 | Frequency (cm⁻¹); negative = imaginary |
ir_intensity | f64 | IR intensity (km/mol) |
displacement | Vec<f64> | |
is_real | bool | True if real vibrational mode |
IrSpectrum
| Field | Type | Description |
|---|---|---|
wavenumbers | Vec<f64> | Wavenumber grid (cm⁻¹) |
intensities | Vec<f64> | Broadened absorption (km/mol) |
peaks | Vec<IrPeak> | Discrete peaks |
gamma | f64 | Broadening HWHM (cm⁻¹) |
notes | Vec<String> | Caveats |
IrPeak
| Field | Type | Description |
|---|---|---|
frequency_cm1 | f64 | Peak centre (cm⁻¹) |
ir_intensity | f64 | Raw intensity (km/mol) |
mode_index | usize | Index into modes |
API
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
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
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
energy evaluations for central differences, where 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.