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