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