chematic_ff/
mmff94_advanced.rs1use chematic_core::{AtomIdx, Molecule};
10
11use crate::mmff94::MMFF94Type;
12use crate::mmff94_params::mmff94_electrostatic_scaling_1_4;
13
14#[derive(Clone, Debug)]
17pub struct ElectrostaticMatrix {
18 pub scaling: Vec<Vec<f64>>,
20 pub is_1_4_pair: Vec<Vec<bool>>,
22 pub is_1_3_pair: Vec<Vec<bool>>,
24}
25
26impl ElectrostaticMatrix {
27 pub fn new(mol: &Molecule, mmff94_types: &[MMFF94Type]) -> Self {
33 let n = mol.atom_count();
34 let mut scaling = vec![vec![4.0; n]; n]; let mut is_1_4_pair = vec![vec![false; n]; n];
36 let mut is_1_3_pair = vec![vec![false; n]; n];
37
38 let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n];
40 for (_, bond) in mol.bonds() {
41 let i = bond.atom1.0 as usize;
42 let j = bond.atom2.0 as usize;
43 adj[i].push(j);
44 adj[j].push(i);
45 }
46
47 let dist = topological_distance_matrix(mol);
49
50 for i in 0..n {
52 for j in 0..n {
53 if i == j {
54 scaling[i][j] = 0.0; continue;
56 }
57
58 let d = dist[i][j];
59
60 if d == 1 {
61 scaling[i][j] = 0.0;
63 } else if d == 2 {
64 is_1_3_pair[i][j] = true;
66 scaling[i][j] = 0.0; } else if d == 3 {
68 is_1_4_pair[i][j] = true;
70 let type_i = mmff94_types[i];
71 let type_j = mmff94_types[j];
72 let params = mmff94_electrostatic_scaling_1_4(type_i, type_j);
73 scaling[i][j] = params.scale_dielectric;
74 } else {
75 scaling[i][j] = 4.0; }
78 }
79 }
80
81 ElectrostaticMatrix {
82 scaling,
83 is_1_4_pair,
84 is_1_3_pair,
85 }
86 }
87
88 pub fn get_scaling(&self, i: usize, j: usize) -> f64 {
90 if i < self.scaling.len() && j < self.scaling[i].len() {
91 self.scaling[i][j]
92 } else {
93 4.0 }
95 }
96
97 pub fn is_1_4(&self, i: usize, j: usize) -> bool {
99 if i < self.is_1_4_pair.len() && j < self.is_1_4_pair[i].len() {
100 self.is_1_4_pair[i][j]
101 } else {
102 false
103 }
104 }
105}
106
107#[derive(Clone, Debug)]
112pub struct MMFF94BatchProperties {
113 pub charges: Vec<Vec<f64>>,
115 pub bond_dipoles: Vec<Vec<(usize, usize, f64)>>,
117 pub electrostatic_matrices: Vec<ElectrostaticMatrix>,
119}
120
121impl MMFF94BatchProperties {
122 pub fn new() -> Self {
124 MMFF94BatchProperties {
125 charges: Vec::new(),
126 bond_dipoles: Vec::new(),
127 electrostatic_matrices: Vec::new(),
128 }
129 }
130
131 pub fn len(&self) -> usize {
133 self.charges.len()
134 }
135
136 pub fn is_empty(&self) -> bool {
138 self.charges.is_empty()
139 }
140
141 pub fn push(
143 &mut self,
144 charges: Vec<f64>,
145 bond_dipoles: Vec<(usize, usize, f64)>,
146 electrostatic_matrix: ElectrostaticMatrix,
147 ) {
148 self.charges.push(charges);
149 self.bond_dipoles.push(bond_dipoles);
150 self.electrostatic_matrices.push(electrostatic_matrix);
151 }
152
153 pub fn average_charges(&self) -> Vec<f64> {
155 if self.charges.is_empty() {
156 return Vec::new();
157 }
158
159 let n_atoms = self.charges[0].len();
160 let mut avg = vec![0.0; n_atoms];
161
162 for charges in &self.charges {
163 for (i, &q) in charges.iter().enumerate() {
164 avg[i] += q;
165 }
166 }
167
168 let n_mols = self.charges.len() as f64;
169 for q in &mut avg {
170 *q /= n_mols;
171 }
172
173 avg
174 }
175
176 pub fn charge_variance(&self) -> Vec<f64> {
178 if self.charges.is_empty() {
179 return Vec::new();
180 }
181
182 let n_atoms = self.charges[0].len();
183 let avg = self.average_charges();
184 let mut var = vec![0.0; n_atoms];
185
186 for charges in &self.charges {
187 for (i, &q) in charges.iter().enumerate() {
188 let diff = q - avg[i];
189 var[i] += diff * diff;
190 }
191 }
192
193 let n_mols = self.charges.len() as f64;
194 for v in &mut var {
195 *v /= n_mols;
196 *v = v.sqrt(); }
198
199 var
200 }
201}
202
203impl Default for MMFF94BatchProperties {
204 fn default() -> Self {
205 Self::new()
206 }
207}
208
209fn topological_distance_matrix(mol: &Molecule) -> Vec<Vec<i32>> {
211 let n = mol.atom_count();
212 let mut dist = vec![vec![i32::MAX; n]; n];
213
214 for (i, row) in dist.iter_mut().enumerate().take(n) {
216 row[i] = 0;
217 }
218
219 for start in 0..n {
221 let mut queue = vec![start];
222 let mut visited = vec![false; n];
223 visited[start] = true;
224
225 while !queue.is_empty() {
226 let current = queue.remove(0);
227 let atom_idx = AtomIdx(current as u32);
228
229 for (neighbor, _) in mol.neighbors(atom_idx) {
230 let neighbor_idx = neighbor.0 as usize;
231 if !visited[neighbor_idx] {
232 visited[neighbor_idx] = true;
233 dist[start][neighbor_idx] = dist[start][current] + 1;
234 queue.push(neighbor_idx);
235 }
236 }
237 }
238 }
239
240 dist
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use crate::mmff94::assign_mmff94_types;
247 use chematic_smiles::parse;
248
249 #[test]
250 fn test_electrostatic_matrix_creation() {
251 let mol = parse("CC").unwrap();
252 let mmff94_types = assign_mmff94_types(&mol).unwrap();
253
254 let matrix = ElectrostaticMatrix::new(&mol, &mmff94_types);
255 assert_eq!(matrix.scaling.len(), 2);
256 assert_eq!(matrix.scaling[0].len(), 2);
257 }
258
259 #[test]
260 fn test_electrostatic_matrix_1_2_exclusion() {
261 let mol = parse("CC").unwrap();
262 let mmff94_types = assign_mmff94_types(&mol).unwrap();
263
264 let matrix = ElectrostaticMatrix::new(&mol, &mmff94_types);
265 assert_eq!(matrix.get_scaling(0, 1), 0.0);
267 assert_eq!(matrix.get_scaling(1, 0), 0.0);
268 }
269
270 #[test]
271 fn test_batch_properties_empty() {
272 let batch = MMFF94BatchProperties::new();
273 assert!(batch.is_empty());
274 assert_eq!(batch.len(), 0);
275 }
276
277 #[test]
278 fn test_batch_properties_average() {
279 let mut batch = MMFF94BatchProperties::new();
280
281 let mol = parse("C").unwrap();
282 let mmff94_types = assign_mmff94_types(&mol).unwrap();
283 let matrix = ElectrostaticMatrix::new(&mol, &mmff94_types);
284
285 batch.push(vec![0.1], vec![], matrix.clone());
287 batch.push(vec![0.3], vec![], matrix);
288
289 let avg = batch.average_charges();
290 assert_eq!(avg.len(), 1);
291 assert!((avg[0] - 0.2).abs() < 1e-6); }
293
294 #[test]
295 fn test_batch_properties_variance() {
296 let mut batch = MMFF94BatchProperties::new();
297
298 let mol = parse("C").unwrap();
299 let mmff94_types = assign_mmff94_types(&mol).unwrap();
300 let matrix = ElectrostaticMatrix::new(&mol, &mmff94_types);
301
302 batch.push(vec![0.0], vec![], matrix.clone());
303 batch.push(vec![1.0], vec![], matrix);
304
305 let var = batch.charge_variance();
306 assert_eq!(var.len(), 1);
307 assert!((var[0] - 0.5).abs() < 1e-6);
310 }
311
312 #[test]
313 fn test_topological_distance_ethane() {
314 let mol = parse("CC").unwrap();
315 let dist = topological_distance_matrix(&mol);
316
317 assert_eq!(dist[0][0], 0);
318 assert_eq!(dist[1][1], 0);
319 assert_eq!(dist[0][1], 1);
320 assert_eq!(dist[1][0], 1);
321 }
322
323 #[test]
324 fn test_topological_distance_propane() {
325 let mol = parse("CCC").unwrap();
326 let dist = topological_distance_matrix(&mol);
327
328 assert_eq!(dist[0][1], 1); assert_eq!(dist[1][2], 1); assert_eq!(dist[0][2], 2); }
333
334 #[test]
335 fn test_electrostatic_matrix_scaling_values() {
336 let mol = parse("CCCC").unwrap();
337 let mmff94_types = assign_mmff94_types(&mol).unwrap();
338
339 let matrix = ElectrostaticMatrix::new(&mol, &mmff94_types);
340
341 for i in 0..4 {
343 assert_eq!(matrix.get_scaling(i, i), 0.0);
345 }
346
347 for i in 0..4 {
349 for j in 0..4 {
350 assert!(matrix.get_scaling(i, j) >= 0.0);
351 }
352 }
353 }
354}