1use std::collections::VecDeque;
16
17use chematic_core::{AtomIdx, Molecule};
18
19use crate::mmff94_energy::{
20 mmff94_angle_energy, mmff94_bond_energy, mmff94_oop, mmff94_stbn, mmff94_torsion_energy,
21 mmff94_vdw_combined,
22};
23use crate::mmff94_numeric::{
24 NumericTypeError, assign_mmff94_numeric_types, mmff94_charges_numeric,
25};
26
27type CoordVec = Vec<[f64; 3]>;
28type LbfgsHistory = VecDeque<(CoordVec, CoordVec, f64)>;
29
30#[derive(Debug, Clone)]
34pub struct MinimizeResult {
35 pub energy: f64,
37 pub rmsd: f64,
39 pub converged: bool,
41 pub iterations: usize,
43}
44
45#[derive(Debug)]
47pub enum MinimizerError {
48 TypeAssignment(NumericTypeError),
49}
50
51impl From<NumericTypeError> for MinimizerError {
52 fn from(e: NumericTypeError) -> Self {
53 MinimizerError::TypeAssignment(e)
54 }
55}
56
57impl std::fmt::Display for MinimizerError {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 MinimizerError::TypeAssignment(e) => write!(f, "MMFF94 type assignment failed: {}", e),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Copy)]
69pub struct EnergyBreakdown {
70 pub bond: f64,
71 pub angle: f64,
72 pub stretch_bend: f64,
74 pub torsion: f64,
75 pub oop: f64,
77 pub vdw: f64,
78 pub electrostatic: f64,
79 pub total: f64,
80}
81
82pub fn mmff94_total_energy(mol: &Molecule, coords: &[[f64; 3]]) -> Result<f64, MinimizerError> {
87 let types = assign_mmff94_numeric_types(mol)?;
88 let charges = mmff94_charges_numeric(mol).unwrap_or_else(|_| vec![0.0; mol.atom_count()]);
89 Ok(total_energy(mol, coords, &types, &charges))
90}
91
92pub fn mmff94_torsion_scan(
97 mol: &Molecule,
98 coords: &[[f64; 3]],
99 atom_i: usize,
100 atom_j: usize,
101 atom_k: usize,
102 atom_l: usize,
103 steps: usize,
104) -> Result<Vec<(f64, f64)>, MinimizerError> {
105 let types = assign_mmff94_numeric_types(mol)?;
106 let charges = mmff94_charges_numeric(mol).unwrap_or_else(|_| vec![0.0; mol.atom_count()]);
107 let n = mol.atom_count();
108 let steps = steps.max(2);
109
110 let mut results = Vec::with_capacity(steps);
111
112 let moving_atoms: Vec<usize> = {
114 let mut visited = vec![false; n];
115 visited[atom_j] = true;
116 let mut queue = std::collections::VecDeque::new();
117 queue.push_back(atom_k);
118 visited[atom_k] = true;
119 let mut group = Vec::new();
120 while let Some(cur) = queue.pop_front() {
121 group.push(cur);
122 for (nb, _) in mol.neighbors(AtomIdx(cur as u32)) {
123 let nbi = nb.0 as usize;
124 if !visited[nbi] {
125 visited[nbi] = true;
126 queue.push_back(nbi);
127 }
128 }
129 }
130 group
131 };
132
133 let mut work = coords.to_vec();
134
135 let step_rad = 2.0 * std::f64::consts::PI / steps as f64;
137
138 for step in 0..steps {
139 let angle_deg = step as f64 * 360.0 / steps as f64;
140
141 if step > 0 {
142 let j = work[atom_j];
144 let k = work[atom_k];
145 let axis = {
146 let d = [k[0] - j[0], k[1] - j[1], k[2] - j[2]];
147 let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
148 if len < 1e-12 {
149 [1.0, 0.0, 0.0]
150 } else {
151 [d[0] / len, d[1] / len, d[2] / len]
152 }
153 };
154 let (sin_a, cos_a) = step_rad.sin_cos();
155 for &ai in &moving_atoms {
156 let p = [work[ai][0] - j[0], work[ai][1] - j[1], work[ai][2] - j[2]];
158 let cross = [
159 axis[1] * p[2] - axis[2] * p[1],
160 axis[2] * p[0] - axis[0] * p[2],
161 axis[0] * p[1] - axis[1] * p[0],
162 ];
163 let dot = axis[0] * p[0] + axis[1] * p[1] + axis[2] * p[2];
164 work[ai] = [
165 j[0] + cos_a * p[0] + sin_a * cross[0] + (1.0 - cos_a) * dot * axis[0],
166 j[1] + cos_a * p[1] + sin_a * cross[1] + (1.0 - cos_a) * dot * axis[1],
167 j[2] + cos_a * p[2] + sin_a * cross[2] + (1.0 - cos_a) * dot * axis[2],
168 ];
169 let _ = (atom_i, atom_l); }
171 }
172
173 let energy = total_energy(mol, &work, &types, &charges);
174 results.push((angle_deg, energy));
175 }
176
177 Ok(results)
178}
179
180pub fn mmff94_energy_breakdown(
182 mol: &Molecule,
183 coords: &[[f64; 3]],
184) -> Result<EnergyBreakdown, MinimizerError> {
185 let types = assign_mmff94_numeric_types(mol)?;
186 let charges = mmff94_charges_numeric(mol).unwrap_or_else(|_| vec![0.0; mol.atom_count()]);
187 let b = bond_energy(mol, coords, &types);
188 let a = angle_energy(mol, coords, &types);
189 let sb = stretch_bend_energy(mol, coords, &types);
190 let t = torsion_energy(mol, coords, &types);
191 let o = oop_energy(mol, coords, &types);
192 let v = vdw_energy(mol, coords, &types);
193 let e = elec_energy(mol, coords, &charges);
194 Ok(EnergyBreakdown {
195 bond: b,
196 angle: a,
197 stretch_bend: sb,
198 torsion: t,
199 oop: o,
200 vdw: v,
201 electrostatic: e,
202 total: b + a + sb + t + o + v + e,
203 })
204}
205
206pub fn minimize_mmff94_full(
216 mol: &Molecule,
217 coords: &mut [[f64; 3]],
218 max_iter: usize,
219) -> Result<MinimizeResult, MinimizerError> {
220 if mol.atom_count() <= 1 {
221 return Ok(MinimizeResult {
222 energy: 0.0,
223 rmsd: 0.0,
224 converged: true,
225 iterations: 0,
226 });
227 }
228
229 let types = assign_mmff94_numeric_types(mol)?;
230 let charges = mmff94_charges_numeric(mol).unwrap_or_else(|_| vec![0.0; mol.atom_count()]);
231
232 let n = mol.atom_count();
233 let initial = coords.to_vec();
234 let convergence = 1e-4_f64;
235 let step_size = 0.05_f64;
236 let delta = 1e-4_f64;
237
238 let mut iters = 0usize;
239 let mut converged = false;
240
241 for _ in 0..max_iter {
242 iters += 1;
243 let grad = compute_gradient(mol, coords, &types, &charges, delta);
244 let max_g = grad
245 .iter()
246 .flat_map(|v| v.iter())
247 .map(|x| x.abs())
248 .fold(0.0_f64, f64::max);
249
250 if max_g < convergence {
251 converged = true;
252 break;
253 }
254
255 let scale = step_size / max_g.max(1e-8);
256 for i in 0..n {
257 for axis in 0..3 {
258 coords[i][axis] -= scale * grad[i][axis];
259 }
260 }
261 }
262
263 let energy = total_energy(mol, coords, &types, &charges);
264
265 let rmsd = {
266 let sum: f64 = coords
267 .iter()
268 .zip(initial.iter())
269 .map(|(c, i0)| {
270 let dx = c[0] - i0[0];
271 let dy = c[1] - i0[1];
272 let dz = c[2] - i0[2];
273 dx * dx + dy * dy + dz * dz
274 })
275 .sum();
276 (sum / n as f64).sqrt()
277 };
278
279 Ok(MinimizeResult {
280 energy,
281 rmsd,
282 converged,
283 iterations: iters,
284 })
285}
286
287pub fn minimize_mmff94_lbfgs(
295 mol: &Molecule,
296 coords: &mut [[f64; 3]],
297 max_iter: usize,
298) -> Result<MinimizeResult, MinimizerError> {
299 const M: usize = 5; const DELTA: f64 = 1e-4; const CONVERGENCE: f64 = 1e-4; const C_ARMIJO: f64 = 1e-4; const TAU: f64 = 0.5; if mol.atom_count() <= 1 {
306 return Ok(MinimizeResult {
307 energy: 0.0,
308 rmsd: 0.0,
309 converged: true,
310 iterations: 0,
311 });
312 }
313
314 let types = assign_mmff94_numeric_types(mol)?;
315 let charges = mmff94_charges_numeric(mol).unwrap_or_else(|_| vec![0.0; mol.atom_count()]);
316
317 let n = mol.atom_count();
318 let initial = coords.to_vec();
319
320 let mut history: LbfgsHistory = VecDeque::new();
322
323 let mut g = compute_gradient(mol, coords, &types, &charges, DELTA);
324 let mut f0 = total_energy(mol, coords, &types, &charges);
325
326 let mut iters = 0usize;
327 let mut converged = false;
328
329 for _ in 0..max_iter {
330 iters += 1;
331
332 let max_g = g
334 .iter()
335 .flat_map(|v| v.iter())
336 .map(|x| x.abs())
337 .fold(0.0_f64, f64::max);
338 if max_g < CONVERGENCE {
339 converged = true;
340 break;
341 }
342
343 let p = lbfgs_direction(&g, &history);
345
346 let gp: f64 = g.iter().zip(p.iter()).map(|(gi, pi)| dot3(*gi, *pi)).sum();
348 let mut alpha = 1.0_f64;
349 let new_coords = loop {
350 let trial: Vec<[f64; 3]> = coords
351 .iter()
352 .zip(p.iter())
353 .map(|(c, pi)| {
354 [
355 c[0] + alpha * pi[0],
356 c[1] + alpha * pi[1],
357 c[2] + alpha * pi[2],
358 ]
359 })
360 .collect();
361 let f_trial = total_energy(mol, &trial, &types, &charges);
362 if f_trial <= f0 + C_ARMIJO * alpha * gp {
363 break trial;
364 }
365 alpha *= TAU;
366 if alpha < 1e-12 {
367 let scale = 0.01 / max_g.max(1e-8);
369 break coords
370 .iter()
371 .zip(g.iter())
372 .map(|(c, gi)| {
373 [
374 c[0] - scale * gi[0],
375 c[1] - scale * gi[1],
376 c[2] - scale * gi[2],
377 ]
378 })
379 .collect();
380 }
381 };
382
383 let g_new = compute_gradient(mol, &new_coords, &types, &charges, DELTA);
385 let f_new = total_energy(mol, &new_coords, &types, &charges);
386
387 let s: Vec<[f64; 3]> = new_coords
389 .iter()
390 .zip(coords.iter())
391 .map(|(xn, xo)| [xn[0] - xo[0], xn[1] - xo[1], xn[2] - xo[2]])
392 .collect();
393 let y: Vec<[f64; 3]> = g_new
394 .iter()
395 .zip(g.iter())
396 .map(|(gn, go)| [gn[0] - go[0], gn[1] - go[1], gn[2] - go[2]])
397 .collect();
398 let ys: f64 = y.iter().zip(s.iter()).map(|(yi, si)| dot3(*yi, *si)).sum();
399
400 if ys > 1e-10 {
402 if history.len() >= M {
403 history.pop_front();
404 }
405 history.push_back((s, y, 1.0 / ys));
406 }
407
408 coords.copy_from_slice(&new_coords);
409 g = g_new;
410 f0 = f_new;
411 }
412
413 let rmsd = {
414 let sum: f64 = coords
415 .iter()
416 .zip(initial.iter())
417 .map(|(c, i0)| {
418 let dx = c[0] - i0[0];
419 let dy = c[1] - i0[1];
420 let dz = c[2] - i0[2];
421 dx * dx + dy * dy + dz * dz
422 })
423 .sum();
424 (sum / n as f64).sqrt()
425 };
426
427 Ok(MinimizeResult {
428 energy: f0,
429 rmsd,
430 converged,
431 iterations: iters,
432 })
433}
434
435fn lbfgs_direction(g: &[[f64; 3]], history: &LbfgsHistory) -> Vec<[f64; 3]> {
437 let m = history.len();
438
439 if m == 0 {
440 return g.iter().map(|gi| [-gi[0], -gi[1], -gi[2]]).collect();
442 }
443
444 let mut q: Vec<[f64; 3]> = g.to_vec();
445 let mut alphas = vec![0.0_f64; m];
446
447 for i in (0..m).rev() {
449 let (s, y, rho) = &history[i];
450 let sq: f64 = s.iter().zip(q.iter()).map(|(si, qi)| dot3(*si, *qi)).sum();
451 alphas[i] = rho * sq;
452 let a = alphas[i];
453 for (qi, yi) in q.iter_mut().zip(y.iter()) {
454 qi[0] -= a * yi[0];
455 qi[1] -= a * yi[1];
456 qi[2] -= a * yi[2];
457 }
458 }
459
460 let (s_last, y_last, _) = &history[m - 1];
462 let sy: f64 = s_last
463 .iter()
464 .zip(y_last.iter())
465 .map(|(si, yi)| dot3(*si, *yi))
466 .sum();
467 let yy: f64 = y_last.iter().map(|yi| dot3(*yi, *yi)).sum();
468 let gamma = if yy > 1e-20 { sy / yy } else { 1.0 };
469 for qi in q.iter_mut() {
470 qi[0] *= gamma;
471 qi[1] *= gamma;
472 qi[2] *= gamma;
473 }
474
475 for i in 0..m {
477 let (s, y, rho) = &history[i];
478 let yr: f64 = y.iter().zip(q.iter()).map(|(yi, ri)| dot3(*yi, *ri)).sum();
479 let beta = rho * yr;
480 let diff = alphas[i] - beta;
481 for (qi, si) in q.iter_mut().zip(s.iter()) {
482 qi[0] += diff * si[0];
483 qi[1] += diff * si[1];
484 qi[2] += diff * si[2];
485 }
486 }
487
488 q.iter().map(|qi| [-qi[0], -qi[1], -qi[2]]).collect()
490}
491
492fn compute_gradient(
494 mol: &Molecule,
495 coords: &[[f64; 3]],
496 types: &[u8],
497 charges: &[f64],
498 delta: f64,
499) -> Vec<[f64; 3]> {
500 let n = coords.len();
501 let mut grad = vec![[0.0_f64; 3]; n];
502 let mut work = coords.to_vec();
503 for i in 0..n {
504 for axis in 0..3 {
505 work[i][axis] += delta;
506 let ep = total_energy(mol, &work, types, charges);
507 work[i][axis] -= 2.0 * delta;
508 let em = total_energy(mol, &work, types, charges);
509 work[i][axis] += delta;
510 grad[i][axis] = (ep - em) / (2.0 * delta);
511 }
512 }
513 grad
514}
515
516fn total_energy(mol: &Molecule, coords: &[[f64; 3]], types: &[u8], charges: &[f64]) -> f64 {
519 bond_energy(mol, coords, types)
520 + angle_energy(mol, coords, types)
521 + stretch_bend_energy(mol, coords, types)
522 + torsion_energy(mol, coords, types)
523 + oop_energy(mol, coords, types)
524 + vdw_energy(mol, coords, types)
525 + elec_energy(mol, coords, charges)
526}
527
528fn stretch_bend_energy(mol: &Molecule, coords: &[[f64; 3]], types: &[u8]) -> f64 {
531 const CONV: f64 = 2.51210; const RAD_TO_DEG: f64 = 180.0 / std::f64::consts::PI;
533 const KB_CONV: f64 = 143.9325;
534 const CS: f64 = 2.0;
535 let mut energy = 0.0;
536 for j_idx in 0..mol.atom_count() {
537 let j = AtomIdx(j_idx as u32);
538 let neighbors: Vec<usize> = mol.neighbors(j).map(|(nb, _)| nb.0 as usize).collect();
539 if neighbors.len() < 2 {
540 continue;
541 }
542 let at = angle_type_for(types[j_idx]);
543 for (ii, &i) in neighbors.iter().enumerate() {
544 for &k in &neighbors[ii + 1..] {
545 if let Some((kba_ijk, kba_kji)) = mmff94_stbn(at, types[i], types[j_idx], types[k])
546 {
547 let r_ij = dist(coords[i], coords[j_idx]);
549 let bt_ij = bond_type_for(types[i], types[j_idx]);
550 let dr_ij = if let Some(p) = mmff94_bond_energy(bt_ij, types[i], types[j_idx]) {
551 r_ij - p.r0
552 } else {
553 0.0
554 };
555 let r_kj = dist(coords[k], coords[j_idx]);
557 let bt_kj = bond_type_for(types[k], types[j_idx]);
558 let dr_kj = if let Some(p) = mmff94_bond_energy(bt_kj, types[k], types[j_idx]) {
559 r_kj - p.r0
560 } else {
561 0.0
562 };
563 let cos_t = cos_angle(coords[i], coords[j_idx], coords[k]);
565 if let Some(ap) = mmff94_angle_energy(at, types[i], types[j_idx], types[k]) {
566 let dtheta = cos_t.acos() * RAD_TO_DEG - ap.theta0;
567 energy += CONV * (kba_ijk * dr_ij + kba_kji * dr_kj) * dtheta;
568 }
569 let _ = (KB_CONV, CS); }
571 }
572 }
573 }
574 energy
575}
576
577fn oop_energy(mol: &Molecule, coords: &[[f64; 3]], types: &[u8]) -> f64 {
580 const CONV: f64 = 0.043844;
581 const RAD_TO_DEG: f64 = 180.0 / std::f64::consts::PI;
582 const SP2_TYPES: &[u8] = &[
584 2, 3, 9, 10, 30, 37, 38, 39, 40, 41, 43, 45, 49, 54, 56, 57, 58, 59, 63, 64, 65, 66, 67,
585 76, 78, 79, 80, 81, 82,
586 ];
587 let mut energy = 0.0;
588 for j_idx in 0..mol.atom_count() {
589 if SP2_TYPES.binary_search(&types[j_idx]).is_err() {
590 continue;
591 }
592 let j = AtomIdx(j_idx as u32);
593 let neighbors: Vec<usize> = mol.neighbors(j).map(|(nb, _)| nb.0 as usize).collect();
594 if neighbors.len() != 3 {
595 continue; }
597 let [i, k, l] = [neighbors[0], neighbors[1], neighbors[2]];
598 if let Some(koop) = mmff94_oop(types[j_idx], types[i], types[k], types[l]) {
599 let pj = coords[j_idx];
601 let pi = coords[i];
602 let pk = coords[k];
603 let pl = coords[l];
604 let rji = [pi[0] - pj[0], pi[1] - pj[1], pi[2] - pj[2]];
605 let rjk = [pk[0] - pj[0], pk[1] - pj[1], pk[2] - pj[2]];
606 let rjl = [pl[0] - pj[0], pl[1] - pj[1], pl[2] - pj[2]];
607 let n = cross(rji, rjk); let n_len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
609 let l_len = (rjl[0] * rjl[0] + rjl[1] * rjl[1] + rjl[2] * rjl[2]).sqrt();
610 if n_len < 1e-12 || l_len < 1e-12 {
611 continue;
612 }
613 let sin_chi = dot3(n, rjl) / (n_len * l_len);
614 let chi_deg = sin_chi.clamp(-1.0, 1.0).asin() * RAD_TO_DEG;
615 energy += (CONV * koop / 2.0) * chi_deg * chi_deg;
616 }
617 }
618 energy
619}
620
621fn bond_energy(mol: &Molecule, coords: &[[f64; 3]], types: &[u8]) -> f64 {
624 const KB_CONV: f64 = 143.9325;
625 const CS: f64 = 2.0;
626 let mut energy = 0.0;
627 for (_, bond) in mol.bonds() {
628 let i = bond.atom1.0 as usize;
629 let j = bond.atom2.0 as usize;
630 let bt = bond_type_for(types[i], types[j]);
631 if let Some(p) = mmff94_bond_energy(bt, types[i], types[j]) {
632 let r = dist(coords[i], coords[j]);
633 let dr = r - p.r0;
634 let cubic = 1.0 - CS * dr + (7.0 / 12.0) * CS * CS * dr * dr;
635 energy += (KB_CONV * p.kb / 2.0) * dr * dr * cubic;
636 }
637 }
638 energy
639}
640
641fn angle_energy(mol: &Molecule, coords: &[[f64; 3]], types: &[u8]) -> f64 {
644 const KA_CONV: f64 = 0.043844;
645 const RAD_TO_DEG: f64 = 180.0 / std::f64::consts::PI;
646 let mut energy = 0.0;
647 for j_idx in 0..mol.atom_count() {
648 let j = AtomIdx(j_idx as u32);
649 let neighbors: Vec<usize> = mol.neighbors(j).map(|(nb, _)| nb.0 as usize).collect();
650 if neighbors.len() < 2 {
651 continue;
652 }
653 let at = angle_type_for(types[j_idx]);
654 for (ii, &i) in neighbors.iter().enumerate() {
655 for &k in &neighbors[ii + 1..] {
656 if let Some(p) = mmff94_angle_energy(at, types[i], types[j_idx], types[k]) {
657 let cos_t = cos_angle(coords[i], coords[j_idx], coords[k]);
658 let theta_deg = cos_t.acos() * RAD_TO_DEG;
659 let dt = theta_deg - p.theta0;
660 let cubic = 1.0 - 0.007 * dt;
661 energy += (KA_CONV * p.ka / 2.0) * dt * dt * cubic;
662 }
663 }
664 }
665 }
666 energy
667}
668
669fn torsion_energy(mol: &Molecule, coords: &[[f64; 3]], types: &[u8]) -> f64 {
672 let mut energy = 0.0;
673 for (_, bond) in mol.bonds() {
674 let j = bond.atom1.0 as usize;
675 let k = bond.atom2.0 as usize;
676 let nbrs_j: Vec<usize> = mol
677 .neighbors(bond.atom1)
678 .map(|(nb, _)| nb.0 as usize)
679 .collect();
680 let nbrs_k: Vec<usize> = mol
681 .neighbors(bond.atom2)
682 .map(|(nb, _)| nb.0 as usize)
683 .collect();
684 let tt = torsion_type_for(types[j], types[k]);
685 for &i in &nbrs_j {
686 if i == k {
687 continue;
688 }
689 for &l in &nbrs_k {
690 if l == j {
691 continue;
692 }
693 if let Some(p) = mmff94_torsion_energy(tt, types[i], types[j], types[k], types[l]) {
694 let phi = dihedral(coords[i], coords[j], coords[k], coords[l]);
695 energy += 0.5 * p.v1 * (1.0 + phi.cos())
696 + 0.5 * p.v2 * (1.0 - (2.0 * phi).cos())
697 + 0.5 * p.v3 * (1.0 + (3.0 * phi).cos());
698 }
699 }
700 }
701 }
702 energy
703}
704
705fn vdw_energy(mol: &Molecule, coords: &[[f64; 3]], types: &[u8]) -> f64 {
709 let n = mol.atom_count();
710 let mut excl = std::collections::HashSet::new();
711 for (_, bond) in mol.bonds() {
712 let i = bond.atom1.0 as usize;
713 let j = bond.atom2.0 as usize;
714 excl.insert((i.min(j), i.max(j)));
715 for (nb_i, _) in mol.neighbors(bond.atom1) {
716 let ni = nb_i.0 as usize;
717 excl.insert((ni.min(j), ni.max(j)));
718 }
719 for (nb_j, _) in mol.neighbors(bond.atom2) {
720 let nj = nb_j.0 as usize;
721 excl.insert((i.min(nj), i.max(nj)));
722 }
723 }
724 let cutoff = 10.0_f64;
725 let mut energy = 0.0;
726 for i in 0..n {
727 for j in (i + 1)..n {
728 if excl.contains(&(i, j)) {
729 continue;
730 }
731 let r = dist(coords[i], coords[j]);
732 if r > cutoff {
733 continue;
734 }
735 if let Some((r_star, eps)) = mmff94_vdw_combined(types[i], types[j])
736 && r_star > 0.0
737 && eps > 0.0
738 && r > 0.01
739 {
740 let t = (1.07 * r_star) / (r + 0.07 * r_star);
741 let t7 = t.powi(7);
742 energy += eps * t7 * (t7 - 2.0);
743 }
744 }
745 }
746 energy
747}
748
749fn elec_energy(mol: &Molecule, coords: &[[f64; 3]], charges: &[f64]) -> f64 {
752 const COULOMB: f64 = 332.0716;
753 const DELTA: f64 = 0.05;
754 let n = mol.atom_count();
755 let mut excl = std::collections::HashSet::new();
756 for (_, bond) in mol.bonds() {
757 let i = bond.atom1.0 as usize;
758 let j = bond.atom2.0 as usize;
759 excl.insert((i.min(j), i.max(j)));
760 for (nb_i, _) in mol.neighbors(bond.atom1) {
761 excl.insert(((nb_i.0 as usize).min(j), (nb_i.0 as usize).max(j)));
762 }
763 for (nb_j, _) in mol.neighbors(bond.atom2) {
764 excl.insert((i.min(nb_j.0 as usize), i.max(nb_j.0 as usize)));
765 }
766 }
767 let mut one_four = std::collections::HashSet::new();
769 for (_, bond) in mol.bonds() {
770 let j = bond.atom1.0 as usize;
771 let k = bond.atom2.0 as usize;
772 for (nb_j, _) in mol.neighbors(bond.atom1) {
773 let i = nb_j.0 as usize;
774 if i == k {
775 continue;
776 }
777 for (nb_k, _) in mol.neighbors(bond.atom2) {
778 let l = nb_k.0 as usize;
779 if l == j {
780 continue;
781 }
782 let key = (i.min(l), i.max(l));
783 if !excl.contains(&key) {
784 one_four.insert(key);
785 }
786 }
787 }
788 }
789 let mut energy = 0.0;
790 for i in 0..n {
791 for j in (i + 1)..n {
792 if excl.contains(&(i, j)) {
793 continue;
794 }
795 let r = dist(coords[i], coords[j]);
796 let scale = if one_four.contains(&(i, j)) {
797 0.75
798 } else {
799 1.0
800 };
801 energy += scale * COULOMB * charges[i] * charges[j] / (r + DELTA);
802 }
803 }
804 energy
805}
806
807#[inline]
810fn dist(a: [f64; 3], b: [f64; 3]) -> f64 {
811 let dx = a[0] - b[0];
812 let dy = a[1] - b[1];
813 let dz = a[2] - b[2];
814 (dx * dx + dy * dy + dz * dz).sqrt()
815}
816
817#[inline]
818fn cos_angle(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
819 let ba = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
820 let bc = [c[0] - b[0], c[1] - b[1], c[2] - b[2]];
821 let dot_val = ba[0] * bc[0] + ba[1] * bc[1] + ba[2] * bc[2];
822 let na = (ba[0] * ba[0] + ba[1] * ba[1] + ba[2] * ba[2]).sqrt();
823 let nc = (bc[0] * bc[0] + bc[1] * bc[1] + bc[2] * bc[2]).sqrt();
824 if na < 1e-12 || nc < 1e-12 {
825 return 0.0;
826 }
827 (dot_val / (na * nc)).clamp(-1.0, 1.0)
828}
829
830#[inline]
832fn dihedral(i: [f64; 3], j: [f64; 3], k: [f64; 3], l: [f64; 3]) -> f64 {
833 let b1 = [j[0] - i[0], j[1] - i[1], j[2] - i[2]];
834 let b2 = [k[0] - j[0], k[1] - j[1], k[2] - j[2]];
835 let b3 = [l[0] - k[0], l[1] - k[1], l[2] - k[2]];
836 let n1 = cross(b1, b2);
837 let n2 = cross(b2, b3);
838 let m1 = cross(n1, b2);
839 let b2_len = (b2[0] * b2[0] + b2[1] * b2[1] + b2[2] * b2[2]).sqrt();
840 if b2_len < 1e-12 {
841 return 0.0;
842 }
843 let x = dot3(n1, n2);
844 let y = dot3(m1, n2) / b2_len;
845 y.atan2(x)
846}
847
848#[inline]
849fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
850 [
851 a[1] * b[2] - a[2] * b[1],
852 a[2] * b[0] - a[0] * b[2],
853 a[0] * b[1] - a[1] * b[0],
854 ]
855}
856
857#[inline]
858fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
859 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
860}
861
862fn bond_type_for(ti: u8, tj: u8) -> u8 {
866 const SP2: &[u8] = &[
867 2, 3, 9, 10, 37, 38, 39, 40, 41, 56, 57, 58, 59, 63, 64, 65, 66, 67, 78, 79, 80, 81, 82,
868 ];
869 if SP2.binary_search(&ti).is_ok() || SP2.binary_search(&tj).is_ok() {
870 1
871 } else {
872 0
873 }
874}
875
876fn angle_type_for(_tj: u8) -> u8 {
878 0
879}
880
881fn torsion_type_for(tj: u8, tk: u8) -> u8 {
883 const SP2: &[u8] = &[
884 2, 3, 9, 10, 37, 38, 39, 40, 41, 56, 57, 58, 59, 63, 64, 65, 66, 67, 78, 79, 80, 81, 82,
885 ];
886 match (
887 SP2.binary_search(&tj).is_ok(),
888 SP2.binary_search(&tk).is_ok(),
889 ) {
890 (false, false) => 0, (true, false) | (false, true) => 1, (true, true) => 2, }
894}
895
896#[cfg(test)]
899mod tests {
900 use super::*;
901 use chematic_core::molecule::MoleculeBuilder;
902 use chematic_core::{Atom, BondOrder, Element};
903
904 fn methane_mol() -> (Molecule, Vec<[f64; 3]>) {
905 let mut b = MoleculeBuilder::new();
906 let c = b.add_atom(Atom::new(Element::C));
907 let h1 = b.add_atom(Atom::new(Element::H));
908 let h2 = b.add_atom(Atom::new(Element::H));
909 let h3 = b.add_atom(Atom::new(Element::H));
910 let h4 = b.add_atom(Atom::new(Element::H));
911 b.add_bond(c, h1, BondOrder::Single).unwrap();
912 b.add_bond(c, h2, BondOrder::Single).unwrap();
913 b.add_bond(c, h3, BondOrder::Single).unwrap();
914 b.add_bond(c, h4, BondOrder::Single).unwrap();
915 let mol = b.build();
916 let coords = vec![
917 [0.0, 0.0, 0.0],
918 [0.630, 0.630, 0.630],
919 [-0.630, -0.630, 0.630],
920 [-0.630, 0.630, -0.630],
921 [0.630, -0.630, -0.630],
922 ];
923 (mol, coords)
924 }
925
926 fn butane_backbone() -> Molecule {
927 let mut b = MoleculeBuilder::new();
928 let c0 = b.add_atom(Atom::new(Element::C));
929 let c1 = b.add_atom(Atom::new(Element::C));
930 let c2 = b.add_atom(Atom::new(Element::C));
931 let c3 = b.add_atom(Atom::new(Element::C));
932 b.add_bond(c0, c1, BondOrder::Single).unwrap();
933 b.add_bond(c1, c2, BondOrder::Single).unwrap();
934 b.add_bond(c2, c3, BondOrder::Single).unwrap();
935 b.build()
936 }
937
938 #[test]
939 fn energy_is_finite_for_methane() {
940 let (mol, coords) = methane_mol();
941 let e = mmff94_total_energy(&mol, &coords).expect("energy");
942 assert!(e.is_finite(), "energy={}", e);
943 }
944
945 #[test]
946 fn torsion_differs_by_conformation() {
947 let mol = butane_backbone();
948 let types = assign_mmff94_numeric_types(&mol).expect("types");
949 let coords_gauche = vec![
951 [0.0, 0.0, 0.0_f64],
952 [1.508, 0.0, 0.0],
953 [2.016, 1.192, 0.688],
954 [3.524, 1.192, 0.688],
955 ];
956 let coords_anti = vec![
958 [0.0, 0.0, 0.0_f64],
959 [1.508, 0.0, 0.0],
960 [3.016, 0.0, 0.0],
961 [4.524, 0.0, 0.0],
962 ];
963 let e_gauche = torsion_energy(&mol, &coords_gauche, &types);
964 let e_anti = torsion_energy(&mol, &coords_anti, &types);
965 assert!(e_gauche.is_finite());
966 assert!(e_anti.is_finite());
967 assert!(
968 (e_gauche - e_anti).abs() > 1e-6,
969 "torsion must differ: gauche={}, anti={}",
970 e_gauche,
971 e_anti
972 );
973 }
974
975 #[test]
976 fn vdw_more_repulsive_at_short_range() {
977 let mol = butane_backbone();
978 let types = assign_mmff94_numeric_types(&mol).expect("types");
979 let coords_close = vec![
981 [0.0, 0.0, 0.0_f64],
982 [1.5, 0.0, 0.0],
983 [3.0, 0.0, 0.0],
984 [0.5, 0.0, 0.0], ];
986 let coords_far = vec![
987 [0.0, 0.0, 0.0_f64],
988 [1.5, 0.0, 0.0],
989 [3.0, 0.0, 0.0],
990 [8.0, 0.0, 0.0],
991 ];
992 let e_close = vdw_energy(&mol, &coords_close, &types);
993 let e_far = vdw_energy(&mol, &coords_far, &types);
994 assert!(e_close.is_finite());
995 assert!(e_far.is_finite());
996 assert!(e_close > e_far, "close={} should > far={}", e_close, e_far);
997 }
998
999 #[test]
1000 fn dihedral_anti_is_pi() {
1001 let i = [0.0_f64, 0.0, 0.0];
1002 let j = [1.0, 0.0, 0.0];
1003 let k = [2.0, 0.0, 1.0];
1004 let l = [3.0, 0.0, 0.0];
1005 let phi = dihedral(i, j, k, l);
1006 assert!(phi.abs() > 2.5, "anti dihedral ≈ π: {}", phi);
1007 }
1008
1009 #[test]
1010 fn dihedral_syn_is_zero() {
1011 let i = [0.0_f64, 1.0, 0.0];
1012 let j = [0.0, 0.0, 0.0];
1013 let k = [1.0, 0.0, 0.0];
1014 let l = [1.0, 1.0, 0.0];
1015 let phi = dihedral(i, j, k, l);
1016 assert!(phi.abs() < 0.1, "syn dihedral ≈ 0: {}", phi);
1017 }
1018
1019 #[test]
1020 fn minimize_reduces_energy_for_methane() {
1021 let (mol, _) = methane_mol();
1022 let mut coords = vec![
1023 [0.0, 0.0, 0.0_f64],
1024 [1.5, 1.5, 1.5],
1025 [-1.5, -1.5, 1.5],
1026 [-1.5, 1.5, -1.5],
1027 [1.5, -1.5, -1.5],
1028 ];
1029 let e_before = mmff94_total_energy(&mol, &coords).expect("energy before");
1030 let result = minimize_mmff94_full(&mol, &mut coords, 300).expect("minimize");
1031 assert!(
1032 result.energy <= e_before,
1033 "minimize should reduce energy: {} → {}",
1034 e_before,
1035 result.energy
1036 );
1037 assert!(result.energy.is_finite());
1038 assert!(result.iterations > 0);
1039 }
1040
1041 #[test]
1042 fn lbfgs_reduces_energy_for_methane() {
1043 let (mol, _) = methane_mol();
1044 let mut coords = vec![
1045 [0.0, 0.0, 0.0_f64],
1046 [1.5, 1.5, 1.5],
1047 [-1.5, -1.5, 1.5],
1048 [-1.5, 1.5, -1.5],
1049 [1.5, -1.5, -1.5],
1050 ];
1051 let e_before = mmff94_total_energy(&mol, &coords).expect("energy before");
1052 let result = minimize_mmff94_lbfgs(&mol, &mut coords, 300).expect("lbfgs");
1053 assert!(
1054 result.energy <= e_before,
1055 "L-BFGS should reduce energy: {} → {}",
1056 e_before,
1057 result.energy
1058 );
1059 assert!(result.energy.is_finite());
1060 }
1061
1062 #[test]
1063 fn lbfgs_converges_in_fewer_iters_than_sd() {
1064 let (mol, _) = methane_mol();
1065 let base_coords = vec![
1067 [0.0, 0.0, 0.0_f64],
1068 [1.2, 1.2, 1.2],
1069 [-1.2, -1.2, 1.2],
1070 [-1.2, 1.2, -1.2],
1071 [1.2, -1.2, -1.2],
1072 ];
1073 let mut coords_sd = base_coords.clone();
1074 let mut coords_lbfgs = base_coords;
1075 let sd = minimize_mmff94_full(&mol, &mut coords_sd, 500).expect("sd");
1076 let lb = minimize_mmff94_lbfgs(&mol, &mut coords_lbfgs, 500).expect("lbfgs");
1077 assert!(
1079 lb.iterations <= sd.iterations || lb.converged,
1080 "L-BFGS iters={} SD iters={}",
1081 lb.iterations,
1082 sd.iterations
1083 );
1084 assert!(lb.energy.is_finite());
1085 }
1086
1087 #[test]
1088 fn energy_breakdown_sums_to_total() {
1089 let (mol, coords) = methane_mol();
1090 let bd = mmff94_energy_breakdown(&mol, &coords).expect("breakdown");
1091 let sum =
1092 bd.bond + bd.angle + bd.stretch_bend + bd.torsion + bd.oop + bd.vdw + bd.electrostatic;
1093 assert!(
1094 (sum - bd.total).abs() < 1e-10,
1095 "sum={} total={}",
1096 sum,
1097 bd.total
1098 );
1099 assert!(bd.total.is_finite());
1100 }
1101
1102 #[test]
1103 fn energy_breakdown_bond_term_positive_for_distorted() {
1104 let (mol, _) = methane_mol();
1105 let stretched = vec![
1107 [0.0, 0.0, 0.0_f64],
1108 [2.0, 2.0, 2.0],
1109 [-2.0, -2.0, 2.0],
1110 [-2.0, 2.0, -2.0],
1111 [2.0, -2.0, -2.0],
1112 ];
1113 let bd = mmff94_energy_breakdown(&mol, &stretched).expect("breakdown");
1114 assert!(
1115 bd.bond > 0.0,
1116 "stretched bond energy should be positive: {}",
1117 bd.bond
1118 );
1119 }
1120}