Skip to main content

chematic_fp/
pattern.rs

1//! Pattern fingerprints — substructure feature hashing.
2//!
3//! Enumerates atom-centric patterns (neighborhood topology) and hashes them
4//! into a 2048-bit fingerprint for fast substructure-based screening.
5
6use crate::bitvec::BitVec2048;
7use chematic_core::{AtomIdx, BondOrder, Molecule};
8
9const HASH_MOD: usize = 2048;
10
11/// Compute pattern fingerprint (2048-bit).
12///
13/// Hashes atom-centric patterns: atomic number + neighbor count + bond types.
14/// Suitable for fast substructure screening and molecular similarity.
15pub 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        // Compute pattern hash for this atom's local neighborhood
24        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
32/// Compute hash for atom's pattern: atomic number + degree + neighbor types.
33fn compute_pattern_hash(mol: &Molecule, idx: AtomIdx) -> usize {
34    let fnv_prime: usize = 16777619;
35    let mut hash: usize = 2166136261; // FNV offset basis
36
37    let atom = mol.atom(idx);
38    let an = atom.element.atomic_number() as usize;
39
40    // Hash atomic number
41    hash ^= an;
42    hash = hash.wrapping_mul(fnv_prime);
43
44    // Hash neighbor information
45    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    // Hash neighbor atomic numbers and bond types
52    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    // Hash aromaticity
71    if atom.aromatic {
72        hash ^= 1;
73        hash = hash.wrapping_mul(fnv_prime);
74    }
75
76    hash
77}
78
79/// Tanimoto similarity between two pattern fingerprints.
80pub 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}