1#![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_ez_direction;
19pub mod stereo2d_local;
20
21pub use aromaticity::{
22 AromaticityAlgorithm, AromaticityModel, AtomElectronTrace, ConjugatedComponent,
23 ContributionDecision, ContributionReason, PiEligibility, RingAromaticity, RingElectronTrace,
24 all_ring_list, apply_aromaticity, apply_aromaticity_authoritative_experimental,
25 apply_aromaticity_ex, aromatic_ring_list, assign_aromaticity,
26 assign_aromaticity_authoritative_experimental, assign_aromaticity_ex, augmented_ring_set,
27 build_conjugated_components, count_aromatic_rings, evaluate_atom_pi_contribution,
28 exhaustive_aromaticity_oracle, ring_bonds_all_aromatic, trace_ring_pi_electrons,
29};
30pub use chematic_core::{ValenceError, validate_valence};
31pub use pharmacophore::{Feature, FeatureType, detect_features, features_to_bitvec};
32pub use rdkit_parity::{
33 AromaticityError, apply_aromaticity_rdkit_parity_experimental,
34 assign_aromaticity_rdkit_parity_experimental,
35};
36pub use ring_family::{RingFamily, RingSystemKind, find_ring_families, find_ring_families_over};
37
38#[cfg(feature = "diagnostics")]
47#[doc(hidden)]
48pub mod diagnostics {
49 pub use crate::rdkit_parity::rdkit_parity_aromaticity;
50}
51pub use sssr::{
52 RingSet, SymmetrizedSssrResult, SymmetrizedSssrStatus, find_smallest_rings_bfs,
53 find_smallest_rings_bfs_with_blocked_bonds, find_smallest_rings_bfs_with_rdkit_tree,
54 find_smallest_rings_bfs_with_trimmed_bonds, find_sssr, find_symmetrized_sssr,
55 find_symmetrized_sssr_with_diagnostics, select_rdkit_d2_roots, trim_ring_bonds,
56};
57pub use stereo_validation::{
58 StereoCompleteness, StereoError, StereoErrorKind, stereo_centers, stereo_completeness,
59 validate_stereo,
60};
61pub use stereo2d::{
62 StereoAssignment2D, apply_stereo_from_2d, assign_ez_from_2d, assign_stereo_from_2d,
63 cip_ez_descriptor,
64};
65pub use stereo2d_ez_direction::{
66 EzDirectionDiagnostic, EzDirectionRejectionReason, apply_ez_directions_from_2d,
67 apply_ez_directions_from_2d_ex, apply_ez_directions_from_2d_with_diagnostics,
68};
69pub use stereo2d_local::{
70 StereoDiagnostic, StereoRejectionReason, apply_local_parity_from_wedges,
71 apply_local_parity_from_wedges_with_diagnostics, local_parity_from_wedges,
72};
73
74use chematic_core::{AtomIdx, Molecule};
75
76pub fn ring_membership(mol: &Molecule) -> Vec<Vec<usize>> {
86 let ring_set = find_sssr(mol);
87 let rings = ring_set.rings();
88 let n = mol.atom_count();
89 let mut membership: Vec<Vec<usize>> = vec![Vec::new(); n];
90 for (ring_idx, ring) in rings.iter().enumerate() {
91 for &atom in ring {
92 membership[atom.0 as usize].push(ring_idx);
93 }
94 }
95 membership
96}
97
98pub fn ring_sizes_for_atom(mol: &Molecule, atom_idx: usize) -> Vec<usize> {
102 let ring_set = find_sssr(mol);
103 let target = AtomIdx(atom_idx as u32);
104 ring_set
105 .rings()
106 .iter()
107 .filter(|ring| ring.contains(&target))
108 .map(|ring| ring.len())
109 .collect()
110}
111
112pub fn is_fused_ring_system(mol: &Molecule) -> bool {
117 let ring_set = find_sssr(mol);
118 let rings = ring_set.rings();
119 for i in 0..rings.len() {
120 for j in (i + 1)..rings.len() {
121 let shared = rings[i].iter().filter(|a| rings[j].contains(a)).count();
123 if shared >= 2 {
124 return true; }
126 }
127 }
128 false
129}
130
131pub fn is_macrocycle(ring: &[AtomIdx]) -> bool {
147 ring.len() >= 9
148}
149
150pub fn aromatize(mol: &mut Molecule) {
152 *mol = apply_aromaticity(mol);
153}
154
155pub fn kekulize_inplace(mol: &mut Molecule) -> Result<(), chematic_core::KekuleError> {
159 use chematic_core::{apply_kekule, kekulize};
160 let result = kekulize(mol)?;
161 *mol = apply_kekule(mol, &result);
162 Ok(())
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use chematic_smiles::parse;
169
170 fn mol(smiles: &str) -> Molecule {
171 parse(smiles).expect("valid SMILES")
172 }
173
174 #[test]
175 fn test_ring_membership_benzene() {
176 let m = mol("c1ccccc1");
177 let membership = ring_membership(&m);
178 assert_eq!(membership.len(), 6);
179 for atom_membership in membership.iter().take(6) {
180 assert_eq!(
181 atom_membership.len(),
182 1,
183 "each benzene atom in exactly 1 ring"
184 );
185 assert_eq!(atom_membership[0], 0, "all in ring index 0");
186 }
187 }
188
189 #[test]
190 fn test_ring_membership_naphthalene() {
191 let m = mol("c1ccc2ccccc2c1");
192 let membership = ring_membership(&m);
193 assert_eq!(membership.len(), 10);
194 for mem in &membership {
197 assert!(
198 !mem.is_empty(),
199 "all naphthalene atoms should be in at least 1 ring"
200 );
201 }
202 }
203
204 #[test]
205 fn test_ring_membership_acyclic() {
206 let m = mol("CC");
207 let membership = ring_membership(&m);
208 assert_eq!(membership.len(), 2);
209 for mem in &membership {
210 assert!(mem.is_empty(), "ethane atoms should not be in rings");
211 }
212 }
213
214 #[test]
215 fn test_ring_sizes_for_atom_benzene() {
216 let m = mol("c1ccccc1");
217 let sizes = ring_sizes_for_atom(&m, 0);
218 assert_eq!(sizes, vec![6]);
219 }
220
221 #[test]
222 fn test_ring_sizes_for_atom_naphthalene() {
223 let m = mol("c1ccc2ccccc2c1");
224 let sizes = ring_sizes_for_atom(&m, 0);
226 assert!(!sizes.is_empty());
227 assert!(sizes.contains(&6), "naphthalene has 6-membered rings");
228 }
229
230 #[test]
231 fn test_ring_sizes_for_atom_acyclic() {
232 let m = mol("CC");
233 let sizes = ring_sizes_for_atom(&m, 0);
234 assert!(sizes.is_empty());
235 }
236
237 #[test]
238 fn test_is_fused_ring_naphthalene() {
239 let m = mol("c1ccc2ccccc2c1");
240 assert!(is_fused_ring_system(&m), "naphthalene is fused");
241 }
242
243 #[test]
244 fn test_is_fused_ring_benzene() {
245 let m = mol("c1ccccc1");
246 assert!(
247 !is_fused_ring_system(&m),
248 "single benzene ring is not fused"
249 );
250 }
251
252 #[test]
253 fn test_is_fused_ring_spiro() {
254 let m = mol("C1CCC2(C1)CCCC2");
256 assert!(
257 !is_fused_ring_system(&m),
258 "spiro compound shares only 1 atom, not fused"
259 );
260 }
261
262 #[test]
263 fn test_is_macrocycle_boundary() {
264 let ring8: Vec<AtomIdx> = (0..8).map(AtomIdx).collect();
266 let ring9: Vec<AtomIdx> = (0..9).map(AtomIdx).collect();
267 assert!(
268 !is_macrocycle(&ring8),
269 "8-membered ring is not a macrocycle"
270 );
271 assert!(is_macrocycle(&ring9), "9-membered ring is a macrocycle");
272 }
273
274 #[test]
275 fn test_is_macrocycle_cyclododecane() {
276 let m = mol("C1CCCCCCCCCCC1");
278 let ring_set = find_sssr(&m);
279 let rings = ring_set.rings();
280 assert_eq!(rings.len(), 1);
281 assert!(is_macrocycle(&rings[0]), "cyclododecane is a macrocycle");
282 }
283
284 #[test]
285 fn test_is_macrocycle_small_rings() {
286 let benzene = mol("c1ccccc1");
288 let rings = find_sssr(&benzene);
289 assert!(
290 !is_macrocycle(&rings.rings()[0]),
291 "benzene is not a macrocycle"
292 );
293
294 let cyclohexane = mol("C1CCCCC1");
295 let rings = find_sssr(&cyclohexane);
296 assert!(
297 !is_macrocycle(&rings.rings()[0]),
298 "cyclohexane is not a macrocycle"
299 );
300 }
301
302 #[test]
303 fn test_aromatize_benzene() {
304 let mut m = mol("c1ccccc1");
305 aromatize(&mut m);
306 for (_, atom) in m.atoms() {
307 assert!(atom.aromatic, "all benzene atoms should be aromatic");
308 }
309 for (_, bond) in m.bonds() {
310 assert_eq!(
311 bond.order,
312 chematic_core::BondOrder::Aromatic,
313 "all benzene bonds should be aromatic"
314 );
315 }
316 }
317
318 #[test]
319 fn test_kekulize_inplace_benzene() {
320 let mut m = mol("c1ccccc1");
321 kekulize_inplace(&mut m).expect("benzene should kekulize");
322 let mut single_count = 0;
323 let mut double_count = 0;
324 for (_, bond) in m.bonds() {
325 match bond.order {
326 chematic_core::BondOrder::Single => single_count += 1,
327 chematic_core::BondOrder::Double => double_count += 1,
328 _ => panic!("unexpected bond order after kekulization"),
329 }
330 }
331 assert_eq!(single_count, 3);
332 assert_eq!(double_count, 3);
333 }
334}