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