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, c_qi: 1,
899 angle_deg: 90.0,
900 penalty_per_degree: 0.04,
901 },
902 SmartsTorsionRule {
905 smarts: "[NH2][C;!a](=O)",
906 b_qi: 0, c_qi: 1,
907 angle_deg: 0.0,
908 penalty_per_degree: 0.18,
909 },
910 SmartsTorsionRule {
912 smarts: "[N;H0;X3][C;!a](=O)",
913 b_qi: 0, c_qi: 1,
914 angle_deg: 180.0,
915 penalty_per_degree: 0.18,
916 },
917 SmartsTorsionRule {
920 smarts: "[n][C;!a](=O)",
921 b_qi: 0, c_qi: 1,
922 angle_deg: 0.0,
923 penalty_per_degree: 0.14,
924 },
925 SmartsTorsionRule {
928 smarts: "[c][O;H0][C;!a](=O)",
929 b_qi: 1, c_qi: 2,
930 angle_deg: 0.0,
931 penalty_per_degree: 0.12,
932 },
933 SmartsTorsionRule {
936 smarts: "[N;!a][C;!a](=O)[O;!a]",
937 b_qi: 0, c_qi: 1,
938 angle_deg: 0.0,
939 penalty_per_degree: 0.12,
940 },
941];
942
943pub fn build_smarts_torsion_map(
953 mol: &Molecule,
954 ring_bond_set: &std::collections::HashSet<(u32, u32)>,
955) -> HashMap<(u32, u32), TorsionPreference> {
956 let mut map: HashMap<(u32, u32), TorsionPreference> = HashMap::new();
957
958 for rule in SMARTS_TORSION_RULES {
959 let Ok(query) = parse_smarts(rule.smarts) else {
960 continue;
961 };
962 for m in find_matches(&query, mol) {
963 let (Some(b_atom), Some(c_atom)) = (m.get(&rule.b_qi), m.get(&rule.c_qi)) else {
964 continue;
965 };
966 let b = b_atom.0;
967 let c = c_atom.0;
968 let key_fwd = (b, c);
970 let key_rev = (c, b);
971 if ring_bond_set.contains(&key_fwd) {
972 continue;
973 }
974 if mol.bond_between(AtomIdx(b), AtomIdx(c)).is_none() {
976 continue;
977 }
978 let pref = TorsionPreference {
979 angle_deg: rule.angle_deg,
980 penalty_per_degree: rule.penalty_per_degree,
981 };
982 map.entry(key_fwd).or_insert_with(|| pref.clone());
984 map.entry(key_rev).or_insert(pref);
985 }
986 }
987
988 map
989}
990
991pub fn default_torsion_preference() -> TorsionPreference {
993 TorsionPreference {
994 angle_deg: 180.0, penalty_per_degree: 0.10,
996 }
997}
998
999fn normalize_angle(angle_deg: f64) -> f64 {
1001 let mut norm = angle_deg % 360.0;
1002 if norm > 180.0 {
1003 norm -= 360.0;
1004 } else if norm < -180.0 {
1005 norm += 360.0;
1006 }
1007 norm
1008}
1009
1010pub fn score_torsion(angle_deg: f64, preference: &TorsionPreference) -> f64 {
1014 let norm = normalize_angle(angle_deg);
1015 let pref = normalize_angle(preference.angle_deg);
1016
1017 let diff = (norm - pref).abs();
1019 let min_diff = if diff > 180.0 { 360.0 - diff } else { diff };
1020
1021 min_diff * preference.penalty_per_degree
1023}
1024
1025#[cfg(test)]
1026mod tests {
1027 use super::*;
1028 use chematic_smiles::parse;
1029
1030 #[test]
1031 fn test_atom_type_methane() {
1032 let mol = parse("C").unwrap();
1033 assert_eq!(classify_atom_type(&mol, AtomIdx(0)), AtomType::CSp3);
1034 }
1035
1036 #[test]
1037 fn test_atom_type_ethene() {
1038 let mol = parse("C=C").unwrap();
1039 assert_eq!(classify_atom_type(&mol, AtomIdx(0)), AtomType::CSp2Alkene);
1040 assert_eq!(classify_atom_type(&mol, AtomIdx(1)), AtomType::CSp2Alkene);
1041 }
1042
1043 #[test]
1044 fn test_atom_type_benzene() {
1045 let mol = parse("c1ccccc1").unwrap();
1046 assert_eq!(classify_atom_type(&mol, AtomIdx(0)), AtomType::CAromatic);
1047 }
1048
1049 #[test]
1050 fn test_atom_type_acetaldehyde() {
1051 let mol = parse("CC=O").unwrap();
1052 let c_sp3 = if mol.atom(AtomIdx(0)).aromatic {
1053 classify_atom_type(&mol, AtomIdx(1))
1054 } else {
1055 classify_atom_type(&mol, AtomIdx(0))
1056 };
1057 assert_eq!(c_sp3, AtomType::CSp3);
1058 }
1059
1060 #[test]
1061 fn test_torsion_score_perfect_match() {
1062 let pref = TorsionPreference {
1063 angle_deg: 180.0,
1064 penalty_per_degree: 0.1,
1065 };
1066 let score = score_torsion(180.0, &pref);
1067 assert!(score.abs() < 1e-6, "perfect match should have zero penalty");
1068 }
1069
1070 #[test]
1071 fn test_torsion_score_deviation() {
1072 let pref = TorsionPreference {
1073 angle_deg: 180.0,
1074 penalty_per_degree: 0.1,
1075 };
1076 let score = score_torsion(160.0, &pref);
1077 assert!(
1078 (score - 2.0).abs() < 1e-6,
1079 "20° deviation should yield 2.0 penalty"
1080 );
1081 }
1082
1083 #[test]
1084 fn test_torsion_score_periodic() {
1085 let pref = TorsionPreference {
1086 angle_deg: 180.0,
1087 penalty_per_degree: 0.1,
1088 };
1089 let score1 = score_torsion(180.0, &pref);
1091 let score2 = score_torsion(-180.0, &pref);
1092 assert!(
1093 (score1 - score2).abs() < 1e-6,
1094 "periodic angles should score the same"
1095 );
1096 }
1097
1098 #[test]
1099 fn test_default_torsion_preference() {
1100 let pref = default_torsion_preference();
1101 assert_eq!(pref.angle_deg, 180.0);
1102 assert!(pref.penalty_per_degree > 0.0);
1103 }
1104
1105 #[test]
1106 fn test_alkane_torsion_preference() {
1107 let mol = parse("CCCC").unwrap(); if mol.atom_count() >= 4 {
1109 let pref = get_torsion_preference(&mol, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3));
1110 assert!(pref.is_some(), "butane C-C-C-C should have preference");
1111 if let Some(p) = pref {
1112 assert_eq!(p.angle_deg, 180.0, "alkane torsions prefer 180°");
1113 }
1114 }
1115 }
1116
1117 #[test]
1118 fn test_biphenyl_torsion_preference() {
1119 let mol = parse("c1ccccc1-c1ccccc1").unwrap(); let pref = get_torsion_preference(&mol, AtomIdx(1), AtomIdx(0), AtomIdx(6), AtomIdx(7));
1122 assert!(
1123 pref.is_some(),
1124 "biphenyl Ar-Ar should have a torsion preference"
1125 );
1126 if let Some(p) = pref {
1127 assert_eq!(p.angle_deg, 45.0, "biphenyl prefers ~45° twist");
1128 }
1129 }
1130
1131 #[test]
1132 fn test_thioester_torsion_preference() {
1133 let mol = parse("SC=O").unwrap();
1135 let _pref = get_torsion_preference(&mol, AtomIdx(0), AtomIdx(0), AtomIdx(1), AtomIdx(2));
1136 let b_type = classify_atom_type(&mol, AtomIdx(0));
1138 let c_type = classify_atom_type(&mol, AtomIdx(1));
1139 assert_eq!(b_type, AtomType::S);
1140 assert_eq!(c_type, AtomType::CCarbonyl);
1141 }
1142
1143 #[test]
1144 fn test_disulfide_torsion_preference() {
1145 let mol = parse("CSSC").unwrap(); let b_type = classify_atom_type(&mol, AtomIdx(1));
1148 let c_type = classify_atom_type(&mol, AtomIdx(2));
1149 assert_eq!(b_type, AtomType::S);
1150 assert_eq!(c_type, AtomType::S);
1151 let pref = get_torsion_preference(&mol, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3));
1152 assert!(pref.is_some(), "disulfide should have ~90° preference");
1153 if let Some(p) = pref {
1154 assert_eq!(p.angle_deg, 90.0, "disulfide prefers 90°");
1155 }
1156 }
1157
1158 #[test]
1159 fn test_nitrile_torsion_preference() {
1160 let mol = parse("CCC#N").unwrap(); let n_type = classify_atom_type(&mol, AtomIdx(3));
1163 assert_eq!(n_type, AtomType::NSp, "nitrile N should be NSp");
1164 let pref = get_torsion_preference(&mol, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3));
1165 assert!(pref.is_some(), "nitrile torsion should have a preference");
1166 if let Some(p) = pref {
1167 assert_eq!(p.angle_deg, 180.0, "linear nitrile end prefers 180°");
1168 }
1169 }
1170
1171 #[test]
1172 fn test_amine_torsion_preference() {
1173 let mol = parse("CCNC").unwrap(); let pref = get_torsion_preference(&mol, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3));
1176 assert!(pref.is_some(), "amine C-C-N-C should have preference");
1177 if let Some(p) = pref {
1178 assert_eq!(p.angle_deg, 180.0);
1179 }
1180 }
1181
1182 #[test]
1183 fn test_phenyl_ketone_torsion_preference() {
1184 let mol = parse("c1ccccc1C(=O)C").unwrap(); let c_carbonyl_idx = (0..mol.atom_count() as u32)
1188 .map(AtomIdx)
1189 .find(|&i| classify_atom_type(&mol, i) == AtomType::CCarbonyl);
1190 assert!(
1191 c_carbonyl_idx.is_some(),
1192 "acetophenone should have a carbonyl C"
1193 );
1194 }
1195
1196 #[test]
1197 fn test_score_torsion_disulfide_at_90() {
1198 let pref = TorsionPreference {
1199 angle_deg: 90.0,
1200 penalty_per_degree: 0.1,
1201 };
1202 let score = score_torsion(90.0, &pref);
1203 assert!(score.abs() < 1e-6, "at preferred angle score should be 0");
1204 let score_off = score_torsion(90.0 + 20.0, &pref);
1205 assert!((score_off - 2.0).abs() < 1e-6);
1206 }
1207
1208 #[test]
1209 fn test_pattern_count_covers_20_plus() {
1210 let mol_alkane = parse("CCCC").unwrap();
1213 let mol_biphenyl = parse("c1ccccc1-c1ccccc1").unwrap();
1214 let mol_amide = parse("CC(=O)N").unwrap();
1215 let mol_ester = parse("CC(=O)OC").unwrap();
1216 let mol_disulfide = parse("CSSC").unwrap();
1217 let mol_nitrile = parse("CCC#N").unwrap();
1218 let mol_amine = parse("CCNC").unwrap();
1219
1220 let cases = [
1221 (&mol_alkane, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3)),
1222 (
1223 &mol_biphenyl,
1224 AtomIdx(1),
1225 AtomIdx(0),
1226 AtomIdx(6),
1227 AtomIdx(7),
1228 ),
1229 (
1230 &mol_disulfide,
1231 AtomIdx(0),
1232 AtomIdx(1),
1233 AtomIdx(2),
1234 AtomIdx(3),
1235 ),
1236 (&mol_nitrile, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3)),
1237 (&mol_amine, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3)),
1238 ];
1239 for (mol, a, b, c, d) in &cases {
1240 let pref = get_torsion_preference(mol, *a, *b, *c, *d);
1241 let _ = pref; }
1244 let pref_amide =
1246 get_torsion_preference(&mol_amide, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(2));
1247 let _ = pref_amide;
1248 let pref_ester =
1249 get_torsion_preference(&mol_ester, AtomIdx(0), AtomIdx(1), AtomIdx(2), AtomIdx(3));
1250 let _ = pref_ester;
1251 }
1252
1253 #[test]
1256 fn test_smarts_map_hindered_biaryl() {
1257 let mol = parse("Cc1ccccc1-c1ccccc1C").unwrap();
1259 let ring_set = chematic_perception::find_sssr(&mol);
1260 let ring_bonds: std::collections::HashSet<(u32, u32)> = ring_set
1261 .rings()
1262 .iter()
1263 .flat_map(|r| {
1264 let n = r.len();
1265 (0..n).flat_map(move |i| {
1266 let a = r[i].0;
1267 let b = r[(i + 1) % n].0;
1268 [(a, b), (b, a)]
1269 })
1270 })
1271 .collect();
1272 let map = build_smarts_torsion_map(&mol, &ring_bonds);
1273 let has_90 = map.values().any(|p| (p.angle_deg - 90.0).abs() < 1.0);
1275 assert!(has_90, "hindered biaryl should get 90° preference in SMARTS map");
1276 }
1277
1278 #[test]
1279 fn test_smarts_map_primary_amide() {
1280 let mol = parse("CC(=O)N").unwrap();
1282 let ring_bonds = std::collections::HashSet::new();
1283 let map = build_smarts_torsion_map(&mol, &ring_bonds);
1284 let has_0 = map.values().any(|p| p.angle_deg.abs() < 1.0);
1286 assert!(has_0, "primary amide N-C bond should get 0° preference");
1287 }
1288
1289 #[test]
1292 fn test_atom_type_furan_oxygen() {
1293 let mol = parse("c1ccco1").unwrap();
1295 let o_idx = (0..mol.atom_count() as u32)
1296 .map(AtomIdx)
1297 .find(|&i| mol.atom(i).element.atomic_number() == 8)
1298 .expect("furan must have an oxygen atom");
1299 assert_eq!(
1300 classify_atom_type(&mol, o_idx),
1301 AtomType::OAromatic,
1302 "furan O should be OAromatic"
1303 );
1304 }
1305
1306 #[test]
1307 fn test_atom_type_thiophene_sulfur() {
1308 let mol = parse("c1cccs1").unwrap();
1310 let s_idx = (0..mol.atom_count() as u32)
1311 .map(AtomIdx)
1312 .find(|&i| mol.atom(i).element.atomic_number() == 16)
1313 .expect("thiophene must have a sulfur atom");
1314 assert_eq!(
1315 classify_atom_type(&mol, s_idx),
1316 AtomType::SAromatic,
1317 "thiophene S should be SAromatic"
1318 );
1319 }
1320
1321 #[test]
1322 fn test_furanyl_biaryl_prefers_planar() {
1323 let mol = parse("c1ccc(-c2ccco2)cc1").unwrap();
1327 let o_idx = (0..mol.atom_count() as u32)
1329 .map(AtomIdx)
1330 .find(|&i| mol.atom(i).element.atomic_number() == 8)
1331 .expect("must have O");
1332 let o_neighbor = mol
1333 .neighbors(o_idx)
1334 .next()
1335 .map(|(n, _)| n)
1336 .expect("O has neighbors");
1337 assert_eq!(classify_atom_type(&mol, o_idx), AtomType::OAromatic);
1338 assert_eq!(classify_atom_type(&mol, o_neighbor), AtomType::CAromatic);
1339 let pref = get_torsion_preference(&mol, o_idx, o_idx, o_neighbor, o_neighbor);
1341 let pref2 = get_torsion_preference(&mol, AtomIdx(0), o_idx, o_neighbor, AtomIdx(0));
1343 assert!(
1344 pref.is_some() || pref2.is_some(),
1345 "OAromatic–CAromatic should have a torsion preference"
1346 );
1347 }
1348
1349 #[test]
1350 fn test_morpholine_gauche_preference() {
1351 let mol = parse("C1CNCCO1").unwrap();
1356 let n_idx = (0..mol.atom_count() as u32)
1358 .map(AtomIdx)
1359 .find(|&i| mol.atom(i).element.atomic_number() == 7)
1360 .expect("morpholine must have N");
1361 let o_idx = (0..mol.atom_count() as u32)
1362 .map(AtomIdx)
1363 .find(|&i| mol.atom(i).element.atomic_number() == 8)
1364 .expect("morpholine must have O");
1365 let pref = get_torsion_preference(&mol, n_idx, AtomIdx(3), AtomIdx(4), o_idx);
1368 assert!(
1369 pref.is_some(),
1370 "morpholine N-C-C-O should have gauche preference"
1371 );
1372 if let Some(p) = pref {
1373 assert_eq!(p.angle_deg, 60.0, "morpholine N-C-C-O prefers 60°");
1374 }
1375 }
1376}