1use super::traits::ForceFieldContribution;
2use petgraph::visit::EdgeRef;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[repr(u8)]
13pub enum Mmff94AtomType {
14 CR = 1, CSp2 = 2, CSp = 3, CO = 4, HC = 5, OR = 6, O2 = 7, NR = 8, N2 = 9, NC = 10, F = 11, Cl = 12, Br = 13, I = 14, S = 15, SdO = 16, SO = 17, SO2 = 18, SI = 19, CR4R = 20, HO = 21, CR3R = 22, HN = 23, HOCO = 24, P = 25, HaP = 26, HOS = 28, NPl3 = 30, ON2 = 31, OX = 32, OM = 33, HNR = 34, HIM = 35, CB = 37, NPOX = 38, NR2 = 39, NAm = 40, NSO = 41, Sthi = 44, NdOx = 45, NPl = 46, C5A = 63, C5B = 64, N5A = 65, N5B = 66, NAZT = 47, NSP = 48, N5M = 49, N2OX = 50, N3OX = 51, NPYD = 52, O5 = 59, Fe2 = 87, Fe3 = 88, HS = 71, HP = 72, PO = 75, Unknown = 0,
72}
73
74pub fn assign_mmff94_type_smarts(mol: &crate::graph::Molecule, atom_idx: usize) -> Mmff94AtomType {
79 use crate::graph::{BondOrder, Hybridization};
80 use petgraph::graph::NodeIndex;
81
82 let node = NodeIndex::new(atom_idx);
83 let atom = &mol.graph[node];
84 let element = atom.element;
85 let hyb = &atom.hybridization;
86
87 let neighbors: Vec<NodeIndex> = mol.graph.neighbors(node).collect();
88 let degree = neighbors.len();
89
90 let nb_elem = |ni: NodeIndex| mol.graph[ni].element;
92
93 let count_nb_elem = |z: u8| neighbors.iter().filter(|&&n| nb_elem(n) == z).count();
95
96 let count_double_bonds = || -> usize {
98 mol.graph
99 .edges(node)
100 .filter(|e| e.weight().order == BondOrder::Double)
101 .count()
102 };
103
104 let in_ring_of_size = |size: usize| -> bool { atom_in_ring_of_size(mol, atom_idx, size) };
106
107 let is_aromatic = is_atom_aromatic_mmff(mol, atom_idx);
109
110 let nb_has_double_bond_to = |ni: NodeIndex, target_z: u8| -> bool {
112 mol.graph.edges(ni).any(|e| {
113 e.weight().order == BondOrder::Double && {
114 let other = if e.source() == ni {
115 e.target()
116 } else {
117 e.source()
118 };
119 mol.graph[other].element == target_z
120 }
121 })
122 };
123
124 let adjacent_to_carbonyl = || -> bool {
126 neighbors
127 .iter()
128 .any(|&ni| nb_elem(ni) == 6 && nb_has_double_bond_to(ni, 8))
129 };
130
131 match element {
132 1 => {
134 if degree == 0 {
135 return Mmff94AtomType::HC;
136 }
137 let parent = neighbors[0];
138 let parent_z = nb_elem(parent);
139 match parent_z {
140 6 => Mmff94AtomType::HC, 7 => {
142 if is_atom_aromatic_mmff(mol, parent.index()) && in_ring_of_size(5) {
144 Mmff94AtomType::HIM } else if atom_in_any_ring(mol, parent.index()) {
146 Mmff94AtomType::HNR } else {
148 Mmff94AtomType::HN
149 }
150 }
151 8 => {
152 let parent_nbs: Vec<NodeIndex> = mol.graph.neighbors(parent).collect();
154 for &pnb in &parent_nbs {
155 if pnb.index() == atom_idx {
156 continue;
157 }
158 let pnb_z = nb_elem(pnb);
159 if pnb_z == 6 && nb_has_double_bond_to(pnb, 8) {
160 return Mmff94AtomType::HOCO; }
162 if pnb_z == 16 {
163 return Mmff94AtomType::HOS; }
165 }
166 Mmff94AtomType::HO
167 }
168 15 => Mmff94AtomType::HP, 16 => Mmff94AtomType::HS, _ => Mmff94AtomType::HC,
171 }
172 }
173
174 6 => {
176 if is_aromatic {
177 if in_ring_of_size(5) {
179 let has_hetero_nb = neighbors.iter().any(|&ni| {
181 let z = nb_elem(ni);
182 (z == 7 || z == 8 || z == 16) && is_atom_aromatic_mmff(mol, ni.index())
183 });
184 if has_hetero_nb {
185 Mmff94AtomType::C5A } else {
187 Mmff94AtomType::C5B }
189 } else {
190 Mmff94AtomType::CB
191 }
192 } else {
193 match hyb {
194 Hybridization::SP => Mmff94AtomType::CSp,
195 Hybridization::SP2 => {
196 if count_double_bonds() > 0
198 && count_nb_elem(8) > 0
199 && mol.graph.edges(node).any(|e| {
200 e.weight().order == BondOrder::Double && {
201 let other = if e.source() == node {
202 e.target()
203 } else {
204 e.source()
205 };
206 nb_elem(other) == 8
207 }
208 })
209 {
210 Mmff94AtomType::CO } else {
212 Mmff94AtomType::CSp2
213 }
214 }
215 _ => {
216 if in_ring_of_size(3) {
218 Mmff94AtomType::CR3R
219 } else if in_ring_of_size(4) {
220 Mmff94AtomType::CR4R
221 } else {
222 Mmff94AtomType::CR
223 }
224 }
225 }
226 }
227 }
228
229 7 => {
231 let n_double = count_double_bonds();
232
233 if is_aromatic {
235 if in_ring_of_size(5) {
236 if degree == 3 {
238 Mmff94AtomType::N5A } else {
240 Mmff94AtomType::N5B }
242 } else {
243 Mmff94AtomType::NR2 }
245 }
246 else if n_double >= 2 && count_nb_elem(8) >= 2 {
248 Mmff94AtomType::N2OX
249 }
250 else if n_double == 1 && count_nb_elem(8) >= 1 && degree >= 3 {
252 let has_o_double = mol.graph.edges(node).any(|e| {
254 e.weight().order == BondOrder::Double && {
255 let o = if e.source() == node {
256 e.target()
257 } else {
258 e.source()
259 };
260 nb_elem(o) == 8
261 }
262 });
263 if has_o_double && degree >= 3 {
264 Mmff94AtomType::NPOX
265 } else {
266 Mmff94AtomType::N2
267 }
268 }
269 else if adjacent_to_carbonyl()
271 && matches!(hyb, Hybridization::SP2 | Hybridization::SP3)
272 && degree <= 3
273 {
274 let has_so = neighbors
276 .iter()
277 .any(|&ni| nb_elem(ni) == 16 && nb_has_double_bond_to(ni, 8));
278 if has_so {
279 Mmff94AtomType::NSO
280 } else {
281 Mmff94AtomType::NAm
282 }
283 }
284 else if matches!(hyb, Hybridization::SP2) {
286 if degree == 3 && n_double == 0 {
287 Mmff94AtomType::NPl3
288 } else if n_double >= 1 {
289 Mmff94AtomType::N2
290 } else {
291 Mmff94AtomType::NPl
292 }
293 }
294 else if matches!(hyb, Hybridization::SP) {
296 Mmff94AtomType::NC
297 }
298 else {
300 Mmff94AtomType::NR
301 }
302 }
303
304 8 => {
306 if is_aromatic && in_ring_of_size(5) {
307 return Mmff94AtomType::O5; }
309
310 let n_double = count_double_bonds();
311
312 if degree == 1 {
314 let parent = neighbors[0];
315 if nb_elem(parent) == 7 {
316 let n_node = parent;
317 let n_o_count = mol
318 .graph
319 .neighbors(n_node)
320 .filter(|&ni| nb_elem(ni) == 8)
321 .count();
322 if n_o_count >= 2 {
323 return Mmff94AtomType::ON2; }
325 }
326 }
327
328 if atom.formal_charge < 0 {
330 return Mmff94AtomType::OM;
331 }
332
333 if degree == 1 && n_double == 0 {
335 let parent = neighbors[0];
337 if nb_elem(parent) == 6 && nb_has_double_bond_to(parent, 8) {
338 return Mmff94AtomType::OX; }
340 }
341
342 match hyb {
343 Hybridization::SP2 => Mmff94AtomType::O2, _ => Mmff94AtomType::OR, }
346 }
347
348 9 => Mmff94AtomType::F,
350
351 14 => Mmff94AtomType::SI,
353
354 15 => {
356 if degree >= 4 && count_nb_elem(8) >= 1 && nb_has_double_bond_to(node, 8) {
357 Mmff94AtomType::PO } else {
359 Mmff94AtomType::P
360 }
361 }
362
363 16 => {
365 if is_aromatic && in_ring_of_size(5) {
366 return Mmff94AtomType::Sthi; }
368
369 let n_double_o = mol
370 .graph
371 .edges(node)
372 .filter(|e| {
373 e.weight().order == BondOrder::Double && {
374 let other = if e.source() == node {
375 e.target()
376 } else {
377 e.source()
378 };
379 nb_elem(other) == 8
380 }
381 })
382 .count();
383
384 if n_double_o >= 2 {
385 Mmff94AtomType::SO2 } else if n_double_o == 1 {
387 Mmff94AtomType::SO } else if degree <= 2 && count_nb_elem(1) >= 1 {
389 Mmff94AtomType::Sthi } else {
391 Mmff94AtomType::S }
393 }
394
395 17 => Mmff94AtomType::Cl,
397
398 35 => Mmff94AtomType::Br,
400
401 26 => {
403 if atom.formal_charge >= 3 {
404 Mmff94AtomType::Fe3
405 } else {
406 Mmff94AtomType::Fe2
407 }
408 }
409
410 53 => Mmff94AtomType::I,
412
413 _ => Mmff94AtomType::Unknown,
414 }
415}
416
417fn atom_in_any_ring(mol: &crate::graph::Molecule, atom_idx: usize) -> bool {
419 atom_in_ring_of_size(mol, atom_idx, 3)
420 || atom_in_ring_of_size(mol, atom_idx, 4)
421 || atom_in_ring_of_size(mol, atom_idx, 5)
422 || atom_in_ring_of_size(mol, atom_idx, 6)
423 || atom_in_ring_of_size(mol, atom_idx, 7)
424}
425
426fn atom_in_ring_of_size(mol: &crate::graph::Molecule, atom_idx: usize, size: usize) -> bool {
428 use petgraph::graph::NodeIndex;
429 use std::collections::VecDeque;
430
431 let start = NodeIndex::new(atom_idx);
432 let mut queue: VecDeque<(NodeIndex, Vec<usize>)> = VecDeque::new();
434 for nb in mol.graph.neighbors(start) {
435 queue.push_back((nb, vec![atom_idx, nb.index()]));
436 }
437
438 while let Some((current, path)) = queue.pop_front() {
439 if path.len() > size + 1 {
440 continue;
441 }
442 if path.len() == size + 1 && current == start {
443 return true;
444 }
445 if path.len() > size {
446 continue;
447 }
448 for nb in mol.graph.neighbors(current) {
449 if nb == start && path.len() == size {
450 return true;
451 }
452 if !path.contains(&nb.index()) {
453 let mut new_path = path.clone();
454 new_path.push(nb.index());
455 queue.push_back((nb, new_path));
456 }
457 }
458 }
459 false
460}
461
462fn is_atom_aromatic_mmff(mol: &crate::graph::Molecule, atom_idx: usize) -> bool {
465 use crate::graph::BondOrder;
466 use petgraph::graph::NodeIndex;
467 let node = NodeIndex::new(atom_idx);
468 mol.graph
469 .edges(node)
470 .any(|e| e.weight().order == BondOrder::Aromatic)
471}
472
473pub fn assign_mmff94_type(
475 element: u8,
476 hyb: &crate::graph::Hybridization,
477 is_aromatic: bool,
478 is_amide_n: bool,
479) -> Mmff94AtomType {
480 use crate::graph::Hybridization::*;
481 match element {
482 1 => Mmff94AtomType::HC,
483 5 => Mmff94AtomType::CSp2,
484 6 => {
485 if is_aromatic {
486 Mmff94AtomType::CB
487 } else {
488 match hyb {
489 SP => Mmff94AtomType::CSp,
490 SP2 => Mmff94AtomType::CSp2,
491 _ => Mmff94AtomType::CR,
492 }
493 }
494 }
495 7 => {
496 if is_aromatic {
497 Mmff94AtomType::NR2
498 } else if is_amide_n {
499 Mmff94AtomType::NAm
500 } else {
501 match hyb {
502 SP => Mmff94AtomType::NC,
503 SP2 => Mmff94AtomType::N2,
504 _ => Mmff94AtomType::NR,
505 }
506 }
507 }
508 8 => match hyb {
509 SP2 => Mmff94AtomType::O2,
510 _ => Mmff94AtomType::OR,
511 },
512 9 => Mmff94AtomType::F,
513 15 => Mmff94AtomType::P,
514 16 => Mmff94AtomType::S,
515 17 => Mmff94AtomType::Cl,
516 35 => Mmff94AtomType::Br,
517 53 => Mmff94AtomType::I,
518 _ => Mmff94AtomType::Unknown,
519 }
520}
521
522pub fn assign_all_mmff94_types(mol: &crate::graph::Molecule) -> Vec<Mmff94AtomType> {
526 let n = mol.graph.node_count();
527 (0..n).map(|i| assign_mmff94_type_smarts(mol, i)).collect()
528}
529
530pub struct Mmff94BondStretch {
536 pub atom_i: usize,
537 pub atom_j: usize,
538 pub k_b: f64, pub r0: f64, }
541
542const MMFF94_CUBIC_STRETCH: f64 = -2.0;
543
544impl ForceFieldContribution for Mmff94BondStretch {
545 fn evaluate_energy_and_inject_gradient(&self, coords: &[f64], grad: &mut [f64]) -> f64 {
546 let ri = self.atom_i * 3;
547 let rj = self.atom_j * 3;
548 let dx = coords[ri] - coords[rj];
549 let dy = coords[ri + 1] - coords[rj + 1];
550 let dz = coords[ri + 2] - coords[rj + 2];
551 let dist = (dx * dx + dy * dy + dz * dz).sqrt().max(1e-8);
552 let dr = dist - self.r0;
553 let cs = MMFF94_CUBIC_STRETCH;
554 let cs2 = cs * cs;
555
556 let energy =
558 143.9325 * 0.5 * self.k_b * dr * dr * (1.0 + cs * dr + (7.0 / 12.0) * cs2 * dr * dr);
559
560 let de_dr = 143.9325 * self.k_b * dr * (1.0 + 1.5 * cs * dr + (7.0 / 6.0) * cs2 * dr * dr);
562 let scale = de_dr / dist;
563 grad[ri] += scale * dx;
564 grad[ri + 1] += scale * dy;
565 grad[ri + 2] += scale * dz;
566 grad[rj] -= scale * dx;
567 grad[rj + 1] -= scale * dy;
568 grad[rj + 2] -= scale * dz;
569
570 energy
571 }
572}
573
574pub struct Mmff94AngleBend {
580 pub atom_i: usize,
581 pub atom_j: usize, pub atom_k: usize,
583 pub k_a: f64, pub theta0: f64, }
586
587const MMFF94_CUBIC_BEND: f64 = -0.014;
588
589impl ForceFieldContribution for Mmff94AngleBend {
590 fn evaluate_energy_and_inject_gradient(&self, coords: &[f64], grad: &mut [f64]) -> f64 {
591 let ri = self.atom_i * 3;
592 let rj = self.atom_j * 3;
593 let rk = self.atom_k * 3;
594
595 let rji = [
596 coords[ri] - coords[rj],
597 coords[ri + 1] - coords[rj + 1],
598 coords[ri + 2] - coords[rj + 2],
599 ];
600 let rjk = [
601 coords[rk] - coords[rj],
602 coords[rk + 1] - coords[rj + 1],
603 coords[rk + 2] - coords[rj + 2],
604 ];
605
606 let d_ji = (rji[0] * rji[0] + rji[1] * rji[1] + rji[2] * rji[2])
607 .sqrt()
608 .max(1e-8);
609 let d_jk = (rjk[0] * rjk[0] + rjk[1] * rjk[1] + rjk[2] * rjk[2])
610 .sqrt()
611 .max(1e-8);
612
613 let cos_theta = (rji[0] * rjk[0] + rji[1] * rjk[1] + rji[2] * rjk[2]) / (d_ji * d_jk);
614 let cos_theta_clamped = cos_theta.clamp(-1.0, 1.0);
615 let theta = cos_theta_clamped.acos();
616 let dt = (theta - self.theta0) * 180.0 / std::f64::consts::PI; let cb = MMFF94_CUBIC_BEND;
619 let energy = 0.043844 * 0.5 * self.k_a * dt * dt * (1.0 + cb * dt);
620
621 let de_dtheta =
623 0.043844 * self.k_a * dt * (1.0 + 1.5 * cb * dt) * (180.0 / std::f64::consts::PI);
624 let sin_theta = (1.0 - cos_theta_clamped * cos_theta_clamped)
625 .sqrt()
626 .max(1e-8);
627 let dcos = -1.0 / sin_theta;
628 let pref = de_dtheta * dcos;
629
630 for dim in 0..3 {
631 let term_i = pref * (rjk[dim] / (d_ji * d_jk) - cos_theta * rji[dim] / (d_ji * d_ji))
632 / d_ji
633 * d_ji;
634 let term_k = pref * (rji[dim] / (d_ji * d_jk) - cos_theta * rjk[dim] / (d_jk * d_jk))
635 / d_jk
636 * d_jk;
637 grad[ri + dim] += term_i;
638 grad[rk + dim] += term_k;
639 grad[rj + dim] -= term_i + term_k;
640 }
641
642 energy
643 }
644}
645
646pub struct Mmff94Torsion {
651 pub atom_i: usize,
652 pub atom_j: usize,
653 pub atom_k: usize,
654 pub atom_l: usize,
655 pub v1: f64,
656 pub v2: f64,
657 pub v3: f64,
658}
659
660impl ForceFieldContribution for Mmff94Torsion {
661 fn evaluate_energy_and_inject_gradient(&self, coords: &[f64], grad: &mut [f64]) -> f64 {
662 let p = |idx: usize| -> [f64; 3] {
663 [coords[idx * 3], coords[idx * 3 + 1], coords[idx * 3 + 2]]
664 };
665 let pi = p(self.atom_i);
666 let pj = p(self.atom_j);
667 let pk = p(self.atom_k);
668 let pl = p(self.atom_l);
669
670 let b1 = [pj[0] - pi[0], pj[1] - pi[1], pj[2] - pi[2]];
672 let b2 = [pk[0] - pj[0], pk[1] - pj[1], pk[2] - pj[2]];
673 let b3 = [pl[0] - pk[0], pl[1] - pk[1], pl[2] - pk[2]];
674
675 let cross = |a: [f64; 3], b: [f64; 3]| -> [f64; 3] {
676 [
677 a[1] * b[2] - a[2] * b[1],
678 a[2] * b[0] - a[0] * b[2],
679 a[0] * b[1] - a[1] * b[0],
680 ]
681 };
682 let dot = |a: [f64; 3], b: [f64; 3]| -> f64 { a[0] * b[0] + a[1] * b[1] + a[2] * b[2] };
683
684 let n1 = cross(b1, b2);
685 let n2 = cross(b2, b3);
686 let m1 = cross(n1, b2);
687
688 let b2_len = dot(b2, b2).sqrt().max(1e-8);
689 let x = dot(n1, n2);
690 let y = dot(m1, n2) / b2_len;
691 let phi = (-y).atan2(x);
692
693 let energy = 0.5
694 * (self.v1 * (1.0 + phi.cos())
695 + self.v2 * (1.0 - (2.0 * phi).cos())
696 + self.v3 * (1.0 + (3.0 * phi).cos()));
697
698 let eps = 1e-5;
700 for atom_idx in [self.atom_i, self.atom_j, self.atom_k, self.atom_l] {
701 for dim in 0..3 {
702 let idx = atom_idx * 3 + dim;
703 let orig = coords[idx];
704 let mut c_plus = coords.to_vec();
705 let mut c_minus = coords.to_vec();
706 c_plus[idx] = orig + eps;
707 c_minus[idx] = orig - eps;
708
709 let phi_p = {
710 let pi = [
711 c_plus[self.atom_i * 3],
712 c_plus[self.atom_i * 3 + 1],
713 c_plus[self.atom_i * 3 + 2],
714 ];
715 let pj = [
716 c_plus[self.atom_j * 3],
717 c_plus[self.atom_j * 3 + 1],
718 c_plus[self.atom_j * 3 + 2],
719 ];
720 let pk = [
721 c_plus[self.atom_k * 3],
722 c_plus[self.atom_k * 3 + 1],
723 c_plus[self.atom_k * 3 + 2],
724 ];
725 let pl = [
726 c_plus[self.atom_l * 3],
727 c_plus[self.atom_l * 3 + 1],
728 c_plus[self.atom_l * 3 + 2],
729 ];
730 let b1 = [pj[0] - pi[0], pj[1] - pi[1], pj[2] - pi[2]];
731 let b2 = [pk[0] - pj[0], pk[1] - pj[1], pk[2] - pj[2]];
732 let b3 = [pl[0] - pk[0], pl[1] - pk[1], pl[2] - pk[2]];
733 let nn1 = cross(b1, b2);
734 let nn2 = cross(b2, b3);
735 let mm1 = cross(nn1, b2);
736 let b2l = dot(b2, b2).sqrt().max(1e-8);
737 (-dot(mm1, nn2) / b2l).atan2(dot(nn1, nn2))
738 };
739 let phi_m = {
740 let pi = [
741 c_minus[self.atom_i * 3],
742 c_minus[self.atom_i * 3 + 1],
743 c_minus[self.atom_i * 3 + 2],
744 ];
745 let pj = [
746 c_minus[self.atom_j * 3],
747 c_minus[self.atom_j * 3 + 1],
748 c_minus[self.atom_j * 3 + 2],
749 ];
750 let pk = [
751 c_minus[self.atom_k * 3],
752 c_minus[self.atom_k * 3 + 1],
753 c_minus[self.atom_k * 3 + 2],
754 ];
755 let pl = [
756 c_minus[self.atom_l * 3],
757 c_minus[self.atom_l * 3 + 1],
758 c_minus[self.atom_l * 3 + 2],
759 ];
760 let b1 = [pj[0] - pi[0], pj[1] - pi[1], pj[2] - pi[2]];
761 let b2 = [pk[0] - pj[0], pk[1] - pj[1], pk[2] - pj[2]];
762 let b3 = [pl[0] - pk[0], pl[1] - pk[1], pl[2] - pk[2]];
763 let nn1 = cross(b1, b2);
764 let nn2 = cross(b2, b3);
765 let mm1 = cross(nn1, b2);
766 let b2l = dot(b2, b2).sqrt().max(1e-8);
767 (-dot(mm1, nn2) / b2l).atan2(dot(nn1, nn2))
768 };
769
770 let e_p = 0.5
771 * (self.v1 * (1.0 + phi_p.cos())
772 + self.v2 * (1.0 - (2.0 * phi_p).cos())
773 + self.v3 * (1.0 + (3.0 * phi_p).cos()));
774 let e_m = 0.5
775 * (self.v1 * (1.0 + phi_m.cos())
776 + self.v2 * (1.0 - (2.0 * phi_m).cos())
777 + self.v3 * (1.0 + (3.0 * phi_m).cos()));
778 grad[idx] += (e_p - e_m) / (2.0 * eps);
779 }
780 }
781
782 energy
783 }
784}
785
786pub struct Mmff94BufferedVanDerWaals {
790 pub atom_i_idx: usize,
791 pub atom_j_idx: usize,
792 pub radius_star: f64, pub epsilon_depth: f64, }
795
796impl ForceFieldContribution for Mmff94BufferedVanDerWaals {
797 fn evaluate_energy_and_inject_gradient(&self, coords: &[f64], grad: &mut [f64]) -> f64 {
798 let root_i = self.atom_i_idx * 3;
799 let root_j = self.atom_j_idx * 3;
800
801 let delta_x = coords[root_i] - coords[root_j];
802 let delta_y = coords[root_i + 1] - coords[root_j + 1];
803 let delta_z = coords[root_i + 2] - coords[root_j + 2];
804
805 let dist_squared = delta_x * delta_x + delta_y * delta_y + delta_z * delta_z;
806 let mut dist_r = dist_squared.sqrt();
807
808 if dist_r < 1e-8 {
810 dist_r = 1e-8;
811 }
812
813 let r_star_powered_7 = self.radius_star.powi(7);
815 let dist_r_powered_7 = dist_r.powi(7);
816
817 let repulsive_denominator = dist_r + 0.07 * self.radius_star;
818 let repulsive_term = (1.07 * self.radius_star / repulsive_denominator).powi(7);
819
820 let attractive_denominator = dist_r_powered_7 + 0.12 * r_star_powered_7;
821 let attractive_term = (1.12 * r_star_powered_7 / attractive_denominator) - 2.0;
822
823 let vdw_total_energy = self.epsilon_depth * repulsive_term * attractive_term;
824
825 let gradient_rep_term = -7.0 * repulsive_term / repulsive_denominator;
827 let gradient_attr_term = -7.0 * dist_r.powi(6) * (1.12 * r_star_powered_7)
828 / (attractive_denominator * attractive_denominator);
829
830 let force_scalar_magnitude = self.epsilon_depth
831 * (gradient_rep_term * attractive_term + repulsive_term * gradient_attr_term);
832
833 let vector_prefactor = force_scalar_magnitude / dist_r;
835 let grad_x = vector_prefactor * delta_x;
836 let grad_y = vector_prefactor * delta_y;
837 let grad_z = vector_prefactor * delta_z;
838
839 grad[root_i] += grad_x;
840 grad[root_i + 1] += grad_y;
841 grad[root_i + 2] += grad_z;
842
843 grad[root_j] -= grad_x;
844 grad[root_j + 1] -= grad_y;
845 grad[root_j + 2] -= grad_z;
846
847 vdw_total_energy
848 }
849}
850
851pub fn validate_gradients(term: &dyn ForceFieldContribution, coords: &[f64], eps: f64) -> f64 {
856 let n = coords.len();
857 let mut analytical_grad = vec![0.0; n];
858 term.evaluate_energy_and_inject_gradient(coords, &mut analytical_grad);
859
860 let mut max_err = 0.0f64;
861 for i in 0..n {
862 let mut c_plus = coords.to_vec();
863 let mut c_minus = coords.to_vec();
864 c_plus[i] += eps;
865 c_minus[i] -= eps;
866
867 let mut g_dummy = vec![0.0; n];
868 let e_plus = term.evaluate_energy_and_inject_gradient(&c_plus, &mut g_dummy);
869 g_dummy.fill(0.0);
870 let e_minus = term.evaluate_energy_and_inject_gradient(&c_minus, &mut g_dummy);
871
872 let numerical = (e_plus - e_minus) / (2.0 * eps);
873 let err = (analytical_grad[i] - numerical).abs();
874 max_err = max_err.max(err);
875 }
876 max_err
877}
878
879pub struct Mmff94Builder;
884
885impl Mmff94Builder {
886 fn bond_params(elem_i: u8, elem_j: u8, _bond_order: u8) -> (f64, f64) {
888 let r_cov = |e: u8| -> f64 {
890 match e {
891 1 => 0.31,
892 6 => 0.76,
893 7 => 0.71,
894 8 => 0.66,
895 9 => 0.57,
896 15 => 1.07,
897 16 => 1.05,
898 17 => 1.02,
899 35 => 1.20,
900 53 => 1.39,
901 _ => 1.0,
902 }
903 };
904 let r0 = r_cov(elem_i) + r_cov(elem_j);
905 let kb = 5.0; (kb, r0)
907 }
908
909 pub fn build(
917 elements: &[u8],
918 bonds: &[(usize, usize, u8)],
919 ) -> Vec<Box<dyn ForceFieldContribution>> {
920 let n_atoms = elements.len();
921 let mut terms: Vec<Box<dyn ForceFieldContribution>> = Vec::new();
922
923 for &(i, j, order) in bonds {
925 let (kb, r0) = Self::bond_params(elements[i], elements[j], order);
926 terms.push(Box::new(Mmff94BondStretch {
927 atom_i: i,
928 atom_j: j,
929 k_b: kb,
930 r0,
931 }));
932 }
933
934 let mut neighbors: Vec<Vec<usize>> = vec![Vec::new(); n_atoms];
936 for &(i, j, _) in bonds {
937 neighbors[i].push(j);
938 neighbors[j].push(i);
939 }
940 for j in 0..n_atoms {
941 let nbrs = &neighbors[j];
942 for a in 0..nbrs.len() {
943 for b in (a + 1)..nbrs.len() {
944 let i = nbrs[a];
945 let k = nbrs[b];
946 terms.push(Box::new(Mmff94AngleBend {
947 atom_i: i,
948 atom_j: j,
949 atom_k: k,
950 k_a: 0.5, theta0: 109.5_f64.to_radians(), }));
953 }
954 }
955 }
956
957 for &(j, k, _) in bonds {
959 for &i in &neighbors[j] {
960 if i == k {
961 continue;
962 }
963 for &l in &neighbors[k] {
964 if l == j || l == i {
965 continue;
966 }
967 terms.push(Box::new(Mmff94Torsion {
968 atom_i: i,
969 atom_j: j,
970 atom_k: k,
971 atom_l: l,
972 v1: 0.0,
973 v2: 1.0,
974 v3: 0.0, }));
976 }
977 }
978 }
979
980 for &(j, k, _) in bonds {
982 for &i in &neighbors[j] {
983 if i == k {
984 continue;
985 }
986 for &l in &neighbors[k] {
987 if l == j || l == i {
988 continue;
989 }
990 let r_star = 3.5; let eps = 0.05;
992 terms.push(Box::new(Mmff94BufferedVanDerWaals {
993 atom_i_idx: i,
994 atom_j_idx: l,
995 radius_star: r_star,
996 epsilon_depth: eps,
997 }));
998 }
999 }
1000 }
1001
1002 terms
1003 }
1004
1005 pub fn total_energy(
1007 terms: &[Box<dyn ForceFieldContribution>],
1008 coords: &[f64],
1009 ) -> (f64, Vec<f64>) {
1010 let n = coords.len();
1011 let mut grad = vec![0.0; n];
1012 let mut total = 0.0;
1013 for term in terms {
1014 total += term.evaluate_energy_and_inject_gradient(coords, &mut grad);
1015 }
1016 (total, grad)
1017 }
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 use super::*;
1023
1024 #[test]
1025 fn test_mmff94_vdw_energy() {
1026 let term = Mmff94BufferedVanDerWaals {
1027 atom_i_idx: 0,
1028 atom_j_idx: 1,
1029 radius_star: 3.6,
1030 epsilon_depth: 0.1,
1031 };
1032 let coords = vec![0.0, 0.0, 0.0, 3.6, 0.0, 0.0];
1033 let mut grad = vec![0.0; 6];
1034 let e = term.evaluate_energy_and_inject_gradient(&coords, &mut grad);
1035 assert!(e < 0.0 && e > -0.2, "vdW energy at R*: {e}");
1037 }
1038
1039 #[test]
1040 fn test_mmff94_vdw_gradient_validation() {
1041 let term = Mmff94BufferedVanDerWaals {
1042 atom_i_idx: 0,
1043 atom_j_idx: 1,
1044 radius_star: 3.6,
1045 epsilon_depth: 0.1,
1046 };
1047 let coords = vec![0.0, 0.0, 0.0, 4.0, 0.0, 0.0];
1048 let max_err = validate_gradients(&term, &coords, 1e-5);
1049 assert!(max_err < 1e-4, "vdW gradient error: {max_err}");
1050 }
1051
1052 #[test]
1053 fn test_mmff94_bond_stretch() {
1054 let term = Mmff94BondStretch {
1055 atom_i: 0,
1056 atom_j: 1,
1057 k_b: 5.0,
1058 r0: 1.5,
1059 };
1060 let coords_eq = vec![0.0, 0.0, 0.0, 1.5, 0.0, 0.0];
1062 let mut grad = vec![0.0; 6];
1063 let e_eq = term.evaluate_energy_and_inject_gradient(&coords_eq, &mut grad);
1064 assert!(e_eq.abs() < 1e-10, "bond stretch at r0: {e_eq}");
1065
1066 let coords_stretch = vec![0.0, 0.0, 0.0, 2.0, 0.0, 0.0];
1068 grad.fill(0.0);
1069 let e_str = term.evaluate_energy_and_inject_gradient(&coords_stretch, &mut grad);
1070 assert!(
1071 e_str > 0.0,
1072 "bond stretch energy should be positive: {e_str}"
1073 );
1074 }
1075
1076 #[test]
1077 fn test_mmff94_bond_stretch_gradient_validation() {
1078 let term = Mmff94BondStretch {
1079 atom_i: 0,
1080 atom_j: 1,
1081 k_b: 5.0,
1082 r0: 1.5,
1083 };
1084 let coords = vec![0.0, 0.0, 0.0, 2.0, 0.1, 0.0];
1085 let max_err = validate_gradients(&term, &coords, 1e-5);
1086 assert!(max_err < 1e-3, "bond stretch gradient error: {max_err}");
1087 }
1088
1089 #[test]
1090 fn test_mmff94_torsion_energy() {
1091 let term = Mmff94Torsion {
1092 atom_i: 0,
1093 atom_j: 1,
1094 atom_k: 2,
1095 atom_l: 3,
1096 v1: 0.0,
1097 v2: 5.0,
1098 v3: 0.0,
1099 };
1100 let coords = vec![-1.5, 1.0, 0.0, 0.0, 0.0, 0.0, 1.5, 0.0, 0.0, 3.0, 1.0, 0.0];
1102 let mut grad = vec![0.0; 12];
1103 let e = term.evaluate_energy_and_inject_gradient(&coords, &mut grad);
1104 assert!(e.is_finite(), "torsion energy should be finite: {e}");
1105 }
1106
1107 #[test]
1108 fn test_mmff94_atom_typing() {
1109 use crate::graph::Hybridization;
1110 let t = assign_mmff94_type(6, &Hybridization::SP3, false, false);
1111 assert_eq!(t, Mmff94AtomType::CR);
1112 let t = assign_mmff94_type(6, &Hybridization::SP2, true, false);
1113 assert_eq!(t, Mmff94AtomType::CB);
1114 let t = assign_mmff94_type(7, &Hybridization::SP3, false, true);
1115 assert_eq!(t, Mmff94AtomType::NAm);
1116 }
1117
1118 #[test]
1119 fn test_mmff94_builder_ethane() {
1120 let elements = vec![6, 6, 1, 1, 1, 1, 1, 1]; let bonds = vec![
1123 (0, 1, 1), (0, 2, 1),
1125 (0, 3, 1),
1126 (0, 4, 1), (1, 5, 1),
1128 (1, 6, 1),
1129 (1, 7, 1), ];
1131 let coords = vec![
1133 0.0, 0.0, 0.0, 1.54, 0.0, 0.0, -0.5, 0.9, 0.0, -0.5, -0.9, 0.0, -0.5, 0.0, 0.9, 2.04, 0.9, 0.0, 2.04, -0.9, 0.0, 2.04, 0.0, 0.9, ];
1142 let terms = Mmff94Builder::build(&elements, &bonds);
1143 assert!(!terms.is_empty(), "should produce force field terms");
1144
1145 let (energy, grad) = Mmff94Builder::total_energy(&terms, &coords);
1146 assert!(
1147 energy.is_finite(),
1148 "total energy should be finite: {energy}"
1149 );
1150 assert!(
1151 grad.iter().all(|g| g.is_finite()),
1152 "all gradients should be finite"
1153 );
1154 }
1155
1156 #[test]
1157 fn test_mmff94_builder_gradient_consistency() {
1158 let elements = vec![6, 6];
1160 let bonds = vec![(0, 1, 1)];
1161 let coords = vec![0.0, 0.0, 0.0, 1.6, 0.1, 0.0];
1162 let terms = Mmff94Builder::build(&elements, &bonds);
1163 let (_, analytical_grad) = Mmff94Builder::total_energy(&terms, &coords);
1164
1165 let eps = 1e-5;
1166 for i in 0..coords.len() {
1167 let mut cp = coords.clone();
1168 let mut cm = coords.clone();
1169 cp[i] += eps;
1170 cm[i] -= eps;
1171 let (ep, _) = Mmff94Builder::total_energy(&terms, &cp);
1172 let (em, _) = Mmff94Builder::total_energy(&terms, &cm);
1173 let numerical = (ep - em) / (2.0 * eps);
1174 let err = (analytical_grad[i] - numerical).abs();
1175 assert!(
1176 err < 0.1,
1177 "gradient mismatch at coord {i}: anal={:.6} num={:.6} err={:.6}",
1178 analytical_grad[i],
1179 numerical,
1180 err
1181 );
1182 }
1183 }
1184}