use crate::constants::codata2018::EV_TO_KCAL_MOL;
use crate::constants::standard_atomic_mass;
use crate::gradients::nuclear_gradients::{
compute_cartesian_gradients_with_options, GradientWorkspace,
};
use crate::parameters::ParameterModel;
use crate::properties::heat::compute_heat_of_formation;
use crate::scf::scf_loop::{run_rhf_scf_adaptive_with_nddo, ScfOptions};
use crate::types::{MolecularBatch, ScfWorkspace};
use crate::vibrations::hessian::{compute_hessian_and_frequencies, HessianOptions};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IrcDirection {
Forward,
Reverse,
Both,
}
#[derive(Debug, Clone)]
pub struct IrcOptions {
pub step_size: f64,
pub max_points: usize,
pub corrector_max_iter: usize,
pub corrector_tol: f64,
pub grad_rms_tol: f64,
pub energy_increase_tol: f64,
pub direction: IrcDirection,
pub use_nddo: bool,
pub transition_vector: Option<Vec<f64>>,
}
impl Default for IrcOptions {
fn default() -> Self {
Self {
step_size: 0.1,
max_points: 50,
corrector_max_iter: 25,
corrector_tol: 1e-4,
grad_rms_tol: 0.05,
energy_increase_tol: 0.02,
direction: IrcDirection::Both,
use_nddo: false,
transition_vector: None,
}
}
}
#[derive(Debug, Clone)]
pub struct IrcPoint {
pub path_coordinate: f64,
pub energy_ev: f64,
pub heat_of_formation_kcal: f64,
pub coordinates: Vec<[f64; 3]>,
pub cartesian_gradient_rms: f64,
pub mass_weighted_gradient_rms: f64,
}
#[derive(Debug, Clone)]
pub struct IrcResult {
pub points: Vec<IrcPoint>,
pub ts_point_index: usize,
pub forward_converged: bool,
pub reverse_converged: bool,
}
#[derive(Debug, Clone)]
pub struct IrcWorkspace {
pub q: Vec<f64>,
pub q_pivot: Vec<f64>,
pub q_cand: Vec<f64>,
pub q_next: Vec<f64>,
pub q_prev: Vec<f64>,
pub gradients_3d: Vec<[f64; 3]>,
pub grad_q: Vec<f64>,
pub tangent: Vec<f64>,
pub ts_vector: Vec<f64>,
pub masses: Vec<f64>,
pub sqrt_masses: Vec<f64>,
pub inv_sqrt_masses: Vec<f64>,
pub ts_coords: Vec<[f64; 3]>,
}
impl IrcWorkspace {
pub fn allocate(batch: &MolecularBatch) -> Self {
let natoms = batch.natoms;
let n3 = 3 * natoms;
let mut masses = Vec::with_capacity(natoms);
let mut sqrt_masses = Vec::with_capacity(natoms);
let mut inv_sqrt_masses = Vec::with_capacity(natoms);
let mut ts_coords = Vec::with_capacity(natoms);
for a in 0..natoms {
let m = standard_atomic_mass(batch.atomic_numbers[a]);
masses.push(m);
let sm = m.sqrt();
sqrt_masses.push(sm);
inv_sqrt_masses.push(1.0 / sm);
ts_coords.push([batch.x[a], batch.y[a], batch.z[a]]);
}
Self {
q: vec![0.0; n3],
q_pivot: vec![0.0; n3],
q_cand: vec![0.0; n3],
q_next: vec![0.0; n3],
q_prev: vec![0.0; n3],
gradients_3d: vec![[0.0; 3]; natoms],
grad_q: vec![0.0; n3],
tangent: vec![0.0; n3],
ts_vector: vec![0.0; n3],
masses,
sqrt_masses,
inv_sqrt_masses,
ts_coords,
}
}
pub fn restore_ts_coords(&self, batch: &mut MolecularBatch) {
for a in 0..batch.natoms {
batch.x[a] = self.ts_coords[a][0];
batch.y[a] = self.ts_coords[a][1];
batch.z[a] = self.ts_coords[a][2];
}
}
pub fn cartesian_to_mass_weighted(&mut self, batch: &MolecularBatch) {
for a in 0..batch.natoms {
let sm = self.sqrt_masses[a];
self.q[3 * a] = sm * batch.x[a];
self.q[3 * a + 1] = sm * batch.y[a];
self.q[3 * a + 2] = sm * batch.z[a];
}
}
pub fn mass_weighted_to_cartesian(&self, q: &[f64], batch: &mut MolecularBatch) {
for a in 0..batch.natoms {
let ism = self.inv_sqrt_masses[a];
batch.x[a] = q[3 * a] * ism;
batch.y[a] = q[3 * a + 1] * ism;
batch.z[a] = q[3 * a + 2] * ism;
}
}
pub fn compute_mass_weighted_gradients(&mut self, natoms: usize) -> (f64, f64) {
let mut sum_sq_cart = 0.0;
let mut sum_sq_mw = 0.0;
let n3 = 3 * natoms;
for a in 0..natoms {
let ism = self.inv_sqrt_masses[a];
for c in 0..3 {
let g_cart = self.gradients_3d[a][c] * EV_TO_KCAL_MOL;
sum_sq_cart += g_cart * g_cart;
let g_mw = g_cart * ism;
self.grad_q[3 * a + c] = g_mw;
sum_sq_mw += g_mw * g_mw;
}
}
let rms_cart = (sum_sq_cart / (n3 as f64)).sqrt();
let rms_mw = (sum_sq_mw / (n3 as f64)).sqrt();
(rms_cart, rms_mw)
}
}
pub fn trace_intrinsic_reaction_coordinate(
batch: &mut MolecularBatch,
model: &dyn ParameterModel,
scf_ws: &mut ScfWorkspace,
grad_ws: &mut GradientWorkspace,
irc_ws: &mut IrcWorkspace,
options: &IrcOptions,
) -> IrcResult {
let natoms = batch.natoms;
let n3 = 3 * natoms;
for a in 0..natoms {
irc_ws.ts_coords[a] = [batch.x[a], batch.y[a], batch.z[a]];
}
if let Some(ref tv) = options.transition_vector {
assert_eq!(tv.len(), n3, "Supplied transition vector length mismatch");
irc_ws.ts_vector.copy_from_slice(tv);
} else {
let scf_opts = ScfOptions {
max_iter: 60,
energy_tol_ev: 1e-8,
density_tol: 1e-7,
use_nddo: options.use_nddo,
..Default::default()
};
let hess_opts = HessianOptions {
delta: 0.005,
recompute_scf: true,
use_nddo: options.use_nddo,
project_external: true,
..Default::default()
};
let vib_res = compute_hessian_and_frequencies(batch, model, scf_ws, &scf_opts, &hess_opts);
let mut found_mode = false;
if let Some(first_mode) = vib_res.normal_modes.first() {
if first_mode.frequency_cm1 < 0.0 {
let mut norm_sq = 0.0;
for a in 0..natoms {
let sm = irc_ws.sqrt_masses[a];
for c in 0..3 {
let val = sm * first_mode.displacements[a][c];
irc_ws.ts_vector[3 * a + c] = val;
norm_sq += val * val;
}
}
let inv_norm = 1.0 / norm_sq.sqrt().max(1e-15);
for i in 0..n3 {
irc_ws.ts_vector[i] *= inv_norm;
}
found_mode = true;
}
}
if !found_mode {
irc_ws.ts_vector.fill(0.0);
irc_ws.ts_vector[0] = 1.0;
}
}
irc_ws.restore_ts_coords(batch);
scf_ws.reset();
let ts_scf =
run_rhf_scf_adaptive_with_nddo(batch, model, scf_ws, 60, 1e-7, 1e-6, options.use_nddo);
let ts_heat =
compute_heat_of_formation(ts_scf.total_energy_ev, &batch.atomic_numbers, model, 0.0).1;
compute_cartesian_gradients_with_options(
batch,
model,
&scf_ws.density,
grad_ws,
&mut irc_ws.gradients_3d,
options.use_nddo,
);
let (ts_rms_cart, ts_rms_mw) = irc_ws.compute_mass_weighted_gradients(natoms);
let g_dot_v: f64 = (0..n3)
.map(|i| irc_ws.grad_q[i] * irc_ws.ts_vector[i])
.sum();
if g_dot_v > 1e-4 {
for i in 0..n3 {
irc_ws.ts_vector[i] = -irc_ws.ts_vector[i];
}
}
let ts_point = IrcPoint {
path_coordinate: 0.0,
energy_ev: ts_scf.total_energy_ev,
heat_of_formation_kcal: ts_heat,
coordinates: irc_ws.ts_coords.clone(),
cartesian_gradient_rms: ts_rms_cart,
mass_weighted_gradient_rms: ts_rms_mw,
};
let mut forward_points = Vec::new();
let mut forward_converged = false;
let mut reverse_points = Vec::new();
let mut reverse_converged = false;
if options.direction == IrcDirection::Forward || options.direction == IrcDirection::Both {
let (pts, conv) = trace_single_branch(batch, model, scf_ws, grad_ws, irc_ws, options, 1.0);
forward_points = pts;
forward_converged = conv;
}
if options.direction == IrcDirection::Reverse || options.direction == IrcDirection::Both {
let (pts, conv) = trace_single_branch(batch, model, scf_ws, grad_ws, irc_ws, options, -1.0);
reverse_points = pts;
reverse_converged = conv;
}
let mut all_points = Vec::with_capacity(reverse_points.len() + 1 + forward_points.len());
for p in reverse_points.into_iter().rev() {
all_points.push(p);
}
let ts_idx = all_points.len();
all_points.push(ts_point);
for p in forward_points {
all_points.push(p);
}
irc_ws.restore_ts_coords(batch);
IrcResult {
points: all_points,
ts_point_index: ts_idx,
forward_converged,
reverse_converged,
}
}
fn trace_single_branch(
batch: &mut MolecularBatch,
model: &dyn ParameterModel,
scf_ws: &mut ScfWorkspace,
grad_ws: &mut GradientWorkspace,
irc_ws: &mut IrcWorkspace,
options: &IrcOptions,
direction_sign: f64,
) -> (Vec<IrcPoint>, bool) {
let natoms = batch.natoms;
let n3 = 3 * natoms;
let ds = options.step_size;
let half_ds = 0.5 * ds;
irc_ws.restore_ts_coords(batch);
irc_ws.cartesian_to_mass_weighted(batch);
for i in 0..n3 {
irc_ws.tangent[i] = direction_sign * irc_ws.ts_vector[i];
}
let mut path_points = Vec::new();
let mut s = 0.0;
let mut branch_converged = false;
let mut last_energy_kcal = compute_heat_of_formation(
run_rhf_scf_adaptive_with_nddo(batch, model, scf_ws, 50, 1e-7, 1e-6, options.use_nddo)
.total_energy_ev,
&batch.atomic_numbers,
model,
0.0,
)
.1;
for _step_idx in 0..options.max_points {
for i in 0..n3 {
irc_ws.q_pivot[i] = irc_ws.q[i] + half_ds * irc_ws.tangent[i];
irc_ws.q_cand[i] = irc_ws.q[i] + ds * irc_ws.tangent[i];
}
let mut _corrector_converged = false;
let mut current_energy_ev = 0.0;
let mut current_heat_kcal = 0.0;
let mut current_rms_cart = 0.0;
let mut current_rms_mw = 0.0;
for _iter in 0..options.corrector_max_iter {
irc_ws.mass_weighted_to_cartesian(&irc_ws.q_cand, batch);
scf_ws.reset();
let scf_res = run_rhf_scf_adaptive_with_nddo(
batch,
model,
scf_ws,
50,
1e-7,
1e-6,
options.use_nddo,
);
current_energy_ev = scf_res.total_energy_ev;
current_heat_kcal =
compute_heat_of_formation(current_energy_ev, &batch.atomic_numbers, model, 0.0).1;
compute_cartesian_gradients_with_options(
batch,
model,
&scf_ws.density,
grad_ws,
&mut irc_ws.gradients_3d,
options.use_nddo,
);
let (rms_cart, rms_mw) = irc_ws.compute_mass_weighted_gradients(natoms);
current_rms_cart = rms_cart;
current_rms_mw = rms_mw;
let mut g_norm_sq = 0.0;
for i in 0..n3 {
g_norm_sq += irc_ws.grad_q[i] * irc_ws.grad_q[i];
}
let g_norm = g_norm_sq.sqrt();
if g_norm < 1e-12 {
for i in 0..n3 {
irc_ws.q_next[i] = irc_ws.q_cand[i];
}
_corrector_converged = true;
break;
}
let scale = half_ds / g_norm;
let mut diff_sq = 0.0;
for i in 0..n3 {
let target = irc_ws.q_pivot[i] - scale * irc_ws.grad_q[i];
let d = target - irc_ws.q_cand[i];
diff_sq += d * d;
irc_ws.q_next[i] = 0.5 * (irc_ws.q_cand[i] + target);
}
let mut rad_sq = 0.0;
for i in 0..n3 {
let dr = irc_ws.q_next[i] - irc_ws.q_pivot[i];
rad_sq += dr * dr;
}
let rad = rad_sq.sqrt().max(1e-15);
let rad_scale = half_ds / rad;
for i in 0..n3 {
irc_ws.q_cand[i] =
irc_ws.q_pivot[i] + rad_scale * (irc_ws.q_next[i] - irc_ws.q_pivot[i]);
}
let disp_change = diff_sq.sqrt();
if disp_change < options.corrector_tol {
_corrector_converged = true;
break;
}
}
irc_ws.mass_weighted_to_cartesian(&irc_ws.q_cand, batch);
let mut coords = Vec::with_capacity(natoms);
for a in 0..natoms {
coords.push([batch.x[a], batch.y[a], batch.z[a]]);
}
s += direction_sign * ds;
path_points.push(IrcPoint {
path_coordinate: s,
energy_ev: current_energy_ev,
heat_of_formation_kcal: current_heat_kcal,
coordinates: coords,
cartesian_gradient_rms: current_rms_cart,
mass_weighted_gradient_rms: current_rms_mw,
});
if current_rms_mw < options.grad_rms_tol {
branch_converged = true;
break;
}
if current_heat_kcal > last_energy_kcal + options.energy_increase_tol {
branch_converged = true;
break;
}
last_energy_kcal = current_heat_kcal;
for i in 0..n3 {
irc_ws.q[i] = irc_ws.q_cand[i];
}
let mut g_norm_sq = 0.0;
for i in 0..n3 {
g_norm_sq += irc_ws.grad_q[i] * irc_ws.grad_q[i];
}
let g_norm = g_norm_sq.sqrt().max(1e-15);
for i in 0..n3 {
irc_ws.tangent[i] = -irc_ws.grad_q[i] / g_norm;
}
}
(path_points, branch_converged)
}