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