1use chematic_core::{AtomIdx, Element, Molecule};
7use std::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[allow(non_camel_case_types)]
13pub enum MMFF94Type {
14 C_sp3,
16 C_sp2_Alkene,
17 C_sp_Alkyne,
18 C_Aromatic,
19 C_Carbonyl,
20 C_Carboxylic,
21 C_Carbamate,
22 C_Ester,
23 C_Amide,
24 C_Imide,
25 C_CarbamideN,
26
27 N_sp3_Amine,
29 N_sp3_AmineAromatic,
30 N_sp2_Imine,
31 N_sp2_Aromatic,
32 N_sp2_Carbonyl,
33 N_sp_Nitrile,
34 N_Amide,
35 N_Carbamate,
36 N_Ester,
37 N_Imide,
38 N_Aromatic_5ring,
39 N_Aromatic_6ring,
40 N_Aromatic_Pyridine,
41 N_Aromatic_Pyrrole,
42 N_Aromatic_Imidazole,
43 N_Aromatic_Triazole,
44 N_Aromatic_Tetrazole,
45 N_Aromatic_Pyrimidine,
46 N_Aromatic_Pyrazine,
47
48 O_Alcohol,
50 O_Phenol,
51 O_Ether,
52 O_Carbonyl,
53 O_Carboxylic,
54 O_Carbamate,
55 O_Ester,
56 O_Amide,
57 O_Imide,
58 O_CarbamideN,
59 O_Sulfoxide,
60 O_Sulfone,
61
62 S_Thiol,
64 S_Thioether,
65 S_Disulfide,
66 S_Sulfoxide,
67 S_Sulfone,
68 S_Aromatic,
69
70 P_sp3,
72 P_Oxide,
73
74 Si_sp3,
76 Si_sp2,
77
78 F,
80 Cl,
81 Br,
82 I,
83
84 H_Carbon,
86 H_Nitrogen,
87 H_Oxygen,
88 H_Sulfur,
89 H_Halogen,
90 H_Aromatic,
91
92 Generic,
94}
95
96impl fmt::Display for MMFF94Type {
97 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
98 let s = match self {
99 Self::C_sp3 => "C_sp3",
100 Self::C_sp2_Alkene => "C_sp2_Alkene",
101 Self::C_sp_Alkyne => "C_sp_Alkyne",
102 Self::C_Aromatic => "C_Aromatic",
103 Self::C_Carbonyl => "C_Carbonyl",
104 Self::C_Carboxylic => "C_Carboxylic",
105 Self::N_sp3_Amine => "N_sp3_Amine",
106 Self::N_sp2_Aromatic => "N_sp2_Aromatic",
107 Self::O_Alcohol => "O_Alcohol",
108 Self::O_Ether => "O_Ether",
109 Self::O_Carbonyl => "O_Carbonyl",
110 Self::F => "F",
111 Self::Cl => "Cl",
112 Self::Br => "Br",
113 Self::I => "I",
114 Self::H_Carbon => "H_Carbon",
115 Self::H_Nitrogen => "H_Nitrogen",
116 Self::Generic => "Generic",
117 _ => "Other",
118 };
119 write!(f, "{}", s)
120 }
121}
122
123#[derive(Debug)]
125pub enum AssignError {
126 UnsupportedElement(String),
127 ComplexAromaticity,
128 CoordinateMismatch,
129}
130
131impl fmt::Display for AssignError {
132 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
133 match self {
134 Self::UnsupportedElement(e) => write!(f, "Unsupported element for MMFF94: {}", e),
135 Self::ComplexAromaticity => write!(f, "Complex aromaticity pattern"),
136 Self::CoordinateMismatch => write!(f, "Coordinate count mismatch with atom count"),
137 }
138 }
139}
140
141impl std::error::Error for AssignError {}
142
143pub fn assign_mmff94_types(mol: &Molecule) -> Result<Vec<MMFF94Type>, AssignError> {
148 let mut types = vec![MMFF94Type::Generic; mol.atom_count()];
149
150 for (i, atom_type) in types.iter_mut().enumerate().take(mol.atom_count()) {
151 let idx = AtomIdx(i as u32);
152 let atom = mol.atom(idx);
153
154 *atom_type = match atom.element {
155 Element::C => assign_carbon_type(mol, idx)?,
156 Element::N => assign_nitrogen_type(mol, idx)?,
157 Element::O => assign_oxygen_type(mol, idx)?,
158 Element::S => assign_sulfur_type(mol, idx)?,
159 Element::P => assign_phosphorus_type(mol, idx)?,
160 Element::F => MMFF94Type::F,
161 Element::CL => MMFF94Type::Cl,
162 Element::BR => MMFF94Type::Br,
163 Element::I => MMFF94Type::I,
164 Element::H => assign_hydrogen_type(mol, idx)?,
165 Element::SI => assign_silicon_type(mol, idx)?,
166 _ => {
167 return Err(AssignError::UnsupportedElement(format!(
168 "{:?}",
169 atom.element
170 )));
171 }
172 };
173 }
174
175 Ok(types)
176}
177
178fn assign_carbon_type(mol: &Molecule, idx: AtomIdx) -> Result<MMFF94Type, AssignError> {
179 let atom = mol.atom(idx);
180
181 let mut max_bond_order = 0;
182 let mut double_bonds = 0;
183 let mut triple_bonds = 0;
184 let mut neighbors = Vec::new();
185
186 for (_, bond) in mol.bonds() {
187 let other_atom = if bond.atom1 == idx {
188 bond.atom2
189 } else if bond.atom2 == idx {
190 bond.atom1
191 } else {
192 continue;
193 };
194
195 let bond_order_val = bond_order_to_int(bond.order);
196 max_bond_order = max_bond_order.max(bond_order_val);
197
198 if bond_order_val == 2 {
199 double_bonds += 1;
200 } else if bond_order_val == 3 {
201 triple_bonds += 1;
202 }
203
204 neighbors.push(mol.atom(other_atom).element);
205 }
206
207 if double_bonds > 0 {
209 for (_, bond) in mol.bonds() {
210 let other = if bond.atom1 == idx {
211 bond.atom2
212 } else if bond.atom2 == idx {
213 bond.atom1
214 } else {
215 continue;
216 };
217 if bond.order == chematic_core::BondOrder::Double
218 && mol.atom(other).element == Element::O
219 {
220 let has_oh = false; return Ok(if has_oh {
224 MMFF94Type::C_Carboxylic
225 } else {
226 MMFF94Type::C_Ester
227 });
228 }
229 }
230 }
231
232 if atom.aromatic {
234 Ok(MMFF94Type::C_Aromatic)
235 } else if triple_bonds > 0 {
236 Ok(MMFF94Type::C_sp_Alkyne)
237 } else if double_bonds > 0 {
238 Ok(MMFF94Type::C_sp2_Alkene)
239 } else {
240 Ok(MMFF94Type::C_sp3)
241 }
242}
243
244fn assign_nitrogen_type(mol: &Molecule, idx: AtomIdx) -> Result<MMFF94Type, AssignError> {
245 let atom = mol.atom(idx);
246
247 if atom.aromatic {
248 Ok(MMFF94Type::N_sp2_Aromatic)
249 } else {
250 let mut double_bonds = 0;
252 for (_, bond) in mol.bonds() {
253 if (bond.atom1 == idx || bond.atom2 == idx)
254 && bond.order == chematic_core::BondOrder::Double
255 {
256 double_bonds += 1;
257 }
258 }
259
260 if double_bonds > 0 {
261 Ok(MMFF94Type::N_sp2_Imine)
262 } else {
263 Ok(MMFF94Type::N_sp3_Amine)
264 }
265 }
266}
267
268fn assign_oxygen_type(mol: &Molecule, idx: AtomIdx) -> Result<MMFF94Type, AssignError> {
269 let atom = mol.atom(idx);
270
271 for (_, bond) in mol.bonds() {
273 if (bond.atom1 == idx || bond.atom2 == idx)
274 && bond.order == chematic_core::BondOrder::Double
275 {
276 return Ok(MMFF94Type::O_Carbonyl);
277 }
278 }
279
280 let bond_count = mol
283 .bonds()
284 .filter(|(_, b)| b.atom1 == idx || b.atom2 == idx)
285 .count();
286 let max_valence = *atom.element.normal_valences().iter().max().unwrap_or(&2) as usize;
287 let h_count = max_valence.saturating_sub(bond_count);
288
289 if atom.aromatic {
290 Ok(MMFF94Type::O_Ether)
291 } else if h_count > 0 {
292 Ok(MMFF94Type::O_Alcohol)
293 } else {
294 Ok(MMFF94Type::O_Ether)
295 }
296}
297
298fn assign_sulfur_type(mol: &Molecule, idx: AtomIdx) -> Result<MMFF94Type, AssignError> {
299 let mut double_bonds = 0;
300
301 for (_, bond) in mol.bonds() {
302 if (bond.atom1 == idx || bond.atom2 == idx)
303 && bond.order == chematic_core::BondOrder::Double
304 {
305 double_bonds += 1;
306 }
307 }
308
309 if double_bonds >= 2 {
310 Ok(MMFF94Type::S_Sulfone)
311 } else if double_bonds == 1 {
312 Ok(MMFF94Type::S_Sulfoxide)
313 } else {
314 Ok(MMFF94Type::S_Thioether)
315 }
316}
317
318fn assign_phosphorus_type(mol: &Molecule, idx: AtomIdx) -> Result<MMFF94Type, AssignError> {
319 let _atom = mol.atom(idx);
320 Ok(MMFF94Type::P_sp3)
321}
322
323fn assign_silicon_type(mol: &Molecule, idx: AtomIdx) -> Result<MMFF94Type, AssignError> {
324 let _atom = mol.atom(idx);
325 Ok(MMFF94Type::Si_sp3)
326}
327
328fn assign_hydrogen_type(mol: &Molecule, idx: AtomIdx) -> Result<MMFF94Type, AssignError> {
329 for (_, bond) in mol.bonds() {
331 let other = if bond.atom1 == idx {
332 Some(bond.atom2)
333 } else if bond.atom2 == idx {
334 Some(bond.atom1)
335 } else {
336 None
337 };
338
339 if let Some(other_idx) = other {
340 let other_atom = mol.atom(other_idx);
341 return Ok(match other_atom.element {
342 Element::N => MMFF94Type::H_Nitrogen,
343 Element::O => MMFF94Type::H_Oxygen,
344 Element::S => MMFF94Type::H_Sulfur,
345 Element::F | Element::CL | Element::BR | Element::I => MMFF94Type::H_Halogen,
346 _ => MMFF94Type::H_Carbon,
347 });
348 }
349 }
350
351 Ok(MMFF94Type::H_Carbon)
352}
353
354fn bond_order_to_int(order: chematic_core::BondOrder) -> usize {
355 match order {
356 chematic_core::BondOrder::Single => 1,
357 chematic_core::BondOrder::Double => 2,
358 chematic_core::BondOrder::Triple => 3,
359 chematic_core::BondOrder::Aromatic => 1, _ => 1,
361 }
362}
363
364pub fn mmff94_charges_3d(
383 mol: &Molecule,
384 coords: &[(f64, f64, f64)],
385) -> Result<Vec<f64>, AssignError> {
386 let n = mol.atom_count();
387 if coords.len() != n {
388 return Err(AssignError::CoordinateMismatch);
389 }
390
391 let types = assign_mmff94_types(mol)?;
393
394 let mut charges = Vec::with_capacity(n);
396 for (i, &atom_type) in types.iter().enumerate() {
397 let atom = mol.atom(AtomIdx(i as u32));
398 let base_charge = crate::mmff94_params::mmff94_charge_params(atom_type).charge;
399 let formal_charge = atom.charge as f64;
400
401 charges.push(base_charge + formal_charge);
402 }
403
404 apply_formal_charge_redistribution(mol, &types, &mut charges);
407
408 apply_bond_polarization_3d(mol, coords, &types, &mut charges);
410
411 apply_hbond_effects(mol, &types, &mut charges);
413
414 Ok(charges)
415}
416
417fn apply_formal_charge_redistribution(mol: &Molecule, types: &[MMFF94Type], charges: &mut [f64]) {
421 use MMFF94Type::*;
422
423 for i in 0..mol.atom_count() {
425 let atom = mol.atom(AtomIdx(i as u32));
426 let atom_type = types[i];
427
428 if matches!(atom_type, C_Carboxylic | C_Ester | C_Carbamate) {
430 let oxygen_neighbors: Vec<(AtomIdx, i8)> = mol
431 .neighbors(AtomIdx(i as u32))
432 .filter_map(|(neighbor_idx, _bond_idx)| {
433 let neighbor = mol.atom(neighbor_idx);
434 if matches!(neighbor.element.atomic_number(), 8) {
435 Some((neighbor_idx, neighbor.charge))
436 } else {
437 None
438 }
439 })
440 .collect();
441
442 if oxygen_neighbors.len() >= 2 {
444 for (o_idx, o_charge) in &oxygen_neighbors {
445 let o_idx_usize = o_idx.0 as usize;
446 if *o_charge < 0 {
447 let redistribution = (*o_charge as f64) * 0.3; charges[i] -= redistribution; charges[o_idx_usize] += redistribution * 0.5; }
452 }
453 }
454 }
455
456 if matches!(atom_type, N_sp3_Amine | N_sp3_AmineAromatic) && atom.charge > 0 {
458 let hydrogen_count = mol
459 .neighbors(AtomIdx(i as u32))
460 .filter(|(neighbor_idx, _)| mol.atom(*neighbor_idx).element.atomic_number() == 1)
461 .count();
462
463 if hydrogen_count > 0 {
464 let charge_per_neighbor = (atom.charge as f64) / ((hydrogen_count + 1) as f64);
466 charges[i] += charge_per_neighbor;
467 }
468 }
469 }
470}
471
472fn apply_bond_polarization_3d(
475 mol: &Molecule,
476 coords: &[(f64, f64, f64)],
477 _types: &[MMFF94Type],
478 charges: &mut [f64],
479) {
480 let _n = mol.atom_count();
481
482 let en_table: fn(Element) -> f64 = |elem| match elem.atomic_number() {
484 1 => 2.10, 6 => 2.55, 7 => 3.04, 8 => 3.44, 9 => 3.98, 15 => 2.19, 16 => 2.58, 17 => 3.16, 35 => 2.96, 53 => 2.66, _ => 2.0,
495 };
496
497 for (_, bond) in mol.bonds() {
499 let i = bond.atom1.0 as usize;
500 let j = bond.atom2.0 as usize;
501
502 let atom_i = mol.atom(bond.atom1);
503 let atom_j = mol.atom(bond.atom2);
504
505 let en_i = en_table(atom_i.element);
506 let en_j = en_table(atom_j.element);
507
508 let (xi, yi, zi) = coords[i];
510 let (xj, yj, zj) = coords[j];
511 let dist_sq = (xi - xj).powi(2) + (yi - yj).powi(2) + (zi - zj).powi(2);
512 let dist = dist_sq.sqrt();
513
514 if dist > 0.1 && dist < 3.0 {
515 let en_diff = (en_i - en_j) / (en_i + en_j).max(0.1);
517 let polarization = en_diff * 0.05; charges[i] += polarization;
520 charges[j] -= polarization;
521 }
522 }
523}
524
525fn apply_hbond_effects(_mol: &Molecule, types: &[MMFF94Type], charges: &mut [f64]) {
528 use MMFF94Type::*;
529
530 for (i, &atom_type) in types.iter().enumerate() {
531 let is_h_donor = matches!(
533 atom_type,
534 N_sp3_Amine | N_sp3_AmineAromatic | O_Alcohol | O_Phenol | O_Carboxylic
535 );
536
537 let is_h_acceptor = matches!(
539 atom_type,
540 N_sp3_Amine
541 | N_sp3_AmineAromatic
542 | N_sp2_Imine
543 | N_sp2_Aromatic
544 | O_Alcohol
545 | O_Phenol
546 | O_Ether
547 | O_Carbonyl
548 | O_Carboxylic
549 | O_Carbamate
550 | O_Ester
551 | O_Amide
552 | O_Imide
553 );
554
555 if is_h_donor {
557 charges[i] -= 0.05; }
559 if is_h_acceptor {
560 charges[i] -= 0.03; }
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 use super::*;
568 use chematic_smiles::parse;
569
570 #[test]
571 fn test_mmff94_ethane_types() {
572 let mol = parse("CC").unwrap();
573 let types = assign_mmff94_types(&mol).unwrap();
574 assert_eq!(types.len(), 2);
575 assert_eq!(types[0], MMFF94Type::C_sp3);
576 assert_eq!(types[1], MMFF94Type::C_sp3);
577 }
578
579 #[test]
580 fn test_mmff94_benzene_types() {
581 let mol = parse("c1ccccc1").unwrap();
582 let types = assign_mmff94_types(&mol).unwrap();
583 assert_eq!(types.len(), 6);
584 for &t in &types {
585 assert_eq!(t, MMFF94Type::C_Aromatic);
586 }
587 }
588
589 #[test]
590 fn test_mmff94_methanol_types() {
591 let mol = parse("CO").unwrap();
592 let types = assign_mmff94_types(&mol).unwrap();
593 assert_eq!(types.len(), 2);
594 assert_eq!(types[0], MMFF94Type::C_sp3);
595 assert_eq!(types[1], MMFF94Type::O_Alcohol);
596 }
597
598 #[test]
599 fn test_mmff94_amine_types() {
600 let mol = parse("CCN").unwrap();
601 let types = assign_mmff94_types(&mol).unwrap();
602 assert_eq!(types.len(), 3);
603 assert_eq!(types[2], MMFF94Type::N_sp3_Amine);
604 }
605
606 #[test]
609 fn test_mmff94_charges_3d_ethane() {
610 let mol = parse("CC").unwrap();
611 let coords = vec![(0.0, 0.0, 0.0), (1.54, 0.0, 0.0)];
613
614 let charges = mmff94_charges_3d(&mol, &coords).unwrap();
615 assert_eq!(charges.len(), 2);
616 assert!((charges[0] - charges[1]).abs() < 0.2);
618 }
619
620 #[test]
621 fn test_mmff94_charges_3d_methanol() {
622 let mol = parse("CO").unwrap();
623 let coords = vec![(0.0, 0.0, 0.0), (1.4, 0.0, 0.0)];
624
625 let charges = mmff94_charges_3d(&mol, &coords).unwrap();
626 assert_eq!(charges.len(), 2);
627
628 assert!(charges[1] < charges[0]);
630 }
631
632 #[test]
633 fn test_mmff94_charges_3d_formal_charge() {
634 let mol = parse("CC(C)N(C)(C)C").unwrap();
636 let n_atoms = mol.atom_count();
637
638 let coords: Vec<(f64, f64, f64)> =
640 (0..n_atoms).map(|i| (i as f64 * 1.5, 0.0, 0.0)).collect();
641
642 let charges = mmff94_charges_3d(&mol, &coords).unwrap();
643 assert_eq!(charges.len(), n_atoms);
644
645 for charge in &charges {
647 assert!(charge.is_finite());
648 }
649 }
650
651 #[test]
652 fn test_mmff94_charges_3d_benzene() {
653 let mol = parse("c1ccccc1").unwrap();
654 let coords = vec![
655 (1.4, 0.0, 0.0),
656 (0.7, 1.2, 0.0),
657 (-0.7, 1.2, 0.0),
658 (-1.4, 0.0, 0.0),
659 (-0.7, -1.2, 0.0),
660 (0.7, -1.2, 0.0),
661 ];
662
663 let charges = mmff94_charges_3d(&mol, &coords).unwrap();
664 assert_eq!(charges.len(), 6);
665
666 for i in 0..5 {
668 assert!((charges[i] - charges[i + 1]).abs() < 0.15);
669 }
670 }
671
672 #[test]
673 fn test_mmff94_charges_3d_coordinate_mismatch() {
674 let mol = parse("CC").unwrap();
675 let coords = vec![(0.0, 0.0, 0.0)]; let result = mmff94_charges_3d(&mol, &coords);
678 assert!(result.is_err());
679 }
680
681 #[test]
682 fn test_mmff94_charge_params_carbon_types() {
683 use crate::mmff94_params::mmff94_charge_params;
684
685 let c_sp3 = mmff94_charge_params(MMFF94Type::C_sp3).charge;
687 assert!(c_sp3.abs() < 0.1);
688
689 let c_carbonyl = mmff94_charge_params(MMFF94Type::C_Carbonyl).charge;
691 assert!(c_carbonyl > 0.3);
692 }
693
694 #[test]
695 fn test_mmff94_charge_params_nitrogen_types() {
696 use crate::mmff94_params::mmff94_charge_params;
697
698 let n_amine = mmff94_charge_params(MMFF94Type::N_sp3_Amine).charge;
700 assert!(n_amine < 0.0);
701
702 let n_aromatic = mmff94_charge_params(MMFF94Type::N_Aromatic_Pyridine).charge;
704 assert!(n_aromatic < 0.0);
705 assert!(n_aromatic > n_amine); }
707
708 #[test]
709 fn test_mmff94_charge_params_oxygen_types() {
710 use crate::mmff94_params::mmff94_charge_params;
711
712 let o_alcohol = mmff94_charge_params(MMFF94Type::O_Alcohol).charge;
714 let o_carbonyl = mmff94_charge_params(MMFF94Type::O_Carbonyl).charge;
715 let o_carboxylic = mmff94_charge_params(MMFF94Type::O_Carboxylic).charge;
716
717 assert!(o_alcohol < 0.0);
718 assert!(o_carbonyl < 0.0);
719 assert!(o_carboxylic < 0.0);
720 }
721
722 #[test]
723 fn test_mmff94_charges_all_positive() {
724 let mol = parse("CCO").unwrap();
726 let coords = vec![(0.0, 0.0, 0.0), (1.5, 0.0, 0.0), (2.5, 0.5, 0.0)];
727
728 let charges = mmff94_charges_3d(&mol, &coords).unwrap();
729 for charge in &charges {
730 assert!(charge.is_finite());
731 assert!(!charge.is_nan());
732 }
733 }
734
735 #[test]
736 fn test_mmff94_charges_carboxylic_acid() {
737 let mol = parse("CC(=O)O").unwrap();
738 let coords = vec![
739 (0.0, 0.0, 0.0), (1.5, 0.0, 0.0), (2.5, 1.0, 0.0), (2.5, -1.0, 0.0), ];
744
745 let charges = mmff94_charges_3d(&mol, &coords).unwrap();
746 assert_eq!(charges.len(), 4);
747
748 assert!(charges[3] < charges[0]);
750 }
751
752 #[test]
753 fn test_mmff94_charges_acetate_carboxylate() {
754 let mol = parse("CC(=O)[O-]").unwrap();
758 let coords = vec![
759 (0.0, 0.0, 0.0), (1.5, 0.0, 0.0), (2.5, 1.0, 0.0), (2.5, -1.0, 0.0), ];
764
765 let charges = mmff94_charges_3d(&mol, &coords).unwrap();
766 assert_eq!(charges.len(), 4);
767
768 let total_charge: f64 = charges.iter().sum();
771 assert!(
773 total_charge < -0.5,
774 "total charge should be negative (carboxylate), got {}",
775 total_charge
776 );
777
778 assert!(
780 charges[2] < -0.2 || charges[3] < -0.2,
781 "at least one O should be negative"
782 );
783 }
784
785 #[test]
786 fn test_mmff94_charges_phosphate() {
787 let mol = parse("C(=O)(O)O").unwrap(); let coords = vec![
793 (0.0, 0.0, 0.0), (1.3, 0.0, 0.0), (-0.65, 1.1, 0.0), (-0.65, -1.1, 0.0), ];
798
799 let charges = mmff94_charges_3d(&mol, &coords).unwrap();
800 assert_eq!(charges.len(), 4);
801
802 let avg_o_charge = (charges[1] + charges[2] + charges[3]) / 3.0;
804 assert!(
805 avg_o_charge < charges[0],
806 "average O charge should be more negative than C"
807 );
808 }
809
810 #[test]
811 fn test_mmff94_charges_finite() {
812 let mol = parse("C1=CC=CC=C1[N+](=O)[O-]").unwrap(); let coords = vec![
815 (0.0, 0.0, 0.0),
816 (1.4, 0.0, 0.0),
817 (2.1, 1.2, 0.0),
818 (1.4, 2.4, 0.0),
819 (0.0, 2.4, 0.0),
820 (-0.7, 1.2, 0.0),
821 (2.8, 1.2, 0.0),
822 (3.6, 1.2, 0.0),
823 (2.8, 0.4, 0.0),
824 ];
825
826 if let Ok(charges) = mmff94_charges_3d(&mol, &coords) {
827 for (i, &charge) in charges.iter().enumerate() {
828 assert!(
829 charge.is_finite(),
830 "charge[{}] = {} is not finite",
831 i,
832 charge
833 );
834 }
835 }
836 }
837}