chematic_perception/
lib.rs1#![forbid(unsafe_code)]
8
9pub mod aromaticity;
10pub mod cip_priority;
11pub mod pharmacophore;
12pub mod ring_family;
13pub mod sssr;
14pub mod stereo_validation;
15
16pub mod stereo2d;
17
18pub use aromaticity::{AromaticityModel, RingAromaticity, apply_aromaticity, assign_aromaticity};
19pub use chematic_core::{ValenceError, validate_valence};
20pub use pharmacophore::{Feature, FeatureType, detect_features, features_to_bitvec};
21pub use ring_family::{RingFamily, RingSystemKind, find_ring_families};
22pub use sssr::{RingSet, find_sssr};
23pub use stereo_validation::{
24 StereoCompleteness, StereoError, StereoErrorKind, stereo_completeness, validate_stereo,
25};
26pub use stereo2d::{
27 StereoAssignment2D, apply_stereo_from_2d, assign_ez_from_2d, assign_stereo_from_2d,
28 cip_ez_descriptor,
29};
30
31use chematic_core::{AtomIdx, Molecule};
32
33pub fn ring_membership(mol: &Molecule) -> Vec<Vec<usize>> {
43 let ring_set = find_sssr(mol);
44 let rings = ring_set.rings();
45 let n = mol.atom_count();
46 let mut membership: Vec<Vec<usize>> = vec![Vec::new(); n];
47 for (ring_idx, ring) in rings.iter().enumerate() {
48 for &atom in ring {
49 membership[atom.0 as usize].push(ring_idx);
50 }
51 }
52 membership
53}
54
55pub fn ring_sizes_for_atom(mol: &Molecule, atom_idx: usize) -> Vec<usize> {
59 let ring_set = find_sssr(mol);
60 let target = AtomIdx(atom_idx as u32);
61 ring_set
62 .rings()
63 .iter()
64 .filter(|ring| ring.contains(&target))
65 .map(|ring| ring.len())
66 .collect()
67}
68
69pub fn is_fused_ring_system(mol: &Molecule) -> bool {
74 let ring_set = find_sssr(mol);
75 let rings = ring_set.rings();
76 for i in 0..rings.len() {
77 for j in (i + 1)..rings.len() {
78 let shared = rings[i].iter().filter(|a| rings[j].contains(a)).count();
80 if shared >= 2 {
81 return true; }
83 }
84 }
85 false
86}
87
88pub fn aromatize(mol: &mut Molecule) {
90 *mol = apply_aromaticity(mol);
91}
92
93pub fn kekulize_inplace(mol: &mut Molecule) -> Result<(), chematic_core::KekuleError> {
97 use chematic_core::{apply_kekule, kekulize};
98 let result = kekulize(mol)?;
99 *mol = apply_kekule(mol, &result);
100 Ok(())
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use chematic_smiles::parse;
107
108 fn mol(smiles: &str) -> Molecule {
109 parse(smiles).expect("valid SMILES")
110 }
111
112 #[test]
113 fn test_ring_membership_benzene() {
114 let m = mol("c1ccccc1");
115 let membership = ring_membership(&m);
116 assert_eq!(membership.len(), 6);
117 for atom_membership in membership.iter().take(6) {
118 assert_eq!(atom_membership.len(), 1, "each benzene atom in exactly 1 ring");
119 assert_eq!(atom_membership[0], 0, "all in ring index 0");
120 }
121 }
122
123 #[test]
124 fn test_ring_membership_naphthalene() {
125 let m = mol("c1ccc2ccccc2c1");
126 let membership = ring_membership(&m);
127 assert_eq!(membership.len(), 10);
128 for mem in &membership {
131 assert!(!mem.is_empty(), "all naphthalene atoms should be in at least 1 ring");
132 }
133 }
134
135 #[test]
136 fn test_ring_membership_acyclic() {
137 let m = mol("CC");
138 let membership = ring_membership(&m);
139 assert_eq!(membership.len(), 2);
140 for mem in &membership {
141 assert!(mem.is_empty(), "ethane atoms should not be in rings");
142 }
143 }
144
145 #[test]
146 fn test_ring_sizes_for_atom_benzene() {
147 let m = mol("c1ccccc1");
148 let sizes = ring_sizes_for_atom(&m, 0);
149 assert_eq!(sizes, vec![6]);
150 }
151
152 #[test]
153 fn test_ring_sizes_for_atom_naphthalene() {
154 let m = mol("c1ccc2ccccc2c1");
155 let sizes = ring_sizes_for_atom(&m, 0);
157 assert!(!sizes.is_empty());
158 assert!(sizes.contains(&6), "naphthalene has 6-membered rings");
159 }
160
161 #[test]
162 fn test_ring_sizes_for_atom_acyclic() {
163 let m = mol("CC");
164 let sizes = ring_sizes_for_atom(&m, 0);
165 assert!(sizes.is_empty());
166 }
167
168 #[test]
169 fn test_is_fused_ring_naphthalene() {
170 let m = mol("c1ccc2ccccc2c1");
171 assert!(is_fused_ring_system(&m), "naphthalene is fused");
172 }
173
174 #[test]
175 fn test_is_fused_ring_benzene() {
176 let m = mol("c1ccccc1");
177 assert!(!is_fused_ring_system(&m), "single benzene ring is not fused");
178 }
179
180 #[test]
181 fn test_is_fused_ring_spiro() {
182 let m = mol("C1CCC2(C1)CCCC2");
184 assert!(!is_fused_ring_system(&m), "spiro compound shares only 1 atom, not fused");
185 }
186
187 #[test]
188 fn test_aromatize_benzene() {
189 let mut m = mol("c1ccccc1");
190 aromatize(&mut m);
191 for (_, atom) in m.atoms() {
192 assert!(atom.aromatic, "all benzene atoms should be aromatic");
193 }
194 for (_, bond) in m.bonds() {
195 assert_eq!(
196 bond.order,
197 chematic_core::BondOrder::Aromatic,
198 "all benzene bonds should be aromatic"
199 );
200 }
201 }
202
203 #[test]
204 fn test_kekulize_inplace_benzene() {
205 let mut m = mol("c1ccccc1");
206 kekulize_inplace(&mut m).expect("benzene should kekulize");
207 let mut single_count = 0;
208 let mut double_count = 0;
209 for (_, bond) in m.bonds() {
210 match bond.order {
211 chematic_core::BondOrder::Single => single_count += 1,
212 chematic_core::BondOrder::Double => double_count += 1,
213 _ => panic!("unexpected bond order after kekulization"),
214 }
215 }
216 assert_eq!(single_count, 3);
217 assert_eq!(double_count, 3);
218 }
219}