1use crate::bitvec::BitVec2048;
7use chematic_core::{AtomIdx, BondOrder, Molecule};
8
9const HASH_MOD: usize = 2048;
10
11pub fn pattern_fp(mol: &Molecule) -> BitVec2048 {
16 let mut fp = BitVec2048::new();
17
18 if mol.atom_count() == 0 {
19 return fp;
20 }
21
22 for (idx, _atom) in mol.atoms() {
23 let pattern_hash = compute_pattern_hash(mol, idx);
25 let bit_idx = pattern_hash % HASH_MOD;
26 fp.set(bit_idx);
27 }
28
29 fp
30}
31
32fn compute_pattern_hash(mol: &Molecule, idx: AtomIdx) -> usize {
34 let fnv_prime: usize = 16777619;
35 let mut hash: usize = 2166136261; let atom = mol.atom(idx);
38 let an = atom.element.atomic_number() as usize;
39
40 hash ^= an;
42 hash = hash.wrapping_mul(fnv_prime);
43
44 let neighbors: Vec<_> = mol.neighbors(idx).collect();
46 let degree = neighbors.len();
47
48 hash ^= degree;
49 hash = hash.wrapping_mul(fnv_prime);
50
51 for (neighbor_idx, bond_idx) in neighbors {
53 let neighbor = mol.atom(neighbor_idx);
54 let neighbor_an = neighbor.element.atomic_number() as usize;
55 let bond = mol.bond(bond_idx);
56 let bond_order = match bond.order {
57 BondOrder::Single => 1,
58 BondOrder::Double => 2,
59 BondOrder::Triple => 3,
60 BondOrder::Aromatic => 4,
61 _ => 1,
62 };
63
64 hash ^= neighbor_an;
65 hash = hash.wrapping_mul(fnv_prime);
66 hash ^= bond_order;
67 hash = hash.wrapping_mul(fnv_prime);
68 }
69
70 if atom.aromatic {
72 hash ^= 1;
73 hash = hash.wrapping_mul(fnv_prime);
74 }
75
76 hash
77}
78
79pub fn tanimoto_pattern(a: &BitVec2048, b: &BitVec2048) -> f64 {
81 a.tanimoto(b)
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87 use chematic_smiles::parse;
88
89 fn mol(smiles: &str) -> Molecule {
90 parse(smiles).unwrap_or_else(|e| panic!("parse '{smiles}': {e}"))
91 }
92
93 #[test]
94 fn test_pattern_fp_ethane() {
95 let m = mol("CC");
96 let fp = pattern_fp(&m);
97 assert!(fp.popcount() > 0, "ethane should have non-zero bits");
98 }
99
100 #[test]
101 fn test_pattern_fp_benzene() {
102 let m = mol("c1ccccc1");
103 let fp = pattern_fp(&m);
104 assert!(fp.popcount() > 0, "benzene should have non-zero bits");
105 }
106
107 #[test]
108 fn test_pattern_fp_similarity() {
109 let m1 = mol("CC");
110 let m2 = mol("CC");
111 let fp1 = pattern_fp(&m1);
112 let fp2 = pattern_fp(&m2);
113 assert_eq!(
114 fp1.tanimoto(&fp2),
115 1.0,
116 "identical molecules should have tanimoto=1.0"
117 );
118 }
119
120 #[test]
121 fn test_pattern_fp_single_atom() {
122 let m = mol("C");
123 let fp = pattern_fp(&m);
124 assert!(fp.popcount() > 0, "single atom should have bits");
125 }
126}