1use crate::bitvec::BitVec2048;
8use chematic_core::{AtomIdx, BondOrder, Molecule};
9
10const HASH_MOD: usize = 2048;
11
12#[derive(Clone, Debug)]
14pub struct RdkitPathConfig {
15 pub max_path_len: usize,
17 pub use_atom_types: bool,
19}
20
21impl Default for RdkitPathConfig {
22 fn default() -> Self {
23 Self {
24 max_path_len: 7,
25 use_atom_types: false,
26 }
27 }
28}
29
30pub fn rdkit_path_fp(mol: &Molecule) -> BitVec2048 {
35 rdkit_path_fp_with_config(mol, &RdkitPathConfig::default())
36}
37
38pub fn rdkit_path_fp_with_config(mol: &Molecule, config: &RdkitPathConfig) -> BitVec2048 {
40 let mut fp = BitVec2048::new();
41
42 if mol.atom_count() == 0 {
43 return fp;
44 }
45
46 for start_idx in 0..mol.atom_count() {
47 let start = AtomIdx(start_idx as u32);
48 let initial_path = PathWithBonds {
49 atoms: vec![start],
50 bonds: vec![],
51 };
52 enumerate_paths_from(
53 mol,
54 start,
55 initial_path,
56 &mut |path| {
57 let hash = hash_path(mol, path);
58 let bit_idx = hash % HASH_MOD;
59 fp.set(bit_idx);
60 },
61 config.max_path_len,
62 );
63 }
64
65 fp
66}
67
68struct PathWithBonds {
70 atoms: Vec<AtomIdx>,
71 bonds: Vec<BondOrder>, }
73
74fn enumerate_paths_from<F>(
76 mol: &Molecule,
77 current: AtomIdx,
78 path: PathWithBonds,
79 callback: &mut F,
80 max_len: usize,
81) where
82 F: FnMut(&PathWithBonds),
83{
84 if path.atoms.len() <= max_len {
85 if path.atoms.len() > 1 {
87 callback(&path);
88 }
89
90 if path.atoms.len() < max_len {
92 for (neighbor, bond_idx) in mol.neighbors(current) {
93 if !path.atoms.contains(&neighbor) {
95 let bond_order = mol.bond(bond_idx).order;
96 let mut new_path = path.clone();
97 new_path.atoms.push(neighbor);
98 new_path.bonds.push(bond_order);
99 enumerate_paths_from(mol, neighbor, new_path, callback, max_len);
100 }
101 }
102 }
103 }
104}
105
106impl Clone for PathWithBonds {
107 fn clone(&self) -> Self {
108 PathWithBonds {
109 atoms: self.atoms.clone(),
110 bonds: self.bonds.clone(),
111 }
112 }
113}
114
115fn hash_path(mol: &Molecule, path: &PathWithBonds) -> usize {
117 let mut bytes: Vec<u8> = Vec::with_capacity(path.atoms.len() * 2);
118 for (i, &atom_idx) in path.atoms.iter().enumerate() {
119 if i > 0 {
120 bytes.push(crate::ecfp::bond_type_int(path.bonds[i - 1]));
121 }
122 bytes.push(mol.atom(atom_idx).element.atomic_number());
123 }
124 crate::ecfp::fnv1a(&bytes) as usize
125}
126
127pub fn tanimoto_rdkit_path(a: &BitVec2048, b: &BitVec2048) -> f64 {
129 a.tanimoto(b)
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use chematic_smiles::parse;
136
137 fn mol(smiles: &str) -> Molecule {
138 parse(smiles).unwrap_or_else(|e| panic!("failed to parse {smiles:?}: {e}"))
139 }
140
141 #[test]
142 fn test_rdkit_path_fp_ethane() {
143 let m = mol("CC");
144 let fp = rdkit_path_fp(&m);
145 assert!(fp.popcount() > 0, "ethane should have non-zero bits");
146 }
147
148 #[test]
149 fn test_rdkit_path_fp_propane() {
150 let m = mol("CCC");
151 let fp = rdkit_path_fp(&m);
152 assert!(fp.popcount() > 0, "propane should have non-zero bits");
153 }
154
155 #[test]
156 fn test_rdkit_path_fp_benzene() {
157 let m = mol("c1ccccc1");
158 let fp = rdkit_path_fp(&m);
159 assert!(fp.popcount() > 0, "benzene should have non-zero bits");
160 }
161
162 #[test]
163 fn test_rdkit_path_fp_identical() {
164 let m1 = mol("CC");
165 let m2 = mol("CC");
166 let fp1 = rdkit_path_fp(&m1);
167 let fp2 = rdkit_path_fp(&m2);
168 assert_eq!(
169 fp1.tanimoto(&fp2),
170 1.0,
171 "identical molecules should have tanimoto=1.0"
172 );
173 }
174
175 #[test]
176 fn test_rdkit_path_fp_single_atom() {
177 let m = mol("C");
178 let fp = rdkit_path_fp(&m);
179 assert_eq!(fp.popcount(), 0, "single atom should have zero bits");
181 }
182
183 #[test]
184 fn test_rdkit_path_fp_bond_type_distinction() {
185 let m_single = mol("CC");
187 let m_double = mol("C=C");
188 let fp_single = rdkit_path_fp(&m_single);
189 let fp_double = rdkit_path_fp(&m_double);
190 assert!(
192 tanimoto_rdkit_path(&fp_single, &fp_double) < 1.0,
193 "single and double bonds should produce different fingerprints"
194 );
195 }
196
197 #[test]
198 fn test_rdkit_path_fp_bond_type_consistency() {
199 let m1 = mol("C=C");
201 let m2 = mol("C=C");
202 let fp1 = rdkit_path_fp(&m1);
203 let fp2 = rdkit_path_fp(&m2);
204 assert_eq!(
205 tanimoto_rdkit_path(&fp1, &fp2),
206 1.0,
207 "identical bond types should produce identical fingerprints"
208 );
209 }
210
211 #[test]
212 fn test_rdkit_path_fp_triple_bond() {
213 let m = mol("C#C");
214 let fp = rdkit_path_fp(&m);
215 assert!(fp.popcount() > 0, "triple bond should have non-zero bits");
216 }
217
218 #[test]
219 fn test_rdkit_path_fp_aromatic_distinction() {
220 let m_aromatic = mol("c1ccccc1");
222 let m_aliphatic = mol("C1CCCC=CC=C1"); let fp_aromatic = rdkit_path_fp(&m_aromatic);
224 let fp_aliphatic = rdkit_path_fp(&m_aliphatic);
225 assert!(
227 tanimoto_rdkit_path(&fp_aromatic, &fp_aliphatic) < 1.0,
228 "aromatic and aliphatic should produce different fingerprints"
229 );
230 }
231}