1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct JCoupling {
13 pub h1_index: usize,
15 pub h2_index: usize,
17 pub j_hz: f64,
19 pub n_bonds: usize,
21 pub coupling_type: String,
23}
24
25const KARPLUS_A: f64 = 7.76;
28const KARPLUS_B: f64 = -1.10;
29const KARPLUS_C: f64 = 1.40;
30
31fn dihedral_angle(p1: &[f64; 3], p2: &[f64; 3], p3: &[f64; 3], p4: &[f64; 3]) -> f64 {
33 let b1 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
34 let b2 = [p3[0] - p2[0], p3[1] - p2[1], p3[2] - p2[2]];
35 let b3 = [p4[0] - p3[0], p4[1] - p3[1], p4[2] - p3[2]];
36
37 let n1 = cross(&b1, &b2);
39 let n2 = cross(&b2, &b3);
41
42 let m1 = cross(&n1, &normalize(&b2));
43
44 let x = dot(&n1, &n2);
45 let y = dot(&m1, &n2);
46
47 (-y).atan2(x)
48}
49
50fn cross(a: &[f64; 3], b: &[f64; 3]) -> [f64; 3] {
51 [
52 a[1] * b[2] - a[2] * b[1],
53 a[2] * b[0] - a[0] * b[2],
54 a[0] * b[1] - a[1] * b[0],
55 ]
56}
57
58fn dot(a: &[f64; 3], b: &[f64; 3]) -> f64 {
59 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
60}
61
62fn normalize(v: &[f64; 3]) -> [f64; 3] {
63 let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
64 if len < 1e-10 {
65 return [0.0, 0.0, 0.0];
66 }
67 [v[0] / len, v[1] / len, v[2] / len]
68}
69
70fn karplus_3j(phi_rad: f64) -> f64 {
72 let cos_phi = phi_rad.cos();
73 KARPLUS_A * cos_phi * cos_phi + KARPLUS_B * cos_phi + KARPLUS_C
74}
75
76pub fn predict_j_couplings(mol: &crate::graph::Molecule, positions: &[[f64; 3]]) -> Vec<JCoupling> {
85 let n = mol.graph.node_count();
86 let has_3d = positions.len() == n;
87 let mut couplings = Vec::new();
88
89 let h_atoms: Vec<usize> = (0..n)
91 .filter(|&i| mol.graph[petgraph::graph::NodeIndex::new(i)].element == 1)
92 .collect();
93
94 for i in 0..h_atoms.len() {
96 for j in (i + 1)..h_atoms.len() {
97 let h1 = h_atoms[i];
98 let h2 = h_atoms[j];
99 let h1_idx = petgraph::graph::NodeIndex::new(h1);
100 let h2_idx = petgraph::graph::NodeIndex::new(h2);
101
102 let parent1: Vec<petgraph::graph::NodeIndex> = mol
104 .graph
105 .neighbors(h1_idx)
106 .filter(|n| mol.graph[*n].element != 1)
107 .collect();
108 let parent2: Vec<petgraph::graph::NodeIndex> = mol
109 .graph
110 .neighbors(h2_idx)
111 .filter(|n| mol.graph[*n].element != 1)
112 .collect();
113
114 if parent1.is_empty() || parent2.is_empty() {
115 continue;
116 }
117
118 let p1 = parent1[0];
119 let p2 = parent2[0];
120
121 if p1 == p2 {
122 let j_hz: f64 = -12.0; couplings.push(JCoupling {
125 h1_index: h1,
126 h2_index: h2,
127 j_hz: j_hz.abs(), n_bonds: 2,
129 coupling_type: "geminal_2J".to_string(),
130 });
131 } else if mol.graph.find_edge(p1, p2).is_some() {
132 let j_hz = if has_3d {
134 let phi = dihedral_angle(
135 &positions[h1],
136 &positions[p1.index()],
137 &positions[p2.index()],
138 &positions[h2],
139 );
140 karplus_3j(phi)
141 } else {
142 7.0
144 };
145
146 couplings.push(JCoupling {
147 h1_index: h1,
148 h2_index: h2,
149 j_hz,
150 n_bonds: 3,
151 coupling_type: "vicinal_3J".to_string(),
152 });
153 }
154 }
156 }
157
158 couplings
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_karplus_equation_values() {
167 let j_0 = karplus_3j(0.0);
169 assert!(
170 j_0 > 8.0 && j_0 < 10.0,
171 "³J(0°) = {} Hz, expected ~9 Hz",
172 j_0
173 );
174
175 let j_90 = karplus_3j(std::f64::consts::FRAC_PI_2);
177 assert!(
178 j_90 > 0.0 && j_90 < 3.0,
179 "³J(90°) = {} Hz, expected ~1.4 Hz",
180 j_90
181 );
182
183 let j_180 = karplus_3j(std::f64::consts::PI);
185 assert!(
186 j_180 > 6.0 && j_180 < 12.0,
187 "³J(180°) = {} Hz, expected ~10 Hz",
188 j_180
189 );
190 }
191
192 #[test]
193 fn test_dihedral_angle_basic() {
194 let p1 = [1.0, 0.0, 0.0];
196 let p2 = [0.0, 0.0, 0.0];
197 let p3 = [0.0, 1.0, 0.0];
198 let p4 = [-1.0, 1.0, 0.0];
199 let angle = dihedral_angle(&p1, &p2, &p3, &p4);
200 assert!(angle.abs() < 0.1 || (angle.abs() - std::f64::consts::PI).abs() < 0.1);
202 }
203
204 #[test]
205 fn test_ethane_j_couplings() {
206 let mol = crate::graph::Molecule::from_smiles("CC").unwrap();
207 let couplings = predict_j_couplings(&mol, &[]);
208
209 assert!(
211 !couplings.is_empty(),
212 "Ethane should have J-coupling predictions"
213 );
214
215 let vicinal: Vec<&JCoupling> = couplings.iter().filter(|c| c.n_bonds == 3).collect();
217 assert!(
218 !vicinal.is_empty(),
219 "Ethane should have ³J vicinal couplings"
220 );
221 }
222
223 #[test]
224 fn test_methane_j_couplings() {
225 let mol = crate::graph::Molecule::from_smiles("C").unwrap();
226 let couplings = predict_j_couplings(&mol, &[]);
227
228 for c in &couplings {
230 assert_eq!(c.n_bonds, 2, "Methane H-H should be 2-bond (geminal)");
231 }
232 }
233}