use crate::constants::codata2018::EV_TO_KCAL_MOL;
use crate::gradients::nuclear_gradients::{
compute_cartesian_gradients_with_options, compute_gradient_norms, GradientWorkspace,
};
use crate::opt::hessian_update::{
update_cartesian_hessian, HessianUpdateScheme, HessianUpdateWorkspace,
};
use crate::parameters::ParameterModel;
use crate::properties::heat::compute_heat_of_formation;
use crate::scf::eigensolver::diagonalize_symmetric_with_work;
use crate::scf::scf_loop::{run_rhf_scf_adaptive_with_nddo, ScfResult};
use crate::types::{AlignedMatrix, AlignedVec64, MolecularBatch, ScfWorkspace};
#[derive(Debug, Clone)]
pub struct TransitionStateOptions {
pub max_cycles: usize,
pub grad_rms_tol: f64,
pub grad_max_tol: f64,
pub trust_radius: f64,
pub min_trust_radius: f64,
pub max_trust_radius: f64,
pub update_scheme: HessianUpdateScheme,
pub mode_following: bool,
pub target_mode: Option<usize>,
pub opt_mask: Option<Vec<bool>>,
pub use_nddo: bool,
pub hessian_delta: f64,
pub initial_hessian: Option<AlignedMatrix<f64>>,
}
impl Default for TransitionStateOptions {
fn default() -> Self {
Self {
max_cycles: 100,
grad_rms_tol: 0.1,
grad_max_tol: 0.2,
trust_radius: 0.1,
min_trust_radius: 0.005,
max_trust_radius: 0.3,
update_scheme: HessianUpdateScheme::Bofill,
mode_following: true,
target_mode: None,
opt_mask: None,
use_nddo: false,
hessian_delta: 0.005,
initial_hessian: None,
}
}
}
#[derive(Debug, Clone)]
pub struct TransitionStateResult {
pub converged: bool,
pub cycles: usize,
pub final_energy_ev: f64,
pub heat_of_formation_kcal: f64,
pub initial_grad_rms: f64,
pub final_grad_rms: f64,
pub final_grad_max: f64,
pub ts_mode_eigenvalue: f64,
pub ts_mode_index: usize,
pub final_hessian: AlignedMatrix<f64>,
pub final_scf: ScfResult,
}
#[derive(Debug, Clone)]
pub struct EigenvectorFollowingWorkspace {
pub eigenvalues: AlignedVec64<f64>,
pub eigenvectors: AlignedMatrix<f64>,
pub work_mat: AlignedMatrix<f64>,
pub f_basis: Vec<f64>,
pub s_basis: Vec<f64>,
pub step_cart: Vec<f64>,
pub grad_cart: Vec<f64>,
pub grad_cart_old: Vec<f64>,
pub v_target: Vec<f64>,
pub gradients_3d: Vec<[f64; 3]>,
pub g_plus: Vec<[f64; 3]>,
pub g_minus: Vec<[f64; 3]>,
pub hess_ws: HessianUpdateWorkspace,
}
impl EigenvectorFollowingWorkspace {
pub fn allocate(natoms: usize) -> Self {
let n3 = 3 * natoms;
Self {
eigenvalues: AlignedVec64::zeroed(n3),
eigenvectors: AlignedMatrix::zeroed(n3, n3),
work_mat: AlignedMatrix::zeroed(n3, n3),
f_basis: vec![0.0; n3],
s_basis: vec![0.0; n3],
step_cart: vec![0.0; n3],
grad_cart: vec![0.0; n3],
grad_cart_old: vec![0.0; n3],
v_target: vec![0.0; n3],
gradients_3d: vec![[0.0; 3]; natoms],
g_plus: vec![[0.0; 3]; natoms],
g_minus: vec![[0.0; 3]; natoms],
hess_ws: HessianUpdateWorkspace::allocate(n3),
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn compute_initial_cartesian_hessian(
batch: &mut MolecularBatch,
model: &dyn ParameterModel,
scf_ws: &mut ScfWorkspace,
grad_ws: &mut GradientWorkspace,
ef_ws: &mut EigenvectorFollowingWorkspace,
delta: f64,
use_nddo: bool,
mask: Option<&[bool]>,
) -> AlignedMatrix<f64> {
let natoms = batch.natoms;
let n3 = 3 * natoms;
let inv_2delta = 1.0 / (2.0 * delta);
let mut hess = AlignedMatrix::zeroed(n3, n3);
let init_density = scf_ws.density.clone();
for a in 0..natoms {
for alpha in 0..3 {
let col = 3 * a + alpha;
if let Some(m) = mask {
if !m[col] {
hess.set(col, col, 500.0);
continue;
}
}
displace_coord(batch, a, alpha, delta);
scf_ws.density.copy_from(&init_density);
run_rhf_scf_adaptive_with_nddo(batch, model, scf_ws, 50, 1e-7, 1e-6, use_nddo);
compute_cartesian_gradients_with_options(
batch,
model,
&scf_ws.density,
grad_ws,
&mut ef_ws.g_plus,
use_nddo,
);
displace_coord(batch, a, alpha, -2.0 * delta);
scf_ws.density.copy_from(&init_density);
run_rhf_scf_adaptive_with_nddo(batch, model, scf_ws, 50, 1e-7, 1e-6, use_nddo);
compute_cartesian_gradients_with_options(
batch,
model,
&scf_ws.density,
grad_ws,
&mut ef_ws.g_minus,
use_nddo,
);
displace_coord(batch, a, alpha, delta);
for b in 0..natoms {
for beta in 0..3 {
let row = 3 * b + beta;
let dg = (ef_ws.g_plus[b][beta] - ef_ws.g_minus[b][beta])
* inv_2delta
* EV_TO_KCAL_MOL;
hess.set(row, col, dg);
}
}
}
}
scf_ws.density.copy_from(&init_density);
for i in 0..n3 {
for j in (i + 1)..n3 {
let avg = 0.5 * (hess.get(i, j) + hess.get(j, i));
hess.set(i, j, avg);
hess.set(j, i, avg);
}
}
hess
}
#[inline(always)]
fn displace_coord(batch: &mut MolecularBatch, atom: usize, axis: usize, delta: f64) {
match axis {
0 => batch.x[atom] += delta,
1 => batch.y[atom] += delta,
2 => batch.z[atom] += delta,
_ => unreachable!(),
}
}
fn compute_effective_gradient_norms(
gradients_3d: &[[f64; 3]],
mask: Option<&[bool]>,
) -> (f64, f64) {
match mask {
Some(m) => {
let mut sum_sq = 0.0;
let mut max_norm = 0.0f64;
let mut n_active_coords = 0;
for (a, g) in gradients_3d.iter().enumerate() {
for c in 0..3 {
if m[3 * a + c] {
n_active_coords += 1;
let val = g[c] * EV_TO_KCAL_MOL;
let sq = val * val;
sum_sq += sq;
let abs_val = val.abs();
if abs_val > max_norm {
max_norm = abs_val;
}
}
}
}
let rms = if n_active_coords > 0 {
(sum_sq / (n_active_coords as f64)).sqrt()
} else {
0.0
};
(rms, max_norm)
}
None => compute_gradient_norms(gradients_3d),
}
}
fn solve_rfo_lambda_transverse(eigenvalues: &[f64], f: &[f64], ts_mode: usize) -> f64 {
let n = eigenvalues.len();
let mut lambda_min = f64::INFINITY;
for (i, &eig) in eigenvalues.iter().enumerate() {
if i != ts_mode && eig < lambda_min {
lambda_min = eig;
}
}
if !lambda_min.is_finite() {
return 0.0;
}
let eval_w = |lam: f64| -> f64 {
let mut sum = -lam;
for i in 0..n {
if i != ts_mode {
let diff = lam - eigenvalues[i];
if diff.abs() > 1e-15 {
sum += (f[i] * f[i]) / diff;
}
}
}
sum
};
let eps = 1e-5;
let mut bu = lambda_min - eps;
if eval_w(bu) >= 0.0 {
bu = lambda_min - 1e-3;
}
let mut step = 1.0;
let mut bl = bu - step;
let mut fl = eval_w(bl);
let mut iter = 0;
while fl <= 0.0 && iter < 60 {
step *= 2.0;
bl -= step;
fl = eval_w(bl);
iter += 1;
}
if fl <= 0.0 {
return lambda_min - 1.0;
}
let mut bisection_iter = 0;
while (bu - bl).abs() > 1e-9 && bisection_iter < 100 {
bisection_iter += 1;
let mid = 0.5 * (bl + bu);
let fm = eval_w(mid);
if fm.abs() < 1e-12 {
return mid;
}
if fm > 0.0 {
bl = mid;
} else {
bu = mid;
}
}
0.5 * (bl + bu)
}
pub fn optimize_transition_state(
batch: &mut MolecularBatch,
model: &dyn ParameterModel,
scf_ws: &mut ScfWorkspace,
grad_ws: &mut GradientWorkspace,
ef_ws: &mut EigenvectorFollowingWorkspace,
options: &TransitionStateOptions,
) -> TransitionStateResult {
let natoms = batch.natoms;
let n3 = 3 * natoms;
scf_ws.reset();
let initial_scf =
run_rhf_scf_adaptive_with_nddo(batch, model, scf_ws, 50, 1e-7, 1e-6, options.use_nddo);
let mut current_energy = initial_scf.total_energy_ev;
let mut last_scf = initial_scf;
compute_cartesian_gradients_with_options(
batch,
model,
&scf_ws.density,
grad_ws,
&mut ef_ws.gradients_3d,
options.use_nddo,
);
if let Some(ref m) = options.opt_mask {
for a in 0..natoms {
for c in 0..3 {
if !m[3 * a + c] {
ef_ws.gradients_3d[a][c] = 0.0;
}
}
}
}
let (mut rms_g, mut max_g) =
compute_effective_gradient_norms(&ef_ws.gradients_3d, options.opt_mask.as_deref());
let initial_rms_g = rms_g;
for a in 0..natoms {
ef_ws.grad_cart[3 * a] = ef_ws.gradients_3d[a][0] * EV_TO_KCAL_MOL;
ef_ws.grad_cart[3 * a + 1] = ef_ws.gradients_3d[a][1] * EV_TO_KCAL_MOL;
ef_ws.grad_cart[3 * a + 2] = ef_ws.gradients_3d[a][2] * EV_TO_KCAL_MOL;
}
let mut hessian = match options.initial_hessian {
Some(ref h) => {
assert_eq!(h.rows, n3);
assert_eq!(h.cols, n3);
h.clone()
}
None => compute_initial_cartesian_hessian(
batch,
model,
scf_ws,
grad_ws,
ef_ws,
options.hessian_delta,
options.use_nddo,
options.opt_mask.as_deref(),
),
};
let mut trust_radius = options.trust_radius;
let mut ts_mode = 0;
let mut converged = false;
let mut cycles_done = 0;
for cycle in 1..=options.max_cycles {
cycles_done = cycle;
diagonalize_symmetric_with_work(
&hessian,
&mut ef_ws.work_mat,
&mut ef_ws.eigenvalues,
&mut ef_ws.eigenvectors,
);
if cycle == 1 {
ts_mode = options.target_mode.unwrap_or(0);
for j in 0..n3 {
ef_ws.v_target[j] = ef_ws.eigenvectors.get(j, ts_mode);
}
} else if options.mode_following {
let mut best_mode = 0;
let mut best_overlap = -1.0;
for i in 0..n3 {
let mut ovlp = 0.0;
for j in 0..n3 {
ovlp += ef_ws.eigenvectors.get(j, i) * ef_ws.v_target[j];
}
let abs_ovlp = ovlp.abs();
if abs_ovlp > best_overlap {
best_overlap = abs_ovlp;
best_mode = i;
}
}
ts_mode = best_mode;
for j in 0..n3 {
ef_ws.v_target[j] = ef_ws.eigenvectors.get(j, ts_mode);
}
}
for i in 0..n3 {
let mut sum = 0.0;
for j in 0..n3 {
sum += ef_ws.eigenvectors.get(j, i) * ef_ws.grad_cart[j];
}
ef_ws.f_basis[i] = sum;
}
let lambda_ts = ef_ws.eigenvalues[ts_mode];
let f_ts = ef_ws.f_basis[ts_mode];
let lambda0 = 0.5 * (lambda_ts + (lambda_ts * lambda_ts + 4.0 * f_ts * f_ts).sqrt());
let denom_ts = lambda0 - lambda_ts;
ef_ws.s_basis[ts_mode] = if denom_ts.abs() > 1e-12 {
f_ts / denom_ts
} else {
0.0
};
let lambda_trans = solve_rfo_lambda_transverse(&ef_ws.eigenvalues, &ef_ws.f_basis, ts_mode);
for i in 0..n3 {
if i != ts_mode {
let diff = lambda_trans - ef_ws.eigenvalues[i];
ef_ws.s_basis[i] = if diff.abs() > 1e-12 {
ef_ws.f_basis[i] / diff
} else {
0.0
};
}
}
for j in 0..n3 {
let mut sum = 0.0;
for i in 0..n3 {
sum += ef_ws.eigenvectors.get(j, i) * ef_ws.s_basis[i];
}
if let Some(ref m) = options.opt_mask {
if !m[j] {
sum = 0.0;
}
}
ef_ws.step_cart[j] = sum;
}
let mut step_norm_sq = 0.0;
for j in 0..n3 {
step_norm_sq += ef_ws.step_cart[j] * ef_ws.step_cart[j];
}
let step_norm = step_norm_sq.sqrt();
let step_scale = if step_norm > trust_radius && step_norm > 1e-14 {
let scale = trust_radius / step_norm;
for j in 0..n3 {
ef_ws.step_cart[j] *= scale;
}
scale
} else {
1.0
};
let mut de_pred = 0.0;
for i in 0..n3 {
let si = ef_ws.s_basis[i] * step_scale;
de_pred += ef_ws.f_basis[i] * si + 0.5 * ef_ws.eigenvalues[i] * si * si;
}
ef_ws.grad_cart_old.copy_from_slice(&ef_ws.grad_cart);
for a in 0..natoms {
batch.x[a] += ef_ws.step_cart[3 * a];
batch.y[a] += ef_ws.step_cart[3 * a + 1];
batch.z[a] += ef_ws.step_cart[3 * a + 2];
}
let new_scf =
run_rhf_scf_adaptive_with_nddo(batch, model, scf_ws, 50, 1e-7, 1e-6, options.use_nddo);
let new_energy = new_scf.total_energy_ev;
let de_act = (new_energy - current_energy) * EV_TO_KCAL_MOL;
current_energy = new_energy;
last_scf = new_scf;
compute_cartesian_gradients_with_options(
batch,
model,
&scf_ws.density,
grad_ws,
&mut ef_ws.gradients_3d,
options.use_nddo,
);
if let Some(ref m) = options.opt_mask {
for a in 0..natoms {
for c in 0..3 {
if !m[3 * a + c] {
ef_ws.gradients_3d[a][c] = 0.0;
}
}
}
}
let (cur_rms, cur_max) =
compute_effective_gradient_norms(&ef_ws.gradients_3d, options.opt_mask.as_deref());
rms_g = cur_rms;
max_g = cur_max;
for a in 0..natoms {
ef_ws.grad_cart[3 * a] = ef_ws.gradients_3d[a][0] * EV_TO_KCAL_MOL;
ef_ws.grad_cart[3 * a + 1] = ef_ws.gradients_3d[a][1] * EV_TO_KCAL_MOL;
ef_ws.grad_cart[3 * a + 2] = ef_ws.gradients_3d[a][2] * EV_TO_KCAL_MOL;
}
if de_pred.abs() > 1e-5 {
let ratio = de_act / de_pred;
if ratio <= 0.1 || ratio >= 3.0 {
trust_radius = (trust_radius.min(step_norm) / 2.0).max(options.min_trust_radius);
} else if (0.75..=1.33).contains(&ratio) && step_norm >= 0.85 * trust_radius {
trust_radius = (trust_radius * 2.0f64.sqrt()).min(options.max_trust_radius);
}
}
if rms_g < options.grad_rms_tol && max_g < options.grad_max_tol {
converged = true;
break;
}
for j in 0..n3 {
ef_ws.grad_cart_old[j] = ef_ws.grad_cart[j] - ef_ws.grad_cart_old[j];
}
update_cartesian_hessian(
&mut hessian,
&ef_ws.step_cart,
&ef_ws.grad_cart_old,
options.update_scheme,
&mut ef_ws.hess_ws,
);
}
let (_, heat_of_formation_kcal) =
compute_heat_of_formation(current_energy, &batch.atomic_numbers, model, 0.0);
TransitionStateResult {
converged,
cycles: cycles_done,
final_energy_ev: current_energy,
heat_of_formation_kcal,
initial_grad_rms: initial_rms_g,
final_grad_rms: rms_g,
final_grad_max: max_g,
ts_mode_eigenvalue: ef_ws.eigenvalues[ts_mode],
ts_mode_index: ts_mode,
final_hessian: hessian,
final_scf: last_scf,
}
}