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