use crate::chem_env::{self, RetroRule};
use crate::evidence::canonical_set;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TemplateMatchStatus {
Unique,
Ambiguous,
NoMatch,
InvalidInput,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateMatchResult {
pub target_smiles: String,
pub precursor_smiles: Vec<String>,
pub matching_template_ids: Vec<String>,
pub status: TemplateMatchStatus,
}
pub fn match_reaction_to_templates(
target_smiles: &str,
precursor_smiles: &[String],
rules: &[RetroRule],
) -> TemplateMatchResult {
let invalid = |target_smiles: String| TemplateMatchResult {
target_smiles,
precursor_smiles: precursor_smiles.to_vec(),
matching_template_ids: Vec::new(),
status: TemplateMatchStatus::InvalidInput,
};
if precursor_smiles.is_empty() {
return invalid(target_smiles.to_string());
}
let target_mol = match chem_env::mol_from_smiles(target_smiles) {
Ok(m) => m,
Err(_) => return invalid(target_smiles.to_string()),
};
let canonical_target = chem_env::to_canonical(&target_mol);
let input_precursor_set = match canonical_set(precursor_smiles) {
Some(set) => set,
None => return invalid(canonical_target),
};
let mut matching_template_ids: Vec<String> = rules
.iter()
.filter(|rule| {
chem_env::apply_retro(&target_mol, rule)
.into_iter()
.any(|site| {
let mut set: Vec<String> = site.into_iter().map(|p| p.smiles).collect();
set.sort();
set.dedup();
set == input_precursor_set
})
})
.map(|rule| rule.template_id.clone())
.collect();
matching_template_ids.sort();
matching_template_ids.dedup();
let status = match matching_template_ids.len() {
0 => TemplateMatchStatus::NoMatch,
1 => TemplateMatchStatus::Unique,
_ => TemplateMatchStatus::Ambiguous,
};
TemplateMatchResult {
target_smiles: canonical_target,
precursor_smiles: input_precursor_set,
matching_template_ids,
status,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chem_env;
fn rules() -> Vec<RetroRule> {
chem_env::default_rules()
}
fn s(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn unique_match_regardless_of_precursor_order() {
let a = match_reaction_to_templates("CC(=O)OCC", &s(&["CC(=O)O", "CCO"]), &rules());
let b = match_reaction_to_templates("CC(=O)OCC", &s(&["CCO", "CC(=O)O"]), &rules());
assert_eq!(a.status, TemplateMatchStatus::Unique);
assert_eq!(a.status, b.status);
assert_eq!(a.matching_template_ids, b.matching_template_ids);
assert_eq!(a.matching_template_ids, vec!["rule:ester_cleavage"]);
}
#[test]
fn different_target_gives_no_match() {
let result = match_reaction_to_templates("CCCCCC", &s(&["CCCCCC"]), &rules());
assert_eq!(result.status, TemplateMatchStatus::NoMatch);
assert!(result.matching_template_ids.is_empty());
}
#[test]
fn different_precursors_give_no_match() {
let result =
match_reaction_to_templates("CC(=O)OCC", &s(&["CC(=O)O", "CCCCCCCC"]), &rules());
assert_eq!(result.status, TemplateMatchStatus::NoMatch);
}
#[test]
fn ambiguous_when_multiple_rules_produce_same_precursor_set() {
let mut two_identical = rules();
let ester = two_identical
.iter()
.find(|r| r.template_id == "rule:ester_cleavage")
.cloned()
.expect("ester_cleavage present in default rules");
let mut renamed = ester.clone();
renamed.template_id = "rule:ester_cleavage_dup".to_string();
two_identical.push(renamed);
let result =
match_reaction_to_templates("CC(=O)OCC", &s(&["CC(=O)O", "CCO"]), &two_identical);
assert_eq!(result.status, TemplateMatchStatus::Ambiguous);
assert_eq!(
result.matching_template_ids,
vec!["rule:ester_cleavage", "rule:ester_cleavage_dup"]
);
}
#[test]
fn duplicate_rule_entries_with_same_template_id_do_not_fake_ambiguity() {
let mut doubled = rules();
let ester = doubled
.iter()
.find(|r| r.template_id == "rule:ester_cleavage")
.cloned()
.expect("ester_cleavage present in default rules");
doubled.push(ester);
let result = match_reaction_to_templates("CC(=O)OCC", &s(&["CC(=O)O", "CCO"]), &doubled);
assert_eq!(result.status, TemplateMatchStatus::Unique);
assert_eq!(result.matching_template_ids, vec!["rule:ester_cleavage"]);
}
#[test]
fn malformed_target_is_invalid_input() {
let result = match_reaction_to_templates("not(a smiles", &s(&["CCO"]), &rules());
assert_eq!(result.status, TemplateMatchStatus::InvalidInput);
assert!(result.matching_template_ids.is_empty());
assert_eq!(result.target_smiles, "not(a smiles");
}
#[test]
fn malformed_precursor_is_invalid_input() {
let result =
match_reaction_to_templates("CC(=O)OCC", &s(&["CC(=O)O", "not(a smiles"]), &rules());
assert_eq!(result.status, TemplateMatchStatus::InvalidInput);
assert!(result.matching_template_ids.is_empty());
}
#[test]
fn empty_precursor_list_is_invalid_input() {
let result = match_reaction_to_templates("CC(=O)OCC", &[], &rules());
assert_eq!(result.status, TemplateMatchStatus::InvalidInput);
}
#[test]
fn matching_template_ids_are_sorted() {
let mut two_identical = rules();
let ester = two_identical
.iter()
.find(|r| r.template_id == "rule:ester_cleavage")
.cloned()
.expect("ester_cleavage present in default rules");
let mut renamed = ester;
renamed.template_id = "rule:aaa_before_ester".to_string();
two_identical.push(renamed);
let result =
match_reaction_to_templates("CC(=O)OCC", &s(&["CC(=O)O", "CCO"]), &two_identical);
assert_eq!(
result.matching_template_ids,
vec!["rule:aaa_before_ester", "rule:ester_cleavage"]
);
}
#[test]
fn stable_template_id_preserved_for_hand_crafted_and_extracted_rules() {
let friedel_crafts = rules()
.into_iter()
.find(|r| r.template_id == "rule:friedel_crafts_acylation_retro")
.expect("friedel_crafts_acylation_retro present in default rules");
let extracted = RetroRule {
name: friedel_crafts.name.clone(),
template_id: chem_env::template_id_for_smirks(&friedel_crafts.smirks),
smirks: friedel_crafts.smirks.clone(),
weight: 1.0,
required_elements: friedel_crafts.required_elements,
};
let mut rule_set = vec![extracted.clone()];
rule_set.extend(rules());
let result =
match_reaction_to_templates("CC(=O)c1ccccc1", &s(&["c1ccccc1", "CC(=O)Cl"]), &rule_set);
assert_eq!(result.status, TemplateMatchStatus::Ambiguous);
assert!(
result
.matching_template_ids
.contains(&extracted.template_id)
);
assert!(
result
.matching_template_ids
.contains(&"rule:friedel_crafts_acylation_retro".to_string())
);
}
}