use crate::bitvec::BitVec2048;
use crate::ecfp::ecfp4;
#[derive(Clone, Debug)]
pub struct ReactionFpConfig {
pub use_xor: bool,
}
impl Default for ReactionFpConfig {
fn default() -> Self {
ReactionFpConfig {
use_xor: true,
}
}
}
#[derive(Clone, Debug)]
pub struct ReactionFingerprint {
pub reactant_fp: BitVec2048,
pub product_fp: BitVec2048,
pub combined_fp: BitVec2048,
}
impl ReactionFingerprint {
pub fn tanimoto(&self, other: &ReactionFingerprint) -> f64 {
self.combined_fp.tanimoto(&other.combined_fp)
}
}
fn combine_fps_or(fps: &[BitVec2048]) -> BitVec2048 {
if fps.is_empty() {
return BitVec2048::new();
}
let mut result = fps[0].clone();
for fp in &fps[1..] {
result = result.or(fp);
}
result
}
fn compute_structural_difference(reactant_fp: &BitVec2048, product_fp: &BitVec2048) -> BitVec2048 {
let mut result = BitVec2048::new();
for i in 0..2048 {
let r_bit = reactant_fp.get(i);
let p_bit = product_fp.get(i);
if r_bit != p_bit {
result.set(i);
}
}
result
}
pub fn reaction_fp(rxn: &chematic_rxn::Reaction) -> ReactionFingerprint {
reaction_fp_with_config(rxn, &ReactionFpConfig::default())
}
pub fn reaction_fp_with_config(
rxn: &chematic_rxn::Reaction,
config: &ReactionFpConfig,
) -> ReactionFingerprint {
let reactant_fp = combine_fps_or(
&rxn.reactants.iter().map(ecfp4).collect::<Vec<_>>(),
);
let product_fp = combine_fps_or(
&rxn.products.iter().map(ecfp4).collect::<Vec<_>>(),
);
let combined_fp = if config.use_xor {
compute_structural_difference(&reactant_fp, &product_fp)
} else {
reactant_fp.or(&product_fp)
};
ReactionFingerprint { reactant_fp, product_fp, combined_fp }
}
pub fn reaction_fp_ecfp4(rxn: &chematic_rxn::Reaction) -> ReactionFingerprint {
reaction_fp(rxn)
}
pub fn tanimoto_reaction_fp(rxn1: &chematic_rxn::Reaction, rxn2: &chematic_rxn::Reaction) -> f64 {
let fp1 = reaction_fp(rxn1);
let fp2 = reaction_fp(rxn2);
fp1.tanimoto(&fp2)
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_reaction(smiles: &str) -> chematic_rxn::Reaction {
chematic_rxn::reaction::parse_reaction(smiles).unwrap()
}
#[test]
fn test_reaction_fp_simple() {
let rxn = create_test_reaction("CC>>C");
let fp = reaction_fp(&rxn);
assert!(fp.combined_fp.popcount() > 0);
assert!(fp.reactant_fp.popcount() > 0);
assert!(fp.product_fp.popcount() > 0);
}
#[test]
fn test_reaction_fp_identical() {
let rxn = create_test_reaction("CC>>C");
let fp1 = reaction_fp(&rxn);
let fp2 = reaction_fp(&rxn);
assert!((fp1.tanimoto(&fp2) - 1.0).abs() < 1e-6);
}
#[test]
fn test_reaction_fp_different_products() {
let rxn1 = create_test_reaction("CC>>C");
let rxn2 = create_test_reaction("CC>>CCC");
let fp1 = reaction_fp(&rxn1);
let fp2 = reaction_fp(&rxn2);
let similarity = fp1.tanimoto(&fp2);
assert!(similarity < 1.0);
assert!(similarity > 0.0);
}
#[test]
fn test_reaction_fp_different_reactants() {
let rxn1 = create_test_reaction("C>>CC");
let rxn2 = create_test_reaction("CC>>CCC");
let fp1 = reaction_fp(&rxn1);
let fp2 = reaction_fp(&rxn2);
let similarity = fp1.tanimoto(&fp2);
assert!(similarity < 1.0);
}
#[test]
fn test_reaction_fp_multi_molecule() {
let rxn = create_test_reaction("C.C>>CC");
let fp = reaction_fp(&rxn);
assert!(fp.combined_fp.popcount() > 0);
}
#[test]
fn test_reaction_tanimoto_symmetry() {
let rxn1 = create_test_reaction("CC>>C");
let rxn2 = create_test_reaction("CCC>>CC");
let sim12 = tanimoto_reaction_fp(&rxn1, &rxn2);
let sim21 = tanimoto_reaction_fp(&rxn2, &rxn1);
assert!((sim12 - sim21).abs() < 1e-6);
}
#[test]
fn test_reaction_fp_bounds() {
let rxn1 = create_test_reaction("CC>>C");
let rxn2 = create_test_reaction("CCCC>>CCC");
let fp1 = reaction_fp(&rxn1);
let fp2 = reaction_fp(&rxn2);
let similarity = fp1.tanimoto(&fp2);
assert!((0.0..=1.0).contains(&similarity));
}
#[test]
fn test_reaction_fp_config() {
let rxn = create_test_reaction("CC>>C");
let config = ReactionFpConfig {
use_xor: true,
};
let fp = reaction_fp_with_config(&rxn, &config);
assert!(fp.combined_fp.popcount() > 0);
}
#[test]
fn test_reaction_fp_structural_difference() {
let rxn = create_test_reaction("C.C>>CC");
let fp = reaction_fp(&rxn);
assert!(fp.combined_fp.popcount() > 0);
}
#[test]
fn test_reaction_fp_transformation_vs_composition() {
let rxn1 = create_test_reaction("CC>>C"); let rxn2 = create_test_reaction("C>>CC");
let fp1 = reaction_fp(&rxn1);
let fp2 = reaction_fp(&rxn2);
let similarity = fp1.tanimoto(&fp2);
assert!((0.0..=1.0).contains(&similarity));
}
}