chematic_perception/
lib.rs1#![forbid(unsafe_code)]
8
9pub mod aromaticity;
10pub mod cip_priority;
11pub mod pharmacophore;
12mod rdkit_parity;
13pub mod ring_family;
14pub mod sssr;
15pub mod stereo_validation;
16
17pub mod stereo2d;
18pub mod stereo2d_local;
19
20pub use aromaticity::{
21 AromaticityAlgorithm, AromaticityModel, AtomElectronTrace, ConjugatedComponent,
22 ContributionDecision, ContributionReason, PiEligibility, RingAromaticity, RingElectronTrace,
23 all_ring_list, apply_aromaticity, apply_aromaticity_authoritative_experimental,
24 apply_aromaticity_ex, aromatic_ring_list, assign_aromaticity,
25 assign_aromaticity_authoritative_experimental, assign_aromaticity_ex, augmented_ring_set,
26 build_conjugated_components, count_aromatic_rings, evaluate_atom_pi_contribution,
27 exhaustive_aromaticity_oracle, ring_bonds_all_aromatic, trace_ring_pi_electrons,
28};
29pub use chematic_core::{ValenceError, validate_valence};
30pub use pharmacophore::{Feature, FeatureType, detect_features, features_to_bitvec};
31pub use rdkit_parity::{
32 AromaticityError, apply_aromaticity_rdkit_parity_experimental,
33 assign_aromaticity_rdkit_parity_experimental,
34};
35pub use ring_family::{RingFamily, RingSystemKind, find_ring_families, find_ring_families_over};
36
37#[cfg(feature = "diagnostics")]
46#[doc(hidden)]
47pub mod diagnostics {
48 pub use crate::rdkit_parity::rdkit_parity_aromaticity;
49}
50pub use sssr::{RingSet, find_sssr};
51pub use stereo_validation::{
52 StereoCompleteness, StereoError, StereoErrorKind, stereo_completeness, validate_stereo,
53};
54pub use stereo2d::{
55 StereoAssignment2D, apply_stereo_from_2d, assign_ez_from_2d, assign_stereo_from_2d,
56 cip_ez_descriptor,
57};
58pub use stereo2d_local::{apply_local_parity_from_wedges, local_parity_from_wedges};
59
60use chematic_core::{AtomIdx, Molecule};
61
62pub fn ring_membership(mol: &Molecule) -> Vec<Vec<usize>> {
72 let ring_set = find_sssr(mol);
73 let rings = ring_set.rings();
74 let n = mol.atom_count();
75 let mut membership: Vec<Vec<usize>> = vec![Vec::new(); n];
76 for (ring_idx, ring) in rings.iter().enumerate() {
77 for &atom in ring {
78 membership[atom.0 as usize].push(ring_idx);
79 }
80 }
81 membership
82}
83
84pub fn ring_sizes_for_atom(mol: &Molecule, atom_idx: usize) -> Vec<usize> {
88 let ring_set = find_sssr(mol);
89 let target = AtomIdx(atom_idx as u32);
90 ring_set
91 .rings()
92 .iter()
93 .filter(|ring| ring.contains(&target))
94 .map(|ring| ring.len())
95 .collect()
96}
97
98pub fn is_fused_ring_system(mol: &Molecule) -> bool {
103 let ring_set = find_sssr(mol);
104 let rings = ring_set.rings();
105 for i in 0..rings.len() {
106 for j in (i + 1)..rings.len() {
107 let shared = rings[i].iter().filter(|a| rings[j].contains(a)).count();
109 if shared >= 2 {
110 return true; }
112 }
113 }
114 false
115}
116
117pub fn aromatize(mol: &mut Molecule) {
119 *mol = apply_aromaticity(mol);
120}
121
122pub fn kekulize_inplace(mol: &mut Molecule) -> Result<(), chematic_core::KekuleError> {
126 use chematic_core::{apply_kekule, kekulize};
127 let result = kekulize(mol)?;
128 *mol = apply_kekule(mol, &result);
129 Ok(())
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use chematic_smiles::parse;
136
137 fn mol(smiles: &str) -> Molecule {
138 parse(smiles).expect("valid SMILES")
139 }
140
141 #[test]
142 fn test_ring_membership_benzene() {
143 let m = mol("c1ccccc1");
144 let membership = ring_membership(&m);
145 assert_eq!(membership.len(), 6);
146 for atom_membership in membership.iter().take(6) {
147 assert_eq!(
148 atom_membership.len(),
149 1,
150 "each benzene atom in exactly 1 ring"
151 );
152 assert_eq!(atom_membership[0], 0, "all in ring index 0");
153 }
154 }
155
156 #[test]
157 fn test_ring_membership_naphthalene() {
158 let m = mol("c1ccc2ccccc2c1");
159 let membership = ring_membership(&m);
160 assert_eq!(membership.len(), 10);
161 for mem in &membership {
164 assert!(
165 !mem.is_empty(),
166 "all naphthalene atoms should be in at least 1 ring"
167 );
168 }
169 }
170
171 #[test]
172 fn test_ring_membership_acyclic() {
173 let m = mol("CC");
174 let membership = ring_membership(&m);
175 assert_eq!(membership.len(), 2);
176 for mem in &membership {
177 assert!(mem.is_empty(), "ethane atoms should not be in rings");
178 }
179 }
180
181 #[test]
182 fn test_ring_sizes_for_atom_benzene() {
183 let m = mol("c1ccccc1");
184 let sizes = ring_sizes_for_atom(&m, 0);
185 assert_eq!(sizes, vec![6]);
186 }
187
188 #[test]
189 fn test_ring_sizes_for_atom_naphthalene() {
190 let m = mol("c1ccc2ccccc2c1");
191 let sizes = ring_sizes_for_atom(&m, 0);
193 assert!(!sizes.is_empty());
194 assert!(sizes.contains(&6), "naphthalene has 6-membered rings");
195 }
196
197 #[test]
198 fn test_ring_sizes_for_atom_acyclic() {
199 let m = mol("CC");
200 let sizes = ring_sizes_for_atom(&m, 0);
201 assert!(sizes.is_empty());
202 }
203
204 #[test]
205 fn test_is_fused_ring_naphthalene() {
206 let m = mol("c1ccc2ccccc2c1");
207 assert!(is_fused_ring_system(&m), "naphthalene is fused");
208 }
209
210 #[test]
211 fn test_is_fused_ring_benzene() {
212 let m = mol("c1ccccc1");
213 assert!(
214 !is_fused_ring_system(&m),
215 "single benzene ring is not fused"
216 );
217 }
218
219 #[test]
220 fn test_is_fused_ring_spiro() {
221 let m = mol("C1CCC2(C1)CCCC2");
223 assert!(
224 !is_fused_ring_system(&m),
225 "spiro compound shares only 1 atom, not fused"
226 );
227 }
228
229 #[test]
230 fn test_aromatize_benzene() {
231 let mut m = mol("c1ccccc1");
232 aromatize(&mut m);
233 for (_, atom) in m.atoms() {
234 assert!(atom.aromatic, "all benzene atoms should be aromatic");
235 }
236 for (_, bond) in m.bonds() {
237 assert_eq!(
238 bond.order,
239 chematic_core::BondOrder::Aromatic,
240 "all benzene bonds should be aromatic"
241 );
242 }
243 }
244
245 #[test]
246 fn test_kekulize_inplace_benzene() {
247 let mut m = mol("c1ccccc1");
248 kekulize_inplace(&mut m).expect("benzene should kekulize");
249 let mut single_count = 0;
250 let mut double_count = 0;
251 for (_, bond) in m.bonds() {
252 match bond.order {
253 chematic_core::BondOrder::Single => single_count += 1,
254 chematic_core::BondOrder::Double => double_count += 1,
255 _ => panic!("unexpected bond order after kekulization"),
256 }
257 }
258 assert_eq!(single_count, 3);
259 assert_eq!(double_count, 3);
260 }
261}