use std::collections::HashMap;
use crate::{
match_vf2::{find_matches_with_config, MatchConfig},
parser::{SmartsError, parse_smarts},
query::QueryMolecule,
};
use chematic_core::{AtomIdx, Molecule};
pub struct SmartsCache {
capacity: usize,
store: HashMap<String, QueryMolecule>,
order: Vec<String>, }
impl SmartsCache {
pub fn new(capacity: usize) -> Self {
Self {
capacity: capacity.max(1),
store: HashMap::new(),
order: Vec::new(),
}
}
pub fn compile(&mut self, smarts: &str) -> Result<&QueryMolecule, SmartsError> {
if !self.store.contains_key(smarts) {
let qmol = parse_smarts(smarts)?;
if self.store.len() >= self.capacity {
if let Some(oldest) = self.order.first().cloned() {
self.store.remove(&oldest);
self.order.remove(0);
}
}
self.store.insert(smarts.to_string(), qmol);
self.order.push(smarts.to_string());
} else {
if let Some(pos) = self.order.iter().position(|s| s == smarts) {
let key = self.order.remove(pos);
self.order.push(key);
}
}
Ok(self.store.get(smarts).unwrap())
}
pub fn find_matches(
&mut self,
smarts: &str,
mol: &Molecule,
) -> Result<Vec<std::collections::HashMap<usize, AtomIdx>>, SmartsError> {
let qmol = self.compile(smarts)?.clone();
Ok(find_matches_with_config(&qmol, mol, &MatchConfig::default()))
}
pub fn find_matches_with_config(
&mut self,
smarts: &str,
mol: &Molecule,
config: &MatchConfig,
) -> Result<Vec<std::collections::HashMap<usize, AtomIdx>>, SmartsError> {
let qmol = self.compile(smarts)?.clone();
Ok(find_matches_with_config(&qmol, mol, config))
}
pub fn has_match(&mut self, smarts: &str, mol: &Molecule) -> Result<bool, SmartsError> {
let matches = self.find_matches(smarts, mol)?;
Ok(!matches.is_empty())
}
pub fn len(&self) -> usize {
self.store.len()
}
pub fn is_empty(&self) -> bool {
self.store.is_empty()
}
pub fn clear(&mut self) {
self.store.clear();
self.order.clear();
}
}
pub fn named_pattern(name: &str) -> Option<&'static str> {
match name {
"donor" => Some("[N;!H0;v3,v4&+1]"),
"donor_strict" => Some("[O,N;!H0]"),
"acceptor" => Some("[N;H0;v3,v4&+1]"),
"acceptor_strict"=> Some("[n;H0;+0,o,s;+0]"),
"aromatic" => Some("[a]"),
"aromatic_ring" => Some("[a]1:[a]:[a]:[a]:[a]:[a]:1"),
"hydrophobic" => Some("[c,s,S&H0&v2,F,Cl,Br,I,#6&!$([#6]~[#7,#8,#15,#16,F,Cl,Br,I])]"),
"positive" => Some("[#7&+,NH2&+0&$(N-[#6]),NH1&+0&$(N(-[#6])-[#6])]"),
"negative" => Some("[C,S](=[O,S,P])-[OH,O-]"),
"carboxylic_acid"=> Some("[C;X3;$(C(-O)-O)](=O)"),
"aldehyde" => Some("[CX3H1](=O)[#6]"),
"ketone" => Some("[#6][CX3](=O)[#6]"),
"alcohol" => Some("[OX2H][CX4;!$(C([OX2H])[O,S,#7,#15])]"),
"phenol" => Some("[OX2H][cX3]:[c]"),
"amine_primary" => Some("[NH2;$(N-[#6])]"),
"amine_secondary"=> Some("[NH1;$(N(-[#6])-[#6])]"),
"amine_tertiary" => Some("[NH0;$(N(-[#6])(-[#6])-[#6])]"),
"amide" => Some("[NX3][CX3](=[OX1])[#6]"),
"ester" => Some("[#6][CX3](=O)[OX2H0][#6]"),
"ether" => Some("[OD2]([#6])[#6]"),
"halide" => Some("[F,Cl,Br,I]"),
"aromatic_n" => Some("[n]"),
"sulfonamide" => Some("[$([#16X4](=[OX1])(=[OX1])([#6])[NX3]),$([#16X4+2]([OX1-])([OX1-])([#6])[NX3])]"),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
#[test]
fn cache_compiles_once() {
let mut cache = SmartsCache::new(10);
let mol = parse("CCO").expect("parse");
let m1 = cache.find_matches("[OH]", &mol).expect("m1");
let m2 = cache.find_matches("[OH]", &mol).expect("m2");
assert_eq!(m1.len(), m2.len());
assert_eq!(cache.len(), 1);
}
#[test]
fn cache_evicts_lru() {
let mut cache = SmartsCache::new(2);
let mol = parse("C").expect("parse");
cache.find_matches("[C]", &mol).unwrap();
cache.find_matches("[#6]", &mol).unwrap();
assert_eq!(cache.len(), 2);
cache.find_matches("[H]", &mol).unwrap();
assert_eq!(cache.len(), 2);
assert!(!cache.store.contains_key("[C]"));
}
#[test]
fn named_pattern_donor_matches_alcohol() {
let mol = parse("CCO").expect("parse ethanol");
let smarts = named_pattern("donor_strict").expect("pattern");
let mut cache = SmartsCache::new(50);
let matches = cache.find_matches(smarts, &mol).expect("matches");
assert!(!matches.is_empty(), "ethanol O-H should match donor");
}
#[test]
fn named_patterns_all_parse() {
let mut cache = SmartsCache::new(50);
for name in &["donor","acceptor","aromatic","hydrophobic","positive","negative",
"carboxylic_acid","aldehyde","ketone","alcohol","phenol",
"amine_primary","amine_secondary","amide","ester","halide"] {
if let Some(pat) = named_pattern(name) {
cache.compile(pat).unwrap_or_else(|e| panic!("pattern '{}' failed: {}", name, e));
}
}
}
#[test]
fn has_match_works() {
let mut cache = SmartsCache::new(10);
let mol = parse("c1ccccc1").expect("benzene");
assert!(cache.has_match("[a]", &mol).unwrap());
assert!(!cache.has_match("[OH]", &mol).unwrap());
}
}