1use nalgebra::DMatrix;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum HessianMethod {
11 Eht,
13 Pm3,
15 Xtb,
17 Uff,
19}
20
21fn evaluate_energy(
23 method: HessianMethod,
24 elements: &[u8],
25 positions: &[[f64; 3]],
26) -> Result<f64, String> {
27 match method {
28 HessianMethod::Eht => {
29 let result = crate::eht::solve_eht(elements, positions, None)?;
30 let n_occ = result.n_electrons / 2;
32 let total: f64 = result.energies.iter().take(n_occ).sum::<f64>() * 2.0;
33 Ok(total)
34 }
35 HessianMethod::Pm3 => {
36 let result = crate::pm3::solve_pm3(elements, positions)?;
37 Ok(result.total_energy)
38 }
39 HessianMethod::Xtb => {
40 let result = crate::xtb::solve_xtb(elements, positions)?;
41 Ok(result.total_energy)
42 }
43 HessianMethod::Uff => {
44 Err("UFF uses analytical Hessian path; use compute_uff_analytical_hessian".to_string())
45 }
46 }
47}
48
49pub fn compute_numerical_hessian(
61 elements: &[u8],
62 positions: &[[f64; 3]],
63 method: HessianMethod,
64 step_size: Option<f64>,
65) -> Result<DMatrix<f64>, String> {
66 let n_atoms = elements.len();
67 if n_atoms != positions.len() {
68 return Err("elements and positions length mismatch".to_string());
69 }
70
71 let n3 = 3 * n_atoms;
72
73 let delta = step_size.unwrap_or_else(|| auto_step_size(elements));
75 let delta_sq = delta * delta;
76
77 let e0 = evaluate_energy(method, elements, positions)?;
79
80 let mut coords: Vec<f64> = Vec::with_capacity(n3);
82 for pos in positions {
83 coords.extend_from_slice(pos);
84 }
85
86 let mut hessian = DMatrix::zeros(n3, n3);
87
88 for i in 0..n3 {
90 let mut coords_plus = coords.clone();
91 let mut coords_minus = coords.clone();
92 coords_plus[i] += delta;
93 coords_minus[i] -= delta;
94
95 let pos_plus = flat_to_positions(&coords_plus);
96 let pos_minus = flat_to_positions(&coords_minus);
97
98 let e_plus = evaluate_energy(method, elements, &pos_plus)?;
99 let e_minus = evaluate_energy(method, elements, &pos_minus)?;
100
101 hessian[(i, i)] = (e_plus - 2.0 * e0 + e_minus) / delta_sq;
102 }
103
104 for i in 0..n3 {
106 for j in (i + 1)..n3 {
107 let mut coords_pp = coords.clone();
108 let mut coords_pm = coords.clone();
109 let mut coords_mp = coords.clone();
110 let mut coords_mm = coords.clone();
111
112 coords_pp[i] += delta;
113 coords_pp[j] += delta;
114 coords_pm[i] += delta;
115 coords_pm[j] -= delta;
116 coords_mp[i] -= delta;
117 coords_mp[j] += delta;
118 coords_mm[i] -= delta;
119 coords_mm[j] -= delta;
120
121 let e_pp = evaluate_energy(method, elements, &flat_to_positions(&coords_pp))?;
122 let e_pm = evaluate_energy(method, elements, &flat_to_positions(&coords_pm))?;
123 let e_mp = evaluate_energy(method, elements, &flat_to_positions(&coords_mp))?;
124 let e_mm = evaluate_energy(method, elements, &flat_to_positions(&coords_mm))?;
125
126 let hij = (e_pp - e_pm - e_mp + e_mm) / (4.0 * delta_sq);
127 hessian[(i, j)] = hij;
128 hessian[(j, i)] = hij;
129 }
130 }
131
132 enforce_symmetry(&mut hessian);
134
135 Ok(hessian)
136}
137
138fn enforce_symmetry(hessian: &mut DMatrix<f64>) {
143 let n = hessian.nrows();
144 for i in 0..n {
145 for j in (i + 1)..n {
146 let avg = (hessian[(i, j)] + hessian[(j, i)]) * 0.5;
147 hessian[(i, j)] = avg;
148 hessian[(j, i)] = avg;
149 }
150 }
151}
152
153fn auto_step_size(elements: &[u8]) -> f64 {
158 let min_mass = elements
159 .iter()
160 .map(|&z| match z {
161 1 => 1.008,
162 6 => 12.011,
163 7 => 14.007,
164 8 => 15.999,
165 9 => 18.998,
166 _ => z as f64 * 1.5,
167 })
168 .fold(f64::INFINITY, f64::min);
169
170 (0.005 * (min_mass / 1.008).sqrt()).clamp(0.005, 0.01)
172}
173
174fn flat_to_positions(coords: &[f64]) -> Vec<[f64; 3]> {
175 coords.chunks(3).map(|c| [c[0], c[1], c[2]]).collect()
176}
177
178pub fn compute_uff_analytical_hessian(
187 smiles: &str,
188 coords_flat: &[f64],
189 step_size: Option<f64>,
190) -> Result<DMatrix<f64>, String> {
191 let mol = crate::graph::Molecule::from_smiles(smiles)?;
192 let n_atoms = mol.graph.node_count();
193 let n3 = 3 * n_atoms;
194
195 if coords_flat.len() != n3 {
196 return Err(format!(
197 "coords length {} != 3 * atoms {}",
198 coords_flat.len(),
199 n_atoms
200 ));
201 }
202
203 let ff = crate::forcefield::builder::build_uff_force_field(&mol);
204 let elements: Vec<u8> = (0..n_atoms)
205 .map(|i| mol.graph[petgraph::graph::NodeIndex::new(i)].element)
206 .collect();
207 let delta = step_size.unwrap_or_else(|| auto_step_size(&elements));
208
209 let mut hessian = DMatrix::zeros(n3, n3);
210
211 for j in 0..n3 {
214 let mut coords_plus = coords_flat.to_vec();
215 let mut coords_minus = coords_flat.to_vec();
216 coords_plus[j] += delta;
217 coords_minus[j] -= delta;
218
219 let mut grad_plus = vec![0.0; n3];
220 let mut grad_minus = vec![0.0; n3];
221
222 ff.compute_system_energy_and_gradients(&coords_plus, &mut grad_plus);
223 ff.compute_system_energy_and_gradients(&coords_minus, &mut grad_minus);
224
225 for i in 0..n3 {
226 hessian[(i, j)] = (grad_plus[i] - grad_minus[i]) / (2.0 * delta);
227 }
228 }
229
230 enforce_symmetry(&mut hessian);
232
233 Ok(hessian)
234}
235
236#[cfg(feature = "parallel")]
242pub fn compute_numerical_hessian_parallel(
243 elements: &[u8],
244 positions: &[[f64; 3]],
245 method: HessianMethod,
246 step_size: Option<f64>,
247) -> Result<DMatrix<f64>, String> {
248 use rayon::prelude::*;
249 use std::sync::Mutex;
250
251 let n_atoms = elements.len();
252 if n_atoms != positions.len() {
253 return Err("elements and positions length mismatch".to_string());
254 }
255
256 let n3 = 3 * n_atoms;
257 let delta = step_size.unwrap_or_else(|| auto_step_size(elements));
258 let delta_sq = delta * delta;
259
260 let e0 = evaluate_energy(method, elements, positions)?;
261
262 let mut coords: Vec<f64> = Vec::with_capacity(n3);
263 for pos in positions {
264 coords.extend_from_slice(pos);
265 }
266
267 let hessian = Mutex::new(DMatrix::zeros(n3, n3));
268
269 let diag_results: Vec<Result<(usize, f64), String>> = (0..n3)
271 .into_par_iter()
272 .map(|i| {
273 let mut cp = coords.clone();
274 let mut cm = coords.clone();
275 cp[i] += delta;
276 cm[i] -= delta;
277 let e_plus = evaluate_energy(method, elements, &flat_to_positions(&cp))?;
278 let e_minus = evaluate_energy(method, elements, &flat_to_positions(&cm))?;
279 Ok((i, (e_plus - 2.0 * e0 + e_minus) / delta_sq))
280 })
281 .collect();
282
283 for res in diag_results {
284 let (i, val) = res?;
285 hessian.lock().unwrap()[(i, i)] = val;
286 }
287
288 let pairs: Vec<(usize, usize)> = (0..n3)
290 .flat_map(|i| ((i + 1)..n3).map(move |j| (i, j)))
291 .collect();
292
293 let offdiag_results: Vec<Result<(usize, usize, f64), String>> = pairs
294 .into_par_iter()
295 .map(|(i, j)| {
296 let mut cpp = coords.clone();
297 let mut cpm = coords.clone();
298 let mut cmp = coords.clone();
299 let mut cmm = coords.clone();
300 cpp[i] += delta;
301 cpp[j] += delta;
302 cpm[i] += delta;
303 cpm[j] -= delta;
304 cmp[i] -= delta;
305 cmp[j] += delta;
306 cmm[i] -= delta;
307 cmm[j] -= delta;
308
309 let e_pp = evaluate_energy(method, elements, &flat_to_positions(&cpp))?;
310 let e_pm = evaluate_energy(method, elements, &flat_to_positions(&cpm))?;
311 let e_mp = evaluate_energy(method, elements, &flat_to_positions(&cmp))?;
312 let e_mm = evaluate_energy(method, elements, &flat_to_positions(&cmm))?;
313 Ok((i, j, (e_pp - e_pm - e_mp + e_mm) / (4.0 * delta_sq)))
314 })
315 .collect();
316
317 {
318 let mut h = hessian.lock().unwrap();
319 for res in offdiag_results {
320 let (i, j, val) = res?;
321 h[(i, j)] = val;
322 h[(j, i)] = val;
323 }
324 enforce_symmetry(&mut h);
325 }
326
327 Ok(hessian.into_inner().unwrap())
328}
329
330#[cfg(feature = "parallel")]
334pub fn compute_uff_analytical_hessian_parallel(
335 smiles: &str,
336 coords_flat: &[f64],
337 step_size: Option<f64>,
338) -> Result<DMatrix<f64>, String> {
339 use rayon::prelude::*;
340 use std::sync::Mutex;
341
342 let mol = crate::graph::Molecule::from_smiles(smiles)?;
343 let n_atoms = mol.graph.node_count();
344 let n3 = 3 * n_atoms;
345
346 if coords_flat.len() != n3 {
347 return Err(format!(
348 "coords length {} != 3 * atoms {}",
349 coords_flat.len(),
350 n_atoms
351 ));
352 }
353
354 let ff = crate::forcefield::builder::build_uff_force_field(&mol);
355 let elements: Vec<u8> = (0..n_atoms)
356 .map(|i| mol.graph[petgraph::graph::NodeIndex::new(i)].element)
357 .collect();
358 let delta = step_size.unwrap_or_else(|| auto_step_size(&elements));
359
360 let hessian = Mutex::new(DMatrix::zeros(n3, n3));
361
362 let results: Vec<(usize, Vec<f64>)> = (0..n3)
363 .into_par_iter()
364 .map(|j| {
365 let mut cp = coords_flat.to_vec();
366 let mut cm = coords_flat.to_vec();
367 cp[j] += delta;
368 cm[j] -= delta;
369
370 let mut gp = vec![0.0; n3];
371 let mut gm = vec![0.0; n3];
372 ff.compute_system_energy_and_gradients(&cp, &mut gp);
373 ff.compute_system_energy_and_gradients(&cm, &mut gm);
374
375 let col: Vec<f64> = (0..n3).map(|i| (gp[i] - gm[i]) / (2.0 * delta)).collect();
376 (j, col)
377 })
378 .collect();
379
380 {
381 let mut h = hessian.lock().unwrap();
382 for (j, col) in results {
383 for i in 0..n3 {
384 h[(i, j)] = col[i];
385 }
386 }
387 enforce_symmetry(&mut h);
388 }
389
390 Ok(hessian.into_inner().unwrap())
391}
392
393fn evaluate_gradient(
396 method: HessianMethod,
397 elements: &[u8],
398 positions: &[[f64; 3]],
399) -> Result<Vec<f64>, String> {
400 match method {
401 HessianMethod::Pm3 => {
402 let grad_result = crate::pm3::gradients::compute_pm3_gradient(elements, positions)?;
403 let mut flat = Vec::with_capacity(positions.len() * 3);
404 for g in &grad_result.gradients {
405 flat.push(g[0]);
406 flat.push(g[1]);
407 flat.push(g[2]);
408 }
409 Ok(flat)
410 }
411 HessianMethod::Xtb => {
412 let grad_result = crate::xtb::gradients::compute_xtb_gradient(elements, positions)?;
413 let mut flat = Vec::with_capacity(positions.len() * 3);
414 for g in &grad_result.gradients {
415 flat.push(g[0]);
416 flat.push(g[1]);
417 flat.push(g[2]);
418 }
419 Ok(flat)
420 }
421 _ => Err(format!(
422 "Semi-analytical Hessian requires PM3 or xTB, got {:?}",
423 method
424 )),
425 }
426}
427
428pub fn compute_semianalytical_hessian(
435 elements: &[u8],
436 positions: &[[f64; 3]],
437 method: HessianMethod,
438 step_size: Option<f64>,
439) -> Result<DMatrix<f64>, String> {
440 let n_atoms = elements.len();
441 if n_atoms != positions.len() {
442 return Err("elements and positions length mismatch".to_string());
443 }
444
445 let n3 = 3 * n_atoms;
446 let delta = step_size.unwrap_or_else(|| auto_step_size(elements));
447
448 let mut coords: Vec<f64> = Vec::with_capacity(n3);
449 for pos in positions {
450 coords.extend_from_slice(pos);
451 }
452
453 let mut hessian = DMatrix::zeros(n3, n3);
454
455 for j in 0..n3 {
456 let mut coords_plus = coords.clone();
457 let mut coords_minus = coords.clone();
458 coords_plus[j] += delta;
459 coords_minus[j] -= delta;
460
461 let pos_plus = flat_to_positions(&coords_plus);
462 let pos_minus = flat_to_positions(&coords_minus);
463
464 let grad_plus = evaluate_gradient(method, elements, &pos_plus)?;
465 let grad_minus = evaluate_gradient(method, elements, &pos_minus)?;
466
467 for i in 0..n3 {
468 hessian[(i, j)] = (grad_plus[i] - grad_minus[i]) / (2.0 * delta);
469 }
470 }
471
472 enforce_symmetry(&mut hessian);
473 Ok(hessian)
474}
475
476#[cfg(feature = "parallel")]
478pub fn compute_semianalytical_hessian_parallel(
479 elements: &[u8],
480 positions: &[[f64; 3]],
481 method: HessianMethod,
482 step_size: Option<f64>,
483) -> Result<DMatrix<f64>, String> {
484 use rayon::prelude::*;
485
486 let n_atoms = elements.len();
487 if n_atoms != positions.len() {
488 return Err("elements and positions length mismatch".to_string());
489 }
490
491 let n3 = 3 * n_atoms;
492 let delta = step_size.unwrap_or_else(|| auto_step_size(elements));
493
494 let mut coords: Vec<f64> = Vec::with_capacity(n3);
495 for pos in positions {
496 coords.extend_from_slice(pos);
497 }
498
499 let results: Vec<Result<(usize, Vec<f64>), String>> = (0..n3)
500 .into_par_iter()
501 .map(|j| {
502 let mut cp = coords.clone();
503 let mut cm = coords.clone();
504 cp[j] += delta;
505 cm[j] -= delta;
506
507 let pp = flat_to_positions(&cp);
508 let pm = flat_to_positions(&cm);
509
510 let gp = evaluate_gradient(method, elements, &pp)?;
511 let gm = evaluate_gradient(method, elements, &pm)?;
512
513 let col: Vec<f64> = (0..n3).map(|i| (gp[i] - gm[i]) / (2.0 * delta)).collect();
514 Ok((j, col))
515 })
516 .collect();
517
518 let mut hessian = DMatrix::zeros(n3, n3);
519 for res in results {
520 let (j, col) = res?;
521 for i in 0..n3 {
522 hessian[(i, j)] = col[i];
523 }
524 }
525
526 enforce_symmetry(&mut hessian);
527 Ok(hessian)
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
535 fn test_hessian_h2_is_symmetric() {
536 let elements = [1u8, 1];
537 let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
538 let hessian =
539 compute_numerical_hessian(&elements, &positions, HessianMethod::Xtb, Some(0.005))
540 .unwrap();
541
542 assert_eq!(hessian.nrows(), 6);
543 assert_eq!(hessian.ncols(), 6);
544
545 for i in 0..6 {
547 for j in 0..6 {
548 assert!(
549 (hessian[(i, j)] - hessian[(j, i)]).abs() < 1e-4,
550 "Hessian not symmetric at ({}, {}): {} vs {}",
551 i,
552 j,
553 hessian[(i, j)],
554 hessian[(j, i)]
555 );
556 }
557 }
558 }
559
560 #[test]
561 fn test_hessian_water_shape() {
562 let elements = [8u8, 1, 1];
563 let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
564 let hessian =
565 compute_numerical_hessian(&elements, &positions, HessianMethod::Xtb, Some(0.005))
566 .unwrap();
567
568 assert_eq!(hessian.nrows(), 9);
569 assert_eq!(hessian.ncols(), 9);
570
571 let mut has_nonzero_offdiag = false;
573 for i in 0..9 {
574 for j in (i + 1)..9 {
575 if hessian[(i, j)].abs() > 1e-6 {
576 has_nonzero_offdiag = true;
577 break;
578 }
579 }
580 }
581 assert!(
582 has_nonzero_offdiag,
583 "Hessian should have non-zero off-diagonal elements"
584 );
585 }
586}