chematic_perception/
pharmacophore.rs1use chematic_core::{AtomIdx, Molecule};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum FeatureType {
12 Donor,
14 Acceptor,
16 Aromatic,
18 Hydrophobic,
20 Positive,
22 Negative,
24}
25
26#[derive(Debug, Clone)]
28pub struct Feature {
29 pub ftype: FeatureType,
31 pub atom: AtomIdx,
33 pub neighbors: Vec<AtomIdx>,
35}
36
37pub fn detect_features(mol: &Molecule) -> Vec<Feature> {
39 let mut features = Vec::new();
40
41 for idx in 0..mol.atom_count() {
42 let atom_idx = AtomIdx(idx as u32);
43 let atom = mol.atom(atom_idx);
44
45 match atom.element {
46 chematic_core::Element::N => {
47 if atom.charge == 0 {
49 features.push(Feature {
50 ftype: FeatureType::Donor,
51 atom: atom_idx,
52 neighbors: vec![],
53 });
54 }
55
56 if !atom.aromatic {
58 features.push(Feature {
59 ftype: FeatureType::Acceptor,
60 atom: atom_idx,
61 neighbors: vec![],
62 });
63 }
64
65 if atom.charge > 0 {
67 features.push(Feature {
68 ftype: FeatureType::Positive,
69 atom: atom_idx,
70 neighbors: vec![],
71 });
72 }
73 }
74 chematic_core::Element::O => {
75 features.push(Feature {
77 ftype: FeatureType::Donor,
78 atom: atom_idx,
79 neighbors: vec![],
80 });
81
82 features.push(Feature {
84 ftype: FeatureType::Acceptor,
85 atom: atom_idx,
86 neighbors: vec![],
87 });
88
89 if atom.charge < 0 {
91 features.push(Feature {
92 ftype: FeatureType::Negative,
93 atom: atom_idx,
94 neighbors: vec![],
95 });
96 }
97 }
98 chematic_core::Element::C => {
99 if atom.aromatic {
101 let ring_atoms = collect_aromatic_ring(mol, atom_idx);
102 if !ring_atoms.is_empty() {
103 features.push(Feature {
104 ftype: FeatureType::Aromatic,
105 atom: atom_idx,
106 neighbors: ring_atoms,
107 });
108 }
109 }
110
111 if !atom.aromatic {
113 features.push(Feature {
114 ftype: FeatureType::Hydrophobic,
115 atom: atom_idx,
116 neighbors: vec![],
117 });
118 }
119 }
120 chematic_core::Element::BR | chematic_core::Element::CL | chematic_core::Element::I => {
121 features.push(Feature {
123 ftype: FeatureType::Hydrophobic,
124 atom: atom_idx,
125 neighbors: vec![],
126 });
127 }
128 chematic_core::Element::S => {
129 let h_count = count_hydrogens(mol, atom_idx);
131 if h_count > 0 {
132 features.push(Feature {
133 ftype: FeatureType::Donor,
134 atom: atom_idx,
135 neighbors: vec![],
136 });
137 }
138 features.push(Feature {
139 ftype: FeatureType::Acceptor,
140 atom: atom_idx,
141 neighbors: vec![],
142 });
143 }
144 _ => {}
145 }
146 }
147
148 features
149}
150
151fn count_hydrogens(mol: &Molecule, atom: AtomIdx) -> u8 {
153 (0..mol.atom_count())
154 .filter(|&i| {
155 let idx = AtomIdx(i as u32);
156 mol.atom(idx).element == chematic_core::Element::H
157 && mol.bond_between(atom, idx).is_some()
158 })
159 .count() as u8
160}
161
162fn collect_aromatic_ring(mol: &Molecule, start: AtomIdx) -> Vec<AtomIdx> {
164 if !mol.atom(start).aromatic {
165 return vec![];
166 }
167
168 let mut ring = vec![start];
169 let mut visited = std::collections::HashSet::from([start]);
170 let mut queue = std::collections::VecDeque::from([start]);
171
172 while let Some(current) = queue.pop_front() {
173 for neighbor_idx in 0..mol.atom_count() {
174 let neighbor = AtomIdx(neighbor_idx as u32);
175 if visited.contains(&neighbor) {
176 continue;
177 }
178
179 if mol.bond_between(current, neighbor).is_some() && mol.atom(neighbor).aromatic {
180 visited.insert(neighbor);
181 queue.push_back(neighbor);
182 ring.push(neighbor);
183 }
184 }
185 }
186
187 ring.sort_by_key(|a| a.0);
188 ring
189}
190
191pub fn features_to_bitvec(features: &[Feature]) -> u64 {
194 let mut bits = 0u64;
195
196 for (i, feature) in features.iter().enumerate() {
197 if i >= 8 {
198 break; }
200
201 let feature_bits = match feature.ftype {
202 FeatureType::Donor => 0b001,
203 FeatureType::Acceptor => 0b010,
204 FeatureType::Aromatic => 0b011,
205 FeatureType::Hydrophobic => 0b100,
206 FeatureType::Positive => 0b101,
207 FeatureType::Negative => 0b110,
208 };
209
210 bits |= (feature_bits as u64) << (i * 3);
211 }
212
213 bits
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use chematic_smiles::parse;
220
221 #[test]
222 fn test_benzene_aromatic() {
223 let mol = parse("c1ccccc1").unwrap();
224 let features = detect_features(&mol);
225 let aromatic_count = features
226 .iter()
227 .filter(|f| f.ftype == FeatureType::Aromatic)
228 .count();
229 assert!(aromatic_count > 0, "benzene should have aromatic feature");
230 }
231
232 #[test]
233 fn test_ethanol_donor_acceptor() {
234 let mol = parse("CCO").unwrap();
235 let features = detect_features(&mol);
236 let has_donor = features.iter().any(|f| f.ftype == FeatureType::Donor);
237 let has_acceptor = features.iter().any(|f| f.ftype == FeatureType::Acceptor);
238 assert!(has_donor, "ethanol should have donor (O-H)");
239 assert!(has_acceptor, "ethanol should have acceptor (O)");
240 }
241
242 #[test]
243 fn test_aniline_donor_aromatic() {
244 let mol = parse("Nc1ccccc1").unwrap();
245 let features = detect_features(&mol);
246 let has_donor = features.iter().any(|f| f.ftype == FeatureType::Donor);
247 let has_aromatic = features.iter().any(|f| f.ftype == FeatureType::Aromatic);
248 assert!(has_donor, "aniline should have N-H donor");
249 assert!(has_aromatic, "aniline should have aromatic rings");
250 }
251}