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_completeness, validate_stereo,
54};
55pub use stereo2d::{
56 StereoAssignment2D, apply_stereo_from_2d, assign_ez_from_2d, assign_stereo_from_2d,
57 cip_ez_descriptor,
58};
59pub use stereo2d_ez_direction::{
60 EzDirectionDiagnostic, EzDirectionRejectionReason, apply_ez_directions_from_2d,
61 apply_ez_directions_from_2d_ex, apply_ez_directions_from_2d_with_diagnostics,
62};
63pub use stereo2d_local::{
64 StereoDiagnostic, StereoRejectionReason, apply_local_parity_from_wedges,
65 apply_local_parity_from_wedges_with_diagnostics, local_parity_from_wedges,
66};
67
68use chematic_core::{AtomIdx, Molecule};
69
70pub fn ring_membership(mol: &Molecule) -> Vec<Vec<usize>> {
80 let ring_set = find_sssr(mol);
81 let rings = ring_set.rings();
82 let n = mol.atom_count();
83 let mut membership: Vec<Vec<usize>> = vec![Vec::new(); n];
84 for (ring_idx, ring) in rings.iter().enumerate() {
85 for &atom in ring {
86 membership[atom.0 as usize].push(ring_idx);
87 }
88 }
89 membership
90}
91
92pub fn ring_sizes_for_atom(mol: &Molecule, atom_idx: usize) -> Vec<usize> {
96 let ring_set = find_sssr(mol);
97 let target = AtomIdx(atom_idx as u32);
98 ring_set
99 .rings()
100 .iter()
101 .filter(|ring| ring.contains(&target))
102 .map(|ring| ring.len())
103 .collect()
104}
105
106pub fn is_fused_ring_system(mol: &Molecule) -> bool {
111 let ring_set = find_sssr(mol);
112 let rings = ring_set.rings();
113 for i in 0..rings.len() {
114 for j in (i + 1)..rings.len() {
115 let shared = rings[i].iter().filter(|a| rings[j].contains(a)).count();
117 if shared >= 2 {
118 return true; }
120 }
121 }
122 false
123}
124
125pub fn aromatize(mol: &mut Molecule) {
127 *mol = apply_aromaticity(mol);
128}
129
130pub fn kekulize_inplace(mol: &mut Molecule) -> Result<(), chematic_core::KekuleError> {
134 use chematic_core::{apply_kekule, kekulize};
135 let result = kekulize(mol)?;
136 *mol = apply_kekule(mol, &result);
137 Ok(())
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use chematic_smiles::parse;
144
145 fn mol(smiles: &str) -> Molecule {
146 parse(smiles).expect("valid SMILES")
147 }
148
149 #[test]
150 fn test_ring_membership_benzene() {
151 let m = mol("c1ccccc1");
152 let membership = ring_membership(&m);
153 assert_eq!(membership.len(), 6);
154 for atom_membership in membership.iter().take(6) {
155 assert_eq!(
156 atom_membership.len(),
157 1,
158 "each benzene atom in exactly 1 ring"
159 );
160 assert_eq!(atom_membership[0], 0, "all in ring index 0");
161 }
162 }
163
164 #[test]
165 fn test_ring_membership_naphthalene() {
166 let m = mol("c1ccc2ccccc2c1");
167 let membership = ring_membership(&m);
168 assert_eq!(membership.len(), 10);
169 for mem in &membership {
172 assert!(
173 !mem.is_empty(),
174 "all naphthalene atoms should be in at least 1 ring"
175 );
176 }
177 }
178
179 #[test]
180 fn test_ring_membership_acyclic() {
181 let m = mol("CC");
182 let membership = ring_membership(&m);
183 assert_eq!(membership.len(), 2);
184 for mem in &membership {
185 assert!(mem.is_empty(), "ethane atoms should not be in rings");
186 }
187 }
188
189 #[test]
190 fn test_ring_sizes_for_atom_benzene() {
191 let m = mol("c1ccccc1");
192 let sizes = ring_sizes_for_atom(&m, 0);
193 assert_eq!(sizes, vec![6]);
194 }
195
196 #[test]
197 fn test_ring_sizes_for_atom_naphthalene() {
198 let m = mol("c1ccc2ccccc2c1");
199 let sizes = ring_sizes_for_atom(&m, 0);
201 assert!(!sizes.is_empty());
202 assert!(sizes.contains(&6), "naphthalene has 6-membered rings");
203 }
204
205 #[test]
206 fn test_ring_sizes_for_atom_acyclic() {
207 let m = mol("CC");
208 let sizes = ring_sizes_for_atom(&m, 0);
209 assert!(sizes.is_empty());
210 }
211
212 #[test]
213 fn test_is_fused_ring_naphthalene() {
214 let m = mol("c1ccc2ccccc2c1");
215 assert!(is_fused_ring_system(&m), "naphthalene is fused");
216 }
217
218 #[test]
219 fn test_is_fused_ring_benzene() {
220 let m = mol("c1ccccc1");
221 assert!(
222 !is_fused_ring_system(&m),
223 "single benzene ring is not fused"
224 );
225 }
226
227 #[test]
228 fn test_is_fused_ring_spiro() {
229 let m = mol("C1CCC2(C1)CCCC2");
231 assert!(
232 !is_fused_ring_system(&m),
233 "spiro compound shares only 1 atom, not fused"
234 );
235 }
236
237 #[test]
238 fn test_aromatize_benzene() {
239 let mut m = mol("c1ccccc1");
240 aromatize(&mut m);
241 for (_, atom) in m.atoms() {
242 assert!(atom.aromatic, "all benzene atoms should be aromatic");
243 }
244 for (_, bond) in m.bonds() {
245 assert_eq!(
246 bond.order,
247 chematic_core::BondOrder::Aromatic,
248 "all benzene bonds should be aromatic"
249 );
250 }
251 }
252
253 #[test]
254 fn test_kekulize_inplace_benzene() {
255 let mut m = mol("c1ccccc1");
256 kekulize_inplace(&mut m).expect("benzene should kekulize");
257 let mut single_count = 0;
258 let mut double_count = 0;
259 for (_, bond) in m.bonds() {
260 match bond.order {
261 chematic_core::BondOrder::Single => single_count += 1,
262 chematic_core::BondOrder::Double => double_count += 1,
263 _ => panic!("unexpected bond order after kekulization"),
264 }
265 }
266 assert_eq!(single_count, 3);
267 assert_eq!(double_count, 3);
268 }
269}