#![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" => {
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 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);
}
}