#![forbid(unsafe_code)]
use std::collections::BTreeMap;
use chematic::core::Element;
use crate::chem_env::mol_from_smiles;
use super::StepValidationStatus;
type ElementDelta = &'static [(Element, i64)];
const ESTER_AMIDE_DELTA: ElementDelta = &[(Element::H, 2), (Element::O, 1)]; const SUZUKI_DELTA: ElementDelta = &[(Element::H, 1), (Element::BR, 1)]; const SULFONYL_DELTA: ElementDelta = &[(Element::H, 1), (Element::CL, 1)]; const BOC_DELTA: ElementDelta = &[(Element::C, -5), (Element::H, -8), (Element::O, -2)]; const CBZ_DELTA: ElementDelta = &[(Element::C, -8), (Element::H, -6), (Element::O, -2)];
fn element_counts(smiles: &[&str]) -> Option<BTreeMap<Element, i64>> {
let mut counts: BTreeMap<Element, i64> = BTreeMap::new();
for s in smiles {
let mol = mol_from_smiles(s).ok()?;
for (_, atom) in mol.atoms() {
*counts.entry(atom.element).or_insert(0) += 1;
}
for h in chematic::chem::implicit_hcount_per_atom(&mol) {
if h > 0 {
*counts.entry(Element::H).or_insert(0) += h as i64;
}
}
}
Some(counts)
}
fn delta_matches(
target_counts: &BTreeMap<Element, i64>,
precursor_counts: &BTreeMap<Element, i64>,
delta: ElementDelta,
) -> bool {
let mut all_elements: std::collections::BTreeSet<Element> = target_counts
.keys()
.chain(precursor_counts.keys())
.copied()
.collect();
all_elements.extend(delta.iter().map(|(e, _)| *e));
all_elements.iter().all(|e| {
let t = *target_counts.get(e).unwrap_or(&0);
let p = *precursor_counts.get(e).unwrap_or(&0);
let expected = delta.iter().find(|(de, _)| de == e).map_or(0, |(_, d)| *d);
p - t == expected
})
}
fn validate_delta(
target: &str,
precursors: &[String],
delta: ElementDelta,
) -> StepValidationStatus {
let precursor_refs: Vec<&str> = precursors.iter().map(String::as_str).collect();
let (Some(target_counts), Some(precursor_counts)) =
(element_counts(&[target]), element_counts(&precursor_refs))
else {
return StepValidationStatus::NotEvaluable;
};
if delta_matches(&target_counts, &precursor_counts, delta) {
StepValidationStatus::Valid
} else {
StepValidationStatus::Invalid
}
}
pub fn validate_graph_step(
rule_name: &str,
target: &str,
precursors: &[String],
) -> StepValidationStatus {
match rule_name {
"ester_cleavage" | "amide_cleavage" | "aryl_ether_retro" => {
validate_delta(target, precursors, ESTER_AMIDE_DELTA)
}
"suzuki_retro" => validate_delta(target, precursors, SUZUKI_DELTA),
"sulfonamide_retro" | "diaryl_sulfone_retro" => {
validate_delta(target, precursors, SULFONYL_DELTA)
}
"boc_deprotection_retro" => validate_delta(target, precursors, BOC_DELTA),
"cbz_deprotection_retro" => validate_delta(target, precursors, CBZ_DELTA),
_ => StepValidationStatus::NotEvaluable,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn precs(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn ester_cleavage_valid() {
let status = validate_graph_step(
"ester_cleavage",
"CC(=O)Oc1ccccc1",
&precs(&["CC(=O)O", "Oc1ccccc1"]),
);
assert_eq!(status, StepValidationStatus::Valid);
}
#[test]
fn ester_cleavage_invalid_wrong_precursors() {
let status = validate_graph_step("ester_cleavage", "CC(=O)Oc1ccccc1", &precs(&["CCO"]));
assert_eq!(status, StepValidationStatus::Invalid);
}
#[test]
fn amide_cleavage_valid() {
let status = validate_graph_step(
"amide_cleavage",
"CC(=O)Nc1ccccc1",
&precs(&["CC(=O)O", "Nc1ccccc1"]),
);
assert_eq!(status, StepValidationStatus::Valid);
}
#[test]
fn amide_cleavage_invalid_missing_precursor() {
let status = validate_graph_step("amide_cleavage", "CC(=O)Nc1ccccc1", &precs(&["CC(=O)O"]));
assert_eq!(status, StepValidationStatus::Invalid);
}
#[test]
fn aryl_ether_retro_valid() {
let status = validate_graph_step(
"aryl_ether_retro",
"c1ccc(Oc2ccccc2)cc1",
&precs(&["Oc1ccccc1", "Oc1ccccc1"]),
);
assert_eq!(status, StepValidationStatus::Valid);
}
#[test]
fn aryl_ether_retro_invalid_missing_precursor() {
let status = validate_graph_step(
"aryl_ether_retro",
"c1ccc(Oc2ccccc2)cc1",
&precs(&["Oc1ccccc1"]),
);
assert_eq!(status, StepValidationStatus::Invalid);
}
#[test]
fn suzuki_retro_valid() {
let status = validate_graph_step(
"suzuki_retro",
"c1ccc(-c2ccccc2)cc1",
&precs(&["Brc1ccccc1", "c1ccccc1"]),
);
assert_eq!(status, StepValidationStatus::Valid);
}
#[test]
fn suzuki_retro_invalid_wrong_halide() {
let status = validate_graph_step(
"suzuki_retro",
"c1ccc(-c2ccccc2)cc1",
&precs(&["Clc1ccccc1", "c1ccccc1"]),
);
assert_eq!(status, StepValidationStatus::Invalid);
}
#[test]
fn sulfonamide_retro_valid() {
let status = validate_graph_step(
"sulfonamide_retro",
"O=S(=O)(Nc1ccccc1)c1ccccc1",
&precs(&["O=S(=O)(Cl)c1ccccc1", "Nc1ccccc1"]),
);
assert_eq!(status, StepValidationStatus::Valid);
}
#[test]
fn sulfonamide_retro_invalid() {
let status = validate_graph_step(
"sulfonamide_retro",
"O=S(=O)(Nc1ccccc1)c1ccccc1",
&precs(&["Nc1ccccc1"]),
);
assert_eq!(status, StepValidationStatus::Invalid);
}
#[test]
fn diaryl_sulfone_retro_valid() {
let status = validate_graph_step(
"diaryl_sulfone_retro",
"O=S(=O)(c1ccccc1)c1ccccc1",
&precs(&["c1ccccc1", "O=S(=O)(Cl)c1ccccc1"]),
);
assert_eq!(status, StepValidationStatus::Valid);
}
#[test]
fn diaryl_sulfone_retro_invalid() {
let status = validate_graph_step(
"diaryl_sulfone_retro",
"O=S(=O)(c1ccccc1)c1ccccc1",
&precs(&["c1ccccc1"]),
);
assert_eq!(status, StepValidationStatus::Invalid);
}
#[test]
fn boc_deprotection_valid() {
let status = validate_graph_step(
"boc_deprotection_retro",
"CC(C)(C)OC(=O)N1CCCCC1",
&precs(&["C1CCNCC1"]),
);
assert_eq!(status, StepValidationStatus::Valid);
}
#[test]
fn boc_deprotection_invalid_wrong_amine() {
let status = validate_graph_step(
"boc_deprotection_retro",
"CC(C)(C)OC(=O)N1CCCCC1",
&precs(&["C1CCNC1"]),
);
assert_eq!(status, StepValidationStatus::Invalid);
}
#[test]
fn cbz_deprotection_valid() {
let status = validate_graph_step(
"cbz_deprotection_retro",
"O=C(OCc1ccccc1)N1CCCCC1",
&precs(&["C1CCNCC1"]),
);
assert_eq!(status, StepValidationStatus::Valid);
}
#[test]
fn cbz_deprotection_invalid_wrong_amine() {
let status = validate_graph_step(
"cbz_deprotection_retro",
"O=C(OCc1ccccc1)N1CCCCC1",
&precs(&["C1CCNC1"]),
);
assert_eq!(status, StepValidationStatus::Invalid);
}
#[test]
fn unknown_graph_rule_not_evaluable() {
let status = validate_graph_step("some_future_graph_rule", "C", &precs(&["C"]));
assert_eq!(status, StepValidationStatus::NotEvaluable);
}
#[test]
fn unparseable_smiles_not_evaluable() {
let status = validate_graph_step("ester_cleavage", "", &precs(&["CCO"]));
assert_eq!(status, StepValidationStatus::NotEvaluable);
}
#[test]
fn every_graph_based_default_rule_has_a_validate_graph_step_case() {
let covered: std::collections::BTreeSet<&str> = [
"ester_cleavage",
"amide_cleavage",
"aryl_ether_retro",
"suzuki_retro",
"sulfonamide_retro",
"diaryl_sulfone_retro",
"boc_deprotection_retro",
"cbz_deprotection_retro",
]
.into_iter()
.collect();
let graph_based: std::collections::BTreeSet<String> = crate::chem_env::default_rules()
.iter()
.filter(|r| r.smirks.is_empty())
.map(|r| r.name.clone())
.collect();
let covered_owned: std::collections::BTreeSet<String> =
covered.iter().map(|s| s.to_string()).collect();
assert_eq!(
graph_based, covered_owned,
"default_rules()'s graph-based rule set and validate_graph_step's \
covered names have drifted apart -- a rule on either side with no \
match on the other silently degrades to NotEvaluable everywhere \
this validator is used (examples/inspect_validation, \
src/bin/benchmark.rs, synthesizability::assessment)"
);
}
}