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