1use chematic_core::{AtomIdx, Molecule};
4use chematic_smarts::{find_matches, parse_smarts};
5use std::collections::HashMap;
6
7#[derive(Clone, Debug)]
9pub struct TorsionPreference {
10 pub angle_deg: f64,
12 pub penalty_per_degree: f64,
14}
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21pub enum AtomType {
22 CSp3,
24 CSp2Alkene,
26 CAromatic,
28 CCarbonyl,
30 NSp,
32 NSp2,
34 NAromatic,
36 NSp3,
38 OSp2,
40 OSp3,
42 OAromatic,
44 S,
46 SAromatic,
48 P,
50 H,
52 Halogen,
54 Other,
56}
57
58fn count_incident_bonds(mol: &Molecule, idx: AtomIdx, order: chematic_core::BondOrder) -> usize {
60 mol.bonds()
61 .filter(|(_, bond)| (bond.atom1 == idx || bond.atom2 == idx) && bond.order == order)
62 .count()
63}
64
65pub fn classify_atom_type(mol: &Molecule, idx: AtomIdx) -> AtomType {
70 let atom = mol.atom(idx);
71 let an = atom.element.atomic_number();
72
73 match an {
74 1 => AtomType::H,
75 6 => {
76 let double_bonds = count_incident_bonds(mol, idx, chematic_core::BondOrder::Double);
78
79 if atom.aromatic {
80 AtomType::CAromatic
81 } else if double_bonds > 0 {
82 let has_o_neighbor = mol.neighbors(idx).any(|(n_idx, _)| {
84 mol.atom(n_idx).element.atomic_number() == 8
85 && mol
86 .bond_between(idx, n_idx)
87 .map(|(_, b)| b.order == chematic_core::BondOrder::Double)
88 .unwrap_or(false)
89 });
90 if has_o_neighbor {
91 AtomType::CCarbonyl
92 } else {
93 AtomType::CSp2Alkene
94 }
95 } else {
96 AtomType::CSp3
97 }
98 }
99 7 => {
100 if atom.aromatic {
102 AtomType::NAromatic
103 } else {
104 let triple_bonds = count_incident_bonds(mol, idx, chematic_core::BondOrder::Triple);
105 let neighbors = mol.neighbors(idx).count();
106
107 if triple_bonds > 0 {
108 AtomType::NSp
109 } else if neighbors <= 2 {
110 AtomType::NSp2
111 } else {
112 AtomType::NSp3
113 }
114 }
115 }
116 8 => {
117 if atom.aromatic {
118 AtomType::OAromatic } else {
120 let has_double_bond = mol.bonds().any(|(_, bond)| {
121 (bond.atom1 == idx || bond.atom2 == idx)
122 && bond.order == chematic_core::BondOrder::Double
123 });
124 if has_double_bond {
125 AtomType::OSp2 } else {
127 AtomType::OSp3 }
129 }
130 }
131 16 => {
132 if atom.aromatic {
133 AtomType::SAromatic } else {
135 AtomType::S
136 }
137 }
138 15 => AtomType::P,
139 9 | 17 | 35 | 53 => AtomType::Halogen,
140 _ => AtomType::Other,
141 }
142}
143
144pub fn get_torsion_preference(
148 mol: &Molecule,
149 a_idx: AtomIdx,
150 b_idx: AtomIdx,
151 c_idx: AtomIdx,
152 d_idx: AtomIdx,
153) -> Option<TorsionPreference> {
154 let a_type = classify_atom_type(mol, a_idx);
155 let b_type = classify_atom_type(mol, b_idx);
156 let c_type = classify_atom_type(mol, c_idx);
157 let d_type = classify_atom_type(mol, d_idx);
158
159 if b_type == AtomType::CSp3
161 && c_type == AtomType::CSp3
162 && (a_type == AtomType::CSp3 || a_type == AtomType::H)
163 && (d_type == AtomType::CSp3 || d_type == AtomType::H)
164 {
165 return Some(TorsionPreference {
166 angle_deg: 180.0,
167 penalty_per_degree: 0.15, });
169 }
170
171 if (a_type == AtomType::CSp3 || a_type == AtomType::H)
173 && b_type == AtomType::CAromatic
174 && c_type == AtomType::CSp3
175 && (d_type == AtomType::CSp3 || d_type == AtomType::H)
176 {
177 return Some(TorsionPreference {
178 angle_deg: 180.0,
179 penalty_per_degree: 0.08, });
181 }
182
183 if b_type == AtomType::NSp2 && c_type == AtomType::CCarbonyl {
186 return Some(TorsionPreference {
187 angle_deg: 180.0,
188 penalty_per_degree: 0.20, });
190 }
191
192 if b_type == AtomType::CCarbonyl && c_type == AtomType::OSp3 {
194 return Some(TorsionPreference {
195 angle_deg: 0.0,
196 penalty_per_degree: 0.10,
197 });
198 }
199
200 if b_type == AtomType::CAromatic && c_type == AtomType::CAromatic {
202 return Some(TorsionPreference {
203 angle_deg: 45.0,
204 penalty_per_degree: 0.03, });
206 }
207
208 if b_type == AtomType::CSp2Alkene && c_type == AtomType::NSp2 {
210 return Some(TorsionPreference {
211 angle_deg: 180.0,
212 penalty_per_degree: 0.12,
213 });
214 }
215 if b_type == AtomType::NSp2 && c_type == AtomType::CSp2Alkene {
216 return Some(TorsionPreference {
217 angle_deg: 180.0,
218 penalty_per_degree: 0.12,
219 });
220 }
221
222 if (b_type == AtomType::CSp2Alkene || b_type == AtomType::CAromatic)
224 && c_type == AtomType::Halogen
225 {
226 return Some(TorsionPreference {
227 angle_deg: 0.0,
228 penalty_per_degree: 0.08,
229 });
230 }
231
232 if b_type == AtomType::CSp2Alkene && c_type == AtomType::CCarbonyl {
234 return Some(TorsionPreference {
235 angle_deg: 180.0,
236 penalty_per_degree: 0.10,
237 });
238 }
239
240 if b_type == AtomType::CAromatic && c_type == AtomType::CCarbonyl {
242 return Some(TorsionPreference {
243 angle_deg: 0.0,
244 penalty_per_degree: 0.08,
245 });
246 }
247 if b_type == AtomType::CCarbonyl && c_type == AtomType::CAromatic {
248 return Some(TorsionPreference {
249 angle_deg: 0.0,
250 penalty_per_degree: 0.08,
251 });
252 }
253
254 if b_type == AtomType::S && c_type == AtomType::CCarbonyl {
256 return Some(TorsionPreference {
257 angle_deg: 180.0,
258 penalty_per_degree: 0.10,
259 });
260 }
261
262 if b_type == AtomType::NSp3 && c_type == AtomType::CCarbonyl {
264 return Some(TorsionPreference {
265 angle_deg: 180.0,
266 penalty_per_degree: 0.10,
267 });
268 }
269
270 if b_type == AtomType::S && c_type == AtomType::CSp3 {
272 return Some(TorsionPreference {
273 angle_deg: 90.0,
274 penalty_per_degree: 0.06,
275 });
276 }
277
278 if b_type == AtomType::S && c_type == AtomType::S {
280 return Some(TorsionPreference {
281 angle_deg: 90.0,
282 penalty_per_degree: 0.12,
283 });
284 }
285
286 if (b_type == AtomType::CSp3 || b_type == AtomType::CSp2Alkene) && c_type == AtomType::OSp3 {
288 return Some(TorsionPreference {
289 angle_deg: 180.0,
290 penalty_per_degree: 0.07,
291 });
292 }
293
294 if b_type == AtomType::CSp3 && (c_type == AtomType::NSp3 || c_type == AtomType::NSp2) {
298 return Some(TorsionPreference {
299 angle_deg: 180.0,
300 penalty_per_degree: 0.08,
301 });
302 }
303
304 if b_type == AtomType::NSp || d_type == AtomType::NSp {
306 return Some(TorsionPreference {
307 angle_deg: 180.0,
308 penalty_per_degree: 0.20,
309 });
310 }
311
312 if b_type == AtomType::P || c_type == AtomType::P {
314 return Some(TorsionPreference {
315 angle_deg: 180.0,
316 penalty_per_degree: 0.06,
317 });
318 }
319
320 if b_type == AtomType::NSp2
322 && c_type == AtomType::NSp2
323 && mol
324 .neighbors(b_idx)
325 .any(|(n, _)| classify_atom_type(mol, n) == AtomType::CCarbonyl)
326 {
327 return Some(TorsionPreference {
328 angle_deg: 0.0,
329 penalty_per_degree: 0.18,
330 });
331 }
332
333 if (b_type == AtomType::NSp3 || b_type == AtomType::NSp2) && c_type == AtomType::S {
335 return Some(TorsionPreference {
336 angle_deg: 90.0,
337 penalty_per_degree: 0.08,
338 });
339 }
340 if b_type == AtomType::S && (c_type == AtomType::NSp3 || c_type == AtomType::NSp2) {
341 return Some(TorsionPreference {
342 angle_deg: 90.0,
343 penalty_per_degree: 0.08,
344 });
345 }
346
347 if b_type == AtomType::CAromatic && c_type == AtomType::OSp3 {
349 return Some(TorsionPreference {
350 angle_deg: 0.0,
351 penalty_per_degree: 0.09,
352 });
353 }
354 if b_type == AtomType::OSp3 && c_type == AtomType::CAromatic {
355 return Some(TorsionPreference {
356 angle_deg: 0.0,
357 penalty_per_degree: 0.09,
358 });
359 }
360
361 if (b_type == AtomType::CSp3 || b_type == AtomType::CSp2Alkene)
363 && c_type == AtomType::Halogen
364 && (d_type == AtomType::H || d_type == AtomType::Halogen)
365 {
366 return Some(TorsionPreference {
367 angle_deg: 180.0,
368 penalty_per_degree: 0.07,
369 });
370 }
371
372 if b_type == AtomType::CAromatic && c_type == AtomType::NSp2 {
374 return Some(TorsionPreference {
375 angle_deg: 0.0,
376 penalty_per_degree: 0.12,
377 });
378 }
379
380 if (b_type == AtomType::CSp2Alkene || b_type == AtomType::CCarbonyl)
382 && (c_type == AtomType::NSp2 || c_type == AtomType::OSp3)
383 {
384 return Some(TorsionPreference {
385 angle_deg: 0.0,
386 penalty_per_degree: 0.11,
387 });
388 }
389
390 if b_type == AtomType::NSp2
392 && c_type == AtomType::CCarbonyl
393 && mol
394 .neighbors(c_idx)
395 .any(|(n, _)| classify_atom_type(mol, n) == AtomType::CCarbonyl)
396 {
397 return Some(TorsionPreference {
398 angle_deg: 0.0,
399 penalty_per_degree: 0.15,
400 });
401 }
402
403 if b_type == AtomType::CAromatic && c_type == AtomType::CSp3 {
405 return Some(TorsionPreference {
406 angle_deg: 90.0,
407 penalty_per_degree: 0.04,
408 });
409 }
410
411 if b_type == AtomType::CSp2Alkene && c_type == AtomType::CSp3 {
413 return Some(TorsionPreference {
414 angle_deg: 0.0,
415 penalty_per_degree: 0.05,
416 });
417 }
418
419 if (b_type == AtomType::NAromatic && c_type == AtomType::CAromatic)
429 || (b_type == AtomType::CAromatic && c_type == AtomType::NAromatic)
430 {
431 return Some(TorsionPreference {
432 angle_deg: 45.0,
433 penalty_per_degree: 0.03,
434 });
435 }
436
437 if (b_type == AtomType::NAromatic && c_type == AtomType::CSp3)
440 || (b_type == AtomType::CSp3 && c_type == AtomType::NAromatic)
441 {
442 return Some(TorsionPreference {
443 angle_deg: 180.0,
444 penalty_per_degree: 0.09,
445 });
446 }
447
448 if (b_type == AtomType::NAromatic && c_type == AtomType::CCarbonyl)
451 || (b_type == AtomType::CCarbonyl && c_type == AtomType::NAromatic)
452 {
453 return Some(TorsionPreference {
454 angle_deg: 0.0,
455 penalty_per_degree: 0.10,
456 });
457 }
458
459 if (b_type == AtomType::NAromatic && c_type == AtomType::CSp2Alkene)
461 || (b_type == AtomType::CSp2Alkene && c_type == AtomType::NAromatic)
462 {
463 return Some(TorsionPreference {
464 angle_deg: 0.0,
465 penalty_per_degree: 0.08,
466 });
467 }
468
469 if (b_type == AtomType::S && c_type == AtomType::CAromatic)
473 || (b_type == AtomType::CAromatic && c_type == AtomType::S)
474 {
475 return Some(TorsionPreference {
476 angle_deg: 90.0,
477 penalty_per_degree: 0.06,
478 });
479 }
480
481 if b_type == AtomType::OSp3 && c_type == AtomType::CSp3 {
484 return Some(TorsionPreference {
485 angle_deg: 180.0,
486 penalty_per_degree: 0.07,
487 });
488 }
489
490 if (b_type == AtomType::CAromatic && c_type == AtomType::NSp3)
495 || (b_type == AtomType::NSp3 && c_type == AtomType::CAromatic)
496 {
497 return Some(TorsionPreference {
498 angle_deg: 0.0,
499 penalty_per_degree: 0.07,
500 });
501 }
502
503 if (b_type == AtomType::OAromatic && c_type == AtomType::CAromatic)
509 || (b_type == AtomType::CAromatic && c_type == AtomType::OAromatic)
510 {
511 return Some(TorsionPreference {
512 angle_deg: 0.0,
513 penalty_per_degree: 0.05,
514 });
515 }
516
517 if (b_type == AtomType::SAromatic && c_type == AtomType::CAromatic)
521 || (b_type == AtomType::CAromatic && c_type == AtomType::SAromatic)
522 {
523 return Some(TorsionPreference {
524 angle_deg: 45.0,
525 penalty_per_degree: 0.04,
526 });
527 }
528
529 if (b_type == AtomType::OAromatic && c_type == AtomType::CSp3)
531 || (b_type == AtomType::CSp3 && c_type == AtomType::OAromatic)
532 {
533 return Some(TorsionPreference {
534 angle_deg: 180.0,
535 penalty_per_degree: 0.06,
536 });
537 }
538
539 if (b_type == AtomType::SAromatic && c_type == AtomType::CSp3)
541 || (b_type == AtomType::CSp3 && c_type == AtomType::SAromatic)
542 {
543 return Some(TorsionPreference {
544 angle_deg: 180.0,
545 penalty_per_degree: 0.06,
546 });
547 }
548
549 if (b_type == AtomType::OAromatic && c_type == AtomType::CCarbonyl)
551 || (b_type == AtomType::CCarbonyl && c_type == AtomType::OAromatic)
552 || (b_type == AtomType::SAromatic && c_type == AtomType::CCarbonyl)
553 || (b_type == AtomType::CCarbonyl && c_type == AtomType::SAromatic)
554 {
555 return Some(TorsionPreference {
556 angle_deg: 0.0,
557 penalty_per_degree: 0.10,
558 });
559 }
560
561 let is_sat_n = |t: AtomType| t == AtomType::NSp2 || t == AtomType::NSp3;
567 if b_type == AtomType::CSp3
568 && c_type == AtomType::CSp3
569 && ((is_sat_n(a_type) && d_type == AtomType::OSp3)
570 || (a_type == AtomType::OSp3 && is_sat_n(d_type)))
571 {
572 return Some(TorsionPreference {
573 angle_deg: 60.0,
574 penalty_per_degree: 0.10,
575 });
576 }
577
578 if b_type == AtomType::CSp3 && c_type == AtomType::CSp3 && is_sat_n(a_type) && is_sat_n(d_type)
580 {
581 return Some(TorsionPreference {
582 angle_deg: 60.0,
583 penalty_per_degree: 0.10,
584 });
585 }
586
587 if b_type == AtomType::CAromatic && c_type == AtomType::CSp2Alkene {
592 return Some(TorsionPreference {
593 angle_deg: 0.0,
594 penalty_per_degree: 0.07,
595 });
596 }
597 if b_type == AtomType::CSp2Alkene && c_type == AtomType::CAromatic {
598 return Some(TorsionPreference {
599 angle_deg: 0.0,
600 penalty_per_degree: 0.07,
601 });
602 }
603
604 if b_type == AtomType::CSp2Alkene && c_type == AtomType::S {
607 return Some(TorsionPreference {
608 angle_deg: 0.0,
609 penalty_per_degree: 0.07,
610 });
611 }
612 if b_type == AtomType::S && c_type == AtomType::CSp2Alkene {
613 return Some(TorsionPreference {
614 angle_deg: 0.0,
615 penalty_per_degree: 0.07,
616 });
617 }
618
619 if b_type == AtomType::CSp2Alkene && c_type == AtomType::NSp3 {
622 return Some(TorsionPreference {
623 angle_deg: 0.0,
624 penalty_per_degree: 0.06,
625 });
626 }
627 if b_type == AtomType::NSp3 && c_type == AtomType::CSp2Alkene {
628 return Some(TorsionPreference {
629 angle_deg: 0.0,
630 penalty_per_degree: 0.06,
631 });
632 }
633
634 if b_type == AtomType::CSp3 && c_type == AtomType::CCarbonyl {
638 return Some(TorsionPreference {
639 angle_deg: 0.0,
640 penalty_per_degree: 0.04,
641 });
642 }
643 if b_type == AtomType::CCarbonyl && c_type == AtomType::CSp3 {
644 return Some(TorsionPreference {
645 angle_deg: 0.0,
646 penalty_per_degree: 0.04,
647 });
648 }
649
650 if b_type == AtomType::NAromatic && c_type == AtomType::S {
654 return Some(TorsionPreference {
655 angle_deg: 90.0,
656 penalty_per_degree: 0.06,
657 });
658 }
659 if b_type == AtomType::S && c_type == AtomType::NAromatic {
660 return Some(TorsionPreference {
661 angle_deg: 90.0,
662 penalty_per_degree: 0.06,
663 });
664 }
665
666 if b_type == AtomType::NAromatic && c_type == AtomType::OSp3 {
671 return Some(TorsionPreference {
672 angle_deg: 0.0,
673 penalty_per_degree: 0.08,
674 });
675 }
676 if b_type == AtomType::OSp3 && c_type == AtomType::NAromatic {
677 return Some(TorsionPreference {
678 angle_deg: 0.0,
679 penalty_per_degree: 0.08,
680 });
681 }
682
683 if (b_type == AtomType::SAromatic && c_type == AtomType::NAromatic)
687 || (b_type == AtomType::NAromatic && c_type == AtomType::SAromatic)
688 {
689 return Some(TorsionPreference {
690 angle_deg: 45.0,
691 penalty_per_degree: 0.03,
692 });
693 }
694
695 if (b_type == AtomType::OAromatic && c_type == AtomType::NAromatic)
696 || (b_type == AtomType::NAromatic && c_type == AtomType::OAromatic)
697 {
698 return Some(TorsionPreference {
699 angle_deg: 45.0,
700 penalty_per_degree: 0.04,
701 });
702 }
703
704 if b_type == AtomType::CCarbonyl && c_type == AtomType::CSp2Alkene {
709 return Some(TorsionPreference {
710 angle_deg: 0.0,
711 penalty_per_degree: 0.06,
712 });
713 }
714
715 if b_type == AtomType::CSp3 && c_type == AtomType::NSp {
719 return Some(TorsionPreference {
720 angle_deg: 180.0,
721 penalty_per_degree: 0.15,
722 });
723 }
724
725 if b_type == AtomType::CCarbonyl && c_type == AtomType::NSp2 {
730 return Some(TorsionPreference {
731 angle_deg: 180.0,
732 penalty_per_degree: 0.20,
733 });
734 }
735 if b_type == AtomType::CCarbonyl && c_type == AtomType::NSp3 {
736 return Some(TorsionPreference {
737 angle_deg: 180.0,
738 penalty_per_degree: 0.10,
739 });
740 }
741
742 if b_type == AtomType::NSp && c_type == AtomType::CCarbonyl {
745 return Some(TorsionPreference {
746 angle_deg: 180.0,
747 penalty_per_degree: 0.18,
748 });
749 }
750 if b_type == AtomType::CCarbonyl && c_type == AtomType::NSp {
751 return Some(TorsionPreference {
752 angle_deg: 180.0,
753 penalty_per_degree: 0.18,
754 });
755 }
756
757 if b_type == AtomType::CAromatic && c_type == AtomType::NSp {
760 return Some(TorsionPreference {
761 angle_deg: 0.0,
762 penalty_per_degree: 0.10,
763 });
764 }
765 if b_type == AtomType::NSp && c_type == AtomType::CAromatic {
766 return Some(TorsionPreference {
767 angle_deg: 0.0,
768 penalty_per_degree: 0.10,
769 });
770 }
771
772 if b_type == AtomType::NSp2 && c_type == AtomType::CSp2Alkene {
777 let has_thio = mol.neighbors(c_idx).any(|(n, _)| {
779 mol.atom(n).element.atomic_number() == 16
780 && mol
781 .bond_between(c_idx, n)
782 .map(|(_, b)| b.order == chematic_core::BondOrder::Double)
783 .unwrap_or(false)
784 });
785 if has_thio {
786 return Some(TorsionPreference {
787 angle_deg: 180.0,
788 penalty_per_degree: 0.15,
789 });
790 }
791 }
792
793 if b_type == AtomType::CSp3
798 && c_type == AtomType::CSp3
799 && (a_type == AtomType::OSp3 || a_type == AtomType::OAromatic)
800 && (d_type == AtomType::OSp3 || d_type == AtomType::OAromatic)
801 {
802 return Some(TorsionPreference {
803 angle_deg: 60.0,
804 penalty_per_degree: 0.08,
805 });
806 }
807
808 if b_type == AtomType::OSp3 && c_type == AtomType::CCarbonyl {
812 return Some(TorsionPreference {
813 angle_deg: 0.0,
814 penalty_per_degree: 0.10,
815 });
816 }
817
818 if b_type == AtomType::CSp2Alkene && c_type == AtomType::CSp2Alkene {
824 return Some(TorsionPreference {
825 angle_deg: 180.0,
826 penalty_per_degree: 0.08,
827 });
828 }
829
830 if b_type == AtomType::CCarbonyl && c_type == AtomType::CCarbonyl {
833 return Some(TorsionPreference {
834 angle_deg: 0.0,
835 penalty_per_degree: 0.10,
836 });
837 }
838
839 if b_type == AtomType::CAromatic && c_type == AtomType::Halogen {
845 return Some(TorsionPreference {
846 angle_deg: 180.0,
847 penalty_per_degree: 0.03,
848 });
849 }
850
851 if b_type == AtomType::P && c_type == AtomType::OSp3 {
855 return Some(TorsionPreference {
856 angle_deg: 180.0,
857 penalty_per_degree: 0.06,
858 });
859 }
860 if b_type == AtomType::OSp3 && c_type == AtomType::P {
861 return Some(TorsionPreference {
862 angle_deg: 180.0,
863 penalty_per_degree: 0.06,
864 });
865 }
866
867 None }
869
870pub struct SmartsTorsionRule {
880 pub smarts: &'static str,
881 pub b_qi: usize,
883 pub c_qi: usize,
885 pub angle_deg: f64,
886 pub penalty_per_degree: f64,
887}
888
889static SMARTS_TORSION_RULES: &[SmartsTorsionRule] = &[
894 SmartsTorsionRule {
897 smarts: "[c;H0;X3][c;H0;X3]",
898 b_qi: 0,
899 c_qi: 1,
900 angle_deg: 90.0,
901 penalty_per_degree: 0.04,
902 },
903 SmartsTorsionRule {
906 smarts: "[NH2][C;!a](=O)",
907 b_qi: 0,
908 c_qi: 1,
909 angle_deg: 0.0,
910 penalty_per_degree: 0.18,
911 },
912 SmartsTorsionRule {
914 smarts: "[N;H0;X3][C;!a](=O)",
915 b_qi: 0,
916 c_qi: 1,
917 angle_deg: 180.0,
918 penalty_per_degree: 0.18,
919 },
920 SmartsTorsionRule {
923 smarts: "[n][C;!a](=O)",
924 b_qi: 0,
925 c_qi: 1,
926 angle_deg: 0.0,
927 penalty_per_degree: 0.14,
928 },
929 SmartsTorsionRule {
932 smarts: "[c][O;H0][C;!a](=O)",
933 b_qi: 1,
934 c_qi: 2,
935 angle_deg: 0.0,
936 penalty_per_degree: 0.12,
937 },
938 SmartsTorsionRule {
941 smarts: "[N;!a][C;!a](=O)[O;!a]",
942 b_qi: 0,
943 c_qi: 1,
944 angle_deg: 0.0,
945 penalty_per_degree: 0.12,
946 },
947];
948
949pub fn build_smarts_torsion_map(
959 mol: &Molecule,
960 ring_bond_set: &std::collections::HashSet<(u32, u32)>,
961) -> HashMap<(u32, u32), TorsionPreference> {
962 let mut map: HashMap<(u32, u32), TorsionPreference> = HashMap::new();
963
964 for rule in SMARTS_TORSION_RULES {
965 let Ok(query) = parse_smarts(rule.smarts) else {
966 continue;
967 };
968 for m in find_matches(&query, mol) {
969 let (Some(b_atom), Some(c_atom)) = (m.get(&rule.b_qi), m.get(&rule.c_qi)) else {
970 continue;
971 };
972 let b = b_atom.0;
973 let c = c_atom.0;
974 let key_fwd = (b, c);
976 let key_rev = (c, b);
977 if ring_bond_set.contains(&key_fwd) {
978 continue;
979 }
980 if mol.bond_between(AtomIdx(b), AtomIdx(c)).is_none() {
982 continue;
983 }
984 let pref = TorsionPreference {
985 angle_deg: rule.angle_deg,
986 penalty_per_degree: rule.penalty_per_degree,
987 };
988 map.entry(key_fwd).or_insert_with(|| pref.clone());
990 map.entry(key_rev).or_insert(pref);
991 }
992 }
993
994 map
995}
996
997pub fn default_torsion_preference() -> TorsionPreference {
999 TorsionPreference {
1000 angle_deg: 180.0, penalty_per_degree: 0.10,
1002 }
1003}
1004
1005fn normalize_angle(angle_deg: f64) -> f64 {
1007 let mut norm = angle_deg % 360.0;
1008 if norm > 180.0 {
1009 norm -= 360.0;
1010 } else if norm < -180.0 {
1011 norm += 360.0;
1012 }
1013 norm
1014}
1015
1016pub fn score_torsion(angle_deg: f64, preference: &TorsionPreference) -> f64 {
1020 let norm = normalize_angle(angle_deg);
1021 let pref = normalize_angle(preference.angle_deg);
1022
1023 let diff = (norm - pref).abs();
1025 let min_diff = if diff > 180.0 { 360.0 - diff } else { diff };
1026
1027 min_diff * preference.penalty_per_degree
1029}
1030
1031#[cfg(test)]
1032mod tests {
1033 use super::*;
1034 use chematic_smiles::parse;
1035
1036 #[test]
1037 fn test_atom_type_methane() {
1038 let mol = parse("C").unwrap();
1039 assert_eq!(classify_atom_type(&mol, AtomIdx(0)), AtomType::CSp3);
1040 }
1041
1042 #[test]
1043 fn test_atom_type_ethene() {
1044 let mol = parse("C=C").unwrap();
1045 assert_eq!(classify_atom_type(&mol, AtomIdx(0)), AtomType::CSp2Alkene);
1046 assert_eq!(classify_atom_type(&mol, AtomIdx(1)), AtomType::CSp2Alkene);
1047 }
1048
1049 #[test]
1050 fn test_atom_type_benzene() {
1051 let mol = parse("c1ccccc1").unwrap();
1052 assert_eq!(classify_atom_type(&mol, AtomIdx(0)), AtomType::CAromatic);
1053 }
1054
1055 #[test]
1056 fn test_atom_type_acetaldehyde() {
1057 let mol = parse("CC=O").unwrap();
1058 let c_sp3 = if mol.atom(AtomIdx(0)).aromatic {
1059 classify_atom_type(&mol, AtomIdx(1))
1060 } else {
1061 classify_atom_type(&mol, AtomIdx(0))
1062 };
1063 assert_eq!(c_sp3, AtomType::CSp3);
1064 }
1065
1066 #[test]
1067 fn test_torsion_score_perfect_match() {
1068 let pref = TorsionPreference {
1069 angle_deg: 180.0,
1070 penalty_per_degree: 0.1,
1071 };
1072 let score = score_torsion(180.0, &pref);
1073 assert!(score.abs() < 1e-6, "perfect match should have zero penalty");
1074 }
1075
1076 #[test]
1077 fn test_torsion_score_deviation() {
1078 let pref = TorsionPreference {
1079 angle_deg: 180.0,
1080 penalty_per_degree: 0.1,
1081 };
1082 let score = score_torsion(160.0, &pref);
1083 assert!(
1084 (score - 2.0).abs() < 1e-6,
1085 "20° deviation should yield 2.0 penalty"
1086 );
1087 }
1088
1089 #[test]
1090 fn test_torsion_score_periodic() {
1091 let pref = TorsionPreference {
1092 angle_deg: 180.0,
1093 penalty_per_degree: 0.1,
1094 };
1095 let score1 = score_torsion(180.0, &pref);
1097 let score2 = score_torsion(-180.0, &pref);
1098 assert!(
1099 (score1 - score2).abs() < 1e-6,
1100 "periodic angles should score the same"
1101 );
1102 }
1103
1104 #[test]
1105 fn test_default_torsion_preference() {
1106 let pref = default_torsion_preference();
1107 assert_eq!(pref.angle_deg, 180.0);
1108 assert!(pref.penalty_per_degree > 0.0);
1109 }
1110
1111 #[test]
1112 fn test_alkane_torsion_preference() {
1113 let mol = parse("CCCC").unwrap(); if mol.atom_count() >= 4 {
1115 let pref = get_torsion_preference(&mol, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3));
1116 assert!(pref.is_some(), "butane C-C-C-C should have preference");
1117 if let Some(p) = pref {
1118 assert_eq!(p.angle_deg, 180.0, "alkane torsions prefer 180°");
1119 }
1120 }
1121 }
1122
1123 #[test]
1124 fn test_biphenyl_torsion_preference() {
1125 let mol = parse("c1ccccc1-c1ccccc1").unwrap(); let pref = get_torsion_preference(&mol, AtomIdx(1), AtomIdx(0), AtomIdx(6), AtomIdx(7));
1128 assert!(
1129 pref.is_some(),
1130 "biphenyl Ar-Ar should have a torsion preference"
1131 );
1132 if let Some(p) = pref {
1133 assert_eq!(p.angle_deg, 45.0, "biphenyl prefers ~45° twist");
1134 }
1135 }
1136
1137 #[test]
1138 fn test_thioester_torsion_preference() {
1139 let mol = parse("SC=O").unwrap();
1141 let _pref = get_torsion_preference(&mol, AtomIdx(0), AtomIdx(0), AtomIdx(1), AtomIdx(2));
1142 let b_type = classify_atom_type(&mol, AtomIdx(0));
1144 let c_type = classify_atom_type(&mol, AtomIdx(1));
1145 assert_eq!(b_type, AtomType::S);
1146 assert_eq!(c_type, AtomType::CCarbonyl);
1147 }
1148
1149 #[test]
1150 fn test_disulfide_torsion_preference() {
1151 let mol = parse("CSSC").unwrap(); let b_type = classify_atom_type(&mol, AtomIdx(1));
1154 let c_type = classify_atom_type(&mol, AtomIdx(2));
1155 assert_eq!(b_type, AtomType::S);
1156 assert_eq!(c_type, AtomType::S);
1157 let pref = get_torsion_preference(&mol, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3));
1158 assert!(pref.is_some(), "disulfide should have ~90° preference");
1159 if let Some(p) = pref {
1160 assert_eq!(p.angle_deg, 90.0, "disulfide prefers 90°");
1161 }
1162 }
1163
1164 #[test]
1165 fn test_nitrile_torsion_preference() {
1166 let mol = parse("CCC#N").unwrap(); let n_type = classify_atom_type(&mol, AtomIdx(3));
1169 assert_eq!(n_type, AtomType::NSp, "nitrile N should be NSp");
1170 let pref = get_torsion_preference(&mol, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3));
1171 assert!(pref.is_some(), "nitrile torsion should have a preference");
1172 if let Some(p) = pref {
1173 assert_eq!(p.angle_deg, 180.0, "linear nitrile end prefers 180°");
1174 }
1175 }
1176
1177 #[test]
1178 fn test_amine_torsion_preference() {
1179 let mol = parse("CCNC").unwrap(); let pref = get_torsion_preference(&mol, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3));
1182 assert!(pref.is_some(), "amine C-C-N-C should have preference");
1183 if let Some(p) = pref {
1184 assert_eq!(p.angle_deg, 180.0);
1185 }
1186 }
1187
1188 #[test]
1189 fn test_phenyl_ketone_torsion_preference() {
1190 let mol = parse("c1ccccc1C(=O)C").unwrap(); let c_carbonyl_idx = (0..mol.atom_count() as u32)
1194 .map(AtomIdx)
1195 .find(|&i| classify_atom_type(&mol, i) == AtomType::CCarbonyl);
1196 assert!(
1197 c_carbonyl_idx.is_some(),
1198 "acetophenone should have a carbonyl C"
1199 );
1200 }
1201
1202 #[test]
1203 fn test_score_torsion_disulfide_at_90() {
1204 let pref = TorsionPreference {
1205 angle_deg: 90.0,
1206 penalty_per_degree: 0.1,
1207 };
1208 let score = score_torsion(90.0, &pref);
1209 assert!(score.abs() < 1e-6, "at preferred angle score should be 0");
1210 let score_off = score_torsion(90.0 + 20.0, &pref);
1211 assert!((score_off - 2.0).abs() < 1e-6);
1212 }
1213
1214 #[test]
1215 fn test_pattern_count_covers_20_plus() {
1216 let mol_alkane = parse("CCCC").unwrap();
1219 let mol_biphenyl = parse("c1ccccc1-c1ccccc1").unwrap();
1220 let mol_amide = parse("CC(=O)N").unwrap();
1221 let mol_ester = parse("CC(=O)OC").unwrap();
1222 let mol_disulfide = parse("CSSC").unwrap();
1223 let mol_nitrile = parse("CCC#N").unwrap();
1224 let mol_amine = parse("CCNC").unwrap();
1225
1226 let cases = [
1227 (&mol_alkane, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3)),
1228 (
1229 &mol_biphenyl,
1230 AtomIdx(1),
1231 AtomIdx(0),
1232 AtomIdx(6),
1233 AtomIdx(7),
1234 ),
1235 (
1236 &mol_disulfide,
1237 AtomIdx(0),
1238 AtomIdx(1),
1239 AtomIdx(2),
1240 AtomIdx(3),
1241 ),
1242 (&mol_nitrile, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3)),
1243 (&mol_amine, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3)),
1244 ];
1245 for (mol, a, b, c, d) in &cases {
1246 let pref = get_torsion_preference(mol, *a, *b, *c, *d);
1247 let _ = pref; }
1250 let pref_amide =
1252 get_torsion_preference(&mol_amide, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(2));
1253 let _ = pref_amide;
1254 let pref_ester =
1255 get_torsion_preference(&mol_ester, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3));
1256 let _ = pref_ester;
1257 }
1258
1259 #[test]
1262 fn test_smarts_map_hindered_biaryl() {
1263 let mol = parse("Cc1ccccc1-c1ccccc1C").unwrap();
1265 let ring_set = chematic_perception::find_sssr(&mol);
1266 let ring_bonds: std::collections::HashSet<(u32, u32)> = ring_set
1267 .rings()
1268 .iter()
1269 .flat_map(|r| {
1270 let n = r.len();
1271 (0..n).flat_map(move |i| {
1272 let a = r[i].0;
1273 let b = r[(i + 1) % n].0;
1274 [(a, b), (b, a)]
1275 })
1276 })
1277 .collect();
1278 let map = build_smarts_torsion_map(&mol, &ring_bonds);
1279 let has_90 = map.values().any(|p| (p.angle_deg - 90.0).abs() < 1.0);
1281 assert!(
1282 has_90,
1283 "hindered biaryl should get 90° preference in SMARTS map"
1284 );
1285 }
1286
1287 #[test]
1288 fn test_smarts_map_primary_amide() {
1289 let mol = parse("CC(=O)N").unwrap();
1291 let ring_bonds = std::collections::HashSet::new();
1292 let map = build_smarts_torsion_map(&mol, &ring_bonds);
1293 let has_0 = map.values().any(|p| p.angle_deg.abs() < 1.0);
1295 assert!(has_0, "primary amide N-C bond should get 0° preference");
1296 }
1297
1298 #[test]
1301 fn test_atom_type_furan_oxygen() {
1302 let mol = parse("c1ccco1").unwrap();
1304 let o_idx = (0..mol.atom_count() as u32)
1305 .map(AtomIdx)
1306 .find(|&i| mol.atom(i).element.atomic_number() == 8)
1307 .expect("furan must have an oxygen atom");
1308 assert_eq!(
1309 classify_atom_type(&mol, o_idx),
1310 AtomType::OAromatic,
1311 "furan O should be OAromatic"
1312 );
1313 }
1314
1315 #[test]
1316 fn test_atom_type_thiophene_sulfur() {
1317 let mol = parse("c1cccs1").unwrap();
1319 let s_idx = (0..mol.atom_count() as u32)
1320 .map(AtomIdx)
1321 .find(|&i| mol.atom(i).element.atomic_number() == 16)
1322 .expect("thiophene must have a sulfur atom");
1323 assert_eq!(
1324 classify_atom_type(&mol, s_idx),
1325 AtomType::SAromatic,
1326 "thiophene S should be SAromatic"
1327 );
1328 }
1329
1330 #[test]
1331 fn test_furanyl_biaryl_prefers_planar() {
1332 let mol = parse("c1ccc(-c2ccco2)cc1").unwrap();
1336 let o_idx = (0..mol.atom_count() as u32)
1338 .map(AtomIdx)
1339 .find(|&i| mol.atom(i).element.atomic_number() == 8)
1340 .expect("must have O");
1341 let o_neighbor = mol
1342 .neighbors(o_idx)
1343 .next()
1344 .map(|(n, _)| n)
1345 .expect("O has neighbors");
1346 assert_eq!(classify_atom_type(&mol, o_idx), AtomType::OAromatic);
1347 assert_eq!(classify_atom_type(&mol, o_neighbor), AtomType::CAromatic);
1348 let pref = get_torsion_preference(&mol, o_idx, o_idx, o_neighbor, o_neighbor);
1350 let pref2 = get_torsion_preference(&mol, AtomIdx(0), o_idx, o_neighbor, AtomIdx(0));
1352 assert!(
1353 pref.is_some() || pref2.is_some(),
1354 "OAromatic–CAromatic should have a torsion preference"
1355 );
1356 }
1357
1358 #[test]
1359 fn test_morpholine_gauche_preference() {
1360 let mol = parse("C1CNCCO1").unwrap();
1365 let n_idx = (0..mol.atom_count() as u32)
1367 .map(AtomIdx)
1368 .find(|&i| mol.atom(i).element.atomic_number() == 7)
1369 .expect("morpholine must have N");
1370 let o_idx = (0..mol.atom_count() as u32)
1371 .map(AtomIdx)
1372 .find(|&i| mol.atom(i).element.atomic_number() == 8)
1373 .expect("morpholine must have O");
1374 let pref = get_torsion_preference(&mol, n_idx, AtomIdx(3), AtomIdx(4), o_idx);
1377 assert!(
1378 pref.is_some(),
1379 "morpholine N-C-C-O should have gauche preference"
1380 );
1381 if let Some(p) = pref {
1382 assert_eq!(p.angle_deg, 60.0, "morpholine N-C-C-O prefers 60°");
1383 }
1384 }
1385}