#[derive(Debug, Clone)]
pub struct IrcPathSegment {
pub frames: Vec<Vec<f64>>,
pub energies: Vec<f64>,
pub arc_lengths: Vec<f64>,
pub converged: bool,
}
#[derive(Debug, Clone)]
pub struct IrcResult {
pub forward: IrcPathSegment,
pub reverse: IrcPathSegment,
}
pub struct IrcConfig {
pub step_size: f64,
pub max_steps: usize,
pub grad_threshold: f64,
pub method: String,
}
impl Default for IrcConfig {
fn default() -> Self {
Self {
step_size: 0.05,
max_steps: 100,
grad_threshold: 0.005,
method: "uff".into(),
}
}
}
pub fn compute_irc(
smiles: &str,
elements: &[u8],
ts_coords: &[f64],
config: &IrcConfig,
) -> Result<IrcResult, String> {
let _n_xyz = ts_coords.len();
let mol = crate::graph::Molecule::from_smiles(smiles)?;
let backend = crate::dynamics::NebBackend::from_method(&config.method)?;
let ts_mode = compute_ts_mode(backend, smiles, elements, &mol, ts_coords)?;
let forward = follow_irc_direction(
backend, smiles, elements, &mol, ts_coords, &ts_mode, 1.0, config,
)?;
let reverse = follow_irc_direction(
backend, smiles, elements, &mol, ts_coords, &ts_mode, -1.0, config,
)?;
Ok(IrcResult { forward, reverse })
}
fn compute_ts_mode(
backend: crate::dynamics::NebBackend,
smiles: &str,
elements: &[u8],
mol: &crate::graph::Molecule,
coords: &[f64],
) -> Result<Vec<f64>, String> {
let n = coords.len();
let h = 0.005;
let mut g0 = vec![0.0f64; n];
crate::dynamics::neb_energy_and_gradient(backend, smiles, elements, mol, coords, &mut g0)?;
#[cfg(feature = "parallel")]
let hessian_rows: Vec<Result<Vec<f64>, String>> = {
use rayon::prelude::*;
(0..n)
.into_par_iter()
.map(|i| {
let mut x_plus = coords.to_vec();
x_plus[i] += h;
let mut g_plus = vec![0.0f64; n];
crate::dynamics::neb_energy_and_gradient(
backend,
smiles,
elements,
mol,
&x_plus,
&mut g_plus,
)?;
let row: Vec<f64> = (0..n).map(|j| (g_plus[j] - g0[j]) / h).collect();
Ok(row)
})
.collect()
};
#[cfg(not(feature = "parallel"))]
let hessian_rows: Vec<Result<Vec<f64>, String>> = {
(0..n)
.map(|i| {
let mut x_plus = coords.to_vec();
x_plus[i] += h;
let mut g_plus = vec![0.0f64; n];
crate::dynamics::neb_energy_and_gradient(
backend,
smiles,
elements,
mol,
&x_plus,
&mut g_plus,
)?;
let row: Vec<f64> = (0..n).map(|j| (g_plus[j] - g0[j]) / h).collect();
Ok(row)
})
.collect()
};
let mut hessian = vec![0.0f64; n * n];
for (i, row_result) in hessian_rows.into_iter().enumerate() {
let row = row_result?;
for j in 0..n {
hessian[i * n + j] = row[j];
}
}
for i in 0..n {
for j in (i + 1)..n {
let avg = 0.5 * (hessian[i * n + j] + hessian[j * n + i]);
hessian[i * n + j] = avg;
hessian[j * n + i] = avg;
}
}
let masses = atomic_masses(elements);
for i in 0..n {
let mi = masses[i / 3].sqrt();
for j in 0..n {
let mj = masses[j / 3].sqrt();
hessian[i * n + j] /= mi * mj;
}
}
let mut v = vec![0.0f64; n];
for i in 0..n {
v[i] = ((i as f64 + 1.0) * 0.618).sin();
}
normalise(&mut v);
let neg_hessian: Vec<f64> = hessian.iter().map(|&x| -x).collect();
for _ in 0..200 {
let mut new_v = vec![0.0f64; n];
for i in 0..n {
for j in 0..n {
new_v[i] += neg_hessian[i * n + j] * v[j];
}
}
normalise(&mut new_v);
v = new_v;
}
for i in 0..n {
v[i] /= masses[i / 3].sqrt();
}
normalise(&mut v);
Ok(v)
}
fn follow_irc_direction(
backend: crate::dynamics::NebBackend,
smiles: &str,
elements: &[u8],
mol: &crate::graph::Molecule,
ts_coords: &[f64],
mode: &[f64],
sign: f64,
config: &IrcConfig,
) -> Result<IrcPathSegment, String> {
let n = ts_coords.len();
let masses = atomic_masses(elements);
let mut x: Vec<f64> = ts_coords
.iter()
.enumerate()
.map(|(i, &c)| c + sign * config.step_size * mode[i])
.collect();
let mut frames = vec![ts_coords.to_vec(), x.clone()];
let mut energies = Vec::new();
let mut arc_lengths = vec![0.0];
let mut total_arc = 0.0f64;
let mut g0 = vec![0.0f64; n];
let e_ts = crate::dynamics::neb_energy_and_gradient(
backend, smiles, elements, mol, ts_coords, &mut g0,
)?;
energies.push(e_ts);
let mut grad = vec![0.0f64; n];
let e0 =
crate::dynamics::neb_energy_and_gradient(backend, smiles, elements, mol, &x, &mut grad)?;
energies.push(e0);
let ds: f64 = (0..n)
.map(|i| {
let dx = x[i] - ts_coords[i];
masses[i / 3] * dx * dx
})
.sum::<f64>()
.sqrt();
total_arc += ds;
arc_lengths.push(total_arc);
let mut converged = false;
for _ in 0..config.max_steps {
let mut grad = vec![0.0f64; n];
let energy = crate::dynamics::neb_energy_and_gradient(
backend, smiles, elements, mol, &x, &mut grad,
)?;
let mw_gnorm: f64 = grad
.iter()
.enumerate()
.map(|(i, &g)| g * g / masses[i / 3])
.sum::<f64>()
.sqrt();
if mw_gnorm < config.grad_threshold {
converged = true;
break;
}
let mut x_new = vec![0.0f64; n];
for i in 0..n {
x_new[i] = x[i] - config.step_size * grad[i] / (masses[i / 3] * mw_gnorm);
}
if !x_new.iter().all(|v| v.is_finite()) {
break;
}
let ds: f64 = (0..n)
.map(|i| {
let dx = x_new[i] - x[i];
masses[i / 3] * dx * dx
})
.sum::<f64>()
.sqrt();
total_arc += ds;
x = x_new;
frames.push(x.clone());
energies.push(energy);
arc_lengths.push(total_arc);
if energies.len() >= 3 {
let last = energies.len() - 1;
if energies[last] > energies[last - 1] && energies[last - 1] > energies[last - 2] {
converged = true;
break;
}
}
}
Ok(IrcPathSegment {
frames,
energies,
arc_lengths,
converged,
})
}
fn normalise(v: &mut [f64]) {
let len: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if len > 1e-14 {
for x in v.iter_mut() {
*x /= len;
}
}
}
fn atomic_masses(elements: &[u8]) -> Vec<f64> {
elements
.iter()
.map(|&z| match z {
1 => 1.008,
6 => 12.011,
7 => 14.007,
8 => 15.999,
9 => 18.998,
15 => 30.974,
16 => 32.06,
17 => 35.45,
35 => 79.904,
53 => 126.904,
_ => z as f64 * 2.0, })
.collect()
}