1use chematic_core::Molecule;
8
9use crate::bitvec::BitVec2048;
10use crate::ecfp::{EcfpConfig, ecfp};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub enum FpType {
19 #[default]
21 Ecfp4,
22 Ecfp6,
24 Ecfp4Chiral,
26 Fcfp4,
28 Maccs,
30 TopoPath,
32}
33
34fn compute_fp(mol: &Molecule, fp_type: FpType) -> BitVec2048 {
35 match fp_type {
36 FpType::Ecfp4 => ecfp(mol, &EcfpConfig::default()),
37 FpType::Ecfp6 => ecfp(
38 mol,
39 &EcfpConfig {
40 radius: 3,
41 nbits: 2048,
42 use_chirality: false,
43 use_double_fold: false,
44 },
45 ),
46 FpType::Ecfp4Chiral => ecfp(
47 mol,
48 &EcfpConfig {
49 radius: 2,
50 nbits: 2048,
51 use_chirality: true,
52 use_double_fold: false,
53 },
54 ),
55 FpType::Fcfp4 => crate::fcfp::fcfp4(mol),
56 FpType::Maccs => crate::maccs::maccs(mol),
57 FpType::TopoPath => {
58 crate::topo_path::topo_path(mol, &crate::topo_path::TopoPathConfig::default())
59 }
60 }
61}
62
63pub fn nearest_neighbors(
74 query: &Molecule,
75 db: &[Molecule],
76 k: usize,
77 fp_type: FpType,
78) -> Vec<(usize, f64)> {
79 if k == 0 || db.is_empty() {
80 return vec![];
81 }
82
83 let query_fp = compute_fp(query, fp_type);
84
85 let mut scores: Vec<(usize, f64)> = db
86 .iter()
87 .enumerate()
88 .map(|(i, mol)| {
89 let fp = compute_fp(mol, fp_type);
90 (i, query_fp.tanimoto(&fp))
91 })
92 .filter(|(_, t)| *t > 0.0)
93 .collect();
94
95 scores.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
96 scores.truncate(k);
97 scores
98}
99
100pub fn nearest_neighbors_from_fp(
105 query_fp: &BitVec2048,
106 db_fps: &[BitVec2048],
107 k: usize,
108) -> Vec<(usize, f64)> {
109 if k == 0 || db_fps.is_empty() {
110 return vec![];
111 }
112
113 let mut scores: Vec<(usize, f64)> = db_fps
114 .iter()
115 .enumerate()
116 .map(|(i, fp)| (i, query_fp.tanimoto(fp)))
117 .filter(|(_, t)| *t > 0.0)
118 .collect();
119
120 scores.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
121 scores.truncate(k);
122 scores
123}
124
125#[cfg(test)]
130mod tests {
131 use super::*;
132 use chematic_smiles::parse;
133
134 fn benzene() -> Molecule {
135 parse("c1ccccc1").unwrap()
136 }
137 fn toluene() -> Molecule {
138 parse("Cc1ccccc1").unwrap()
139 }
140 fn naphthalene() -> Molecule {
141 parse("c1ccc2ccccc2c1").unwrap()
142 }
143 fn ethane() -> Molecule {
144 parse("CC").unwrap()
145 }
146
147 #[test]
148 fn test_nn_self_is_first() {
149 let query = benzene();
150 let db = vec![ethane(), toluene(), benzene(), naphthalene()];
151 let results = nearest_neighbors(&query, &db, 3, FpType::Ecfp4);
152 assert!(!results.is_empty());
153 assert_eq!(results[0].0, 2, "benzene should match itself first");
155 assert!(
156 (results[0].1 - 1.0).abs() < 1e-9,
157 "self-similarity should be 1.0"
158 );
159 }
160
161 #[test]
162 fn test_nn_returns_k_results() {
163 let query = benzene();
164 let db = vec![ethane(), toluene(), benzene(), naphthalene()];
165 let results = nearest_neighbors(&query, &db, 2, FpType::Ecfp4);
166 assert!(results.len() <= 2, "should return at most k results");
167 }
168
169 #[test]
170 fn test_nn_sorted_descending() {
171 let query = benzene();
172 let db = vec![ethane(), toluene(), benzene(), naphthalene()];
173 let results = nearest_neighbors(&query, &db, 4, FpType::Ecfp4);
174 for w in results.windows(2) {
175 assert!(
176 w[0].1 >= w[1].1,
177 "results should be sorted by descending Tanimoto"
178 );
179 }
180 }
181
182 #[test]
183 fn test_nn_empty_db() {
184 let query = benzene();
185 let results = nearest_neighbors(&query, &[], 5, FpType::Ecfp4);
186 assert!(results.is_empty());
187 }
188
189 #[test]
190 fn test_nn_k_zero() {
191 let query = benzene();
192 let db = vec![benzene()];
193 let results = nearest_neighbors(&query, &db, 0, FpType::Ecfp4);
194 assert!(results.is_empty());
195 }
196
197 #[test]
198 fn test_nn_from_fp() {
199 let query = benzene();
200 let db = [ethane(), toluene(), benzene()];
201 let query_fp = crate::ecfp::ecfp4(&query);
202 let db_fps: Vec<_> = db.iter().map(crate::ecfp::ecfp4).collect();
203 let results = nearest_neighbors_from_fp(&query_fp, &db_fps, 3);
204 assert!(!results.is_empty());
205 assert_eq!(results[0].0, 2, "benzene fp should match itself");
206 }
207
208 #[test]
209 fn test_nn_maccs_type() {
210 let query = benzene();
214 let db = vec![toluene(), benzene(), ethane()];
215 let results = nearest_neighbors(&query, &db, 3, FpType::Maccs);
216 assert!(!results.is_empty());
217 assert_ne!(
219 results[0].0, 2,
220 "ethane should not be the top MACCS hit for benzene"
221 );
222 assert!(
224 results[0].1 > 0.5,
225 "top MACCS hit should have Tanimoto > 0.5"
226 );
227 }
228}