#![forbid(unsafe_code)]
pub mod atom_conservation;
pub mod forward;
pub mod graph_rules;
use serde::Serialize;
use crate::chem_env::RetroRule;
use crate::search::ReactionStep;
pub use atom_conservation::{route_balanced, step_balanced};
pub use forward::route_forward_validated;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum StepValidationStatus {
Valid,
Invalid,
NotEvaluable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RouteValidationStatus {
Validated,
Invalid,
PartiallyValidated,
NotEvaluable,
}
pub fn validate_step(step: &ReactionStep, rules: &[RetroRule]) -> StepValidationStatus {
match rules.iter().find(|r| r.name == step.rule) {
Some(r) if r.smirks.is_empty() => {
graph_rules::validate_graph_step(&step.rule, &step.target, &step.precursors)
}
Some(r) => {
if forward::rule_reproduces(&step.target, &step.precursors, r) {
StepValidationStatus::Valid
} else {
StepValidationStatus::Invalid
}
}
None => StepValidationStatus::NotEvaluable,
}
}
pub fn aggregate_route(statuses: &[StepValidationStatus]) -> RouteValidationStatus {
if statuses.contains(&StepValidationStatus::Invalid) {
return RouteValidationStatus::Invalid;
}
if statuses.iter().all(|s| *s == StepValidationStatus::Valid) {
return RouteValidationStatus::Validated;
}
if statuses
.iter()
.all(|s| *s == StepValidationStatus::NotEvaluable)
{
return RouteValidationStatus::NotEvaluable;
}
RouteValidationStatus::PartiallyValidated
}
pub fn validate_route_steps(
steps: &[ReactionStep],
rules: &[RetroRule],
) -> (Vec<StepValidationStatus>, RouteValidationStatus) {
let statuses: Vec<StepValidationStatus> =
steps.iter().map(|s| validate_step(s, rules)).collect();
let route_status = aggregate_route(&statuses);
(statuses, route_status)
}
#[cfg(test)]
mod tests {
use super::*;
use StepValidationStatus::{Invalid, NotEvaluable, Valid};
#[test]
fn aggregate_all_valid_is_validated() {
assert_eq!(
aggregate_route(&[Valid, Valid]),
RouteValidationStatus::Validated
);
}
#[test]
fn aggregate_one_invalid_dominates() {
assert_eq!(
aggregate_route(&[Valid, Invalid, Valid]),
RouteValidationStatus::Invalid
);
}
#[test]
fn aggregate_all_not_evaluable() {
assert_eq!(
aggregate_route(&[NotEvaluable, NotEvaluable]),
RouteValidationStatus::NotEvaluable
);
}
#[test]
fn aggregate_valid_and_not_evaluable_mix_is_partial() {
assert_eq!(
aggregate_route(&[Valid, NotEvaluable]),
RouteValidationStatus::PartiallyValidated
);
}
#[test]
fn aggregate_empty_is_validated() {
assert_eq!(aggregate_route(&[]), RouteValidationStatus::Validated);
}
fn step(rule: &str, target: &str, precursors: &[&str]) -> ReactionStep {
ReactionStep {
rule: rule.to_string(),
target: target.to_string(),
precursors: precursors.iter().map(|s| s.to_string()).collect(),
conditions: None,
atom_economy: None,
step_confidence: 1.0,
procedure_hint: None,
reaction_family: None,
metadata_source: None,
metadata_scope: None,
}
}
#[test]
fn smirks_rule_step_valid_on_forward_match() {
let rules = crate::chem_env::default_rules();
let s = step(
"friedel_crafts_acylation_retro",
"CC(=O)c1ccccc1",
&["c1ccccc1", "CC(=O)Cl"],
);
assert_eq!(validate_step(&s, &rules), Valid);
}
#[test]
fn smirks_rule_step_invalid_on_forward_mismatch() {
let rules = crate::chem_env::default_rules();
let s = step("friedel_crafts_acylation_retro", "CC(=O)c1ccccc1", &["CCO"]);
assert_eq!(validate_step(&s, &rules), Invalid);
}
#[test]
fn graph_rule_step_valid() {
let rules = crate::chem_env::default_rules();
let s = step(
"ester_cleavage",
"CC(=O)Oc1ccccc1",
&["CC(=O)O", "Oc1ccccc1"],
);
assert_eq!(validate_step(&s, &rules), Valid);
}
#[test]
fn graph_rule_step_invalid() {
let rules = crate::chem_env::default_rules();
let s = step("ester_cleavage", "CC(=O)Oc1ccccc1", &["CCO"]);
assert_eq!(validate_step(&s, &rules), Invalid);
}
#[test]
fn unmatched_rule_name_not_evaluable() {
let rules = crate::chem_env::default_rules();
let s = step("some_extracted_template_no_name_match", "CCO", &["CC", "O"]);
assert_eq!(validate_step(&s, &rules), NotEvaluable);
}
#[test]
fn cross_rule_corroboration_does_not_upgrade_to_valid() {
let rules = vec![
RetroRule {
name: "aryl_chloride_retro".to_string(),
smirks: "[c:1][Cl]>>[c:1]".to_string(),
..Default::default()
},
RetroRule {
name: "unrelated_bromide_to_chloride_swap".to_string(),
smirks: "[c:1]Cl>>[c:1][Br]".to_string(),
..Default::default()
},
];
let s = step("aryl_chloride_retro", "Clc1ccccc1", &["Brc1ccccc1"]);
assert!(
!forward::rule_reproduces("Clc1ccccc1", &["Brc1ccccc1".to_string()], &rules[0]),
"sanity check failed: aryl_chloride_retro's own reversal unexpectedly reproduces the target"
);
assert!(
forward::smirks_reproduces("Clc1ccccc1", &["Brc1ccccc1".to_string()], &rules),
"sanity check failed: fixture no longer exercises a cross-rule coincidental match"
);
assert_eq!(validate_step(&s, &rules), Invalid);
}
#[test]
fn route_with_one_invalid_step_is_invalid_end_to_end() {
let rules = crate::chem_env::default_rules();
let valid_step = step(
"friedel_crafts_acylation_retro",
"CC(=O)c1ccccc1",
&["c1ccccc1", "CC(=O)Cl"],
);
let invalid_step = step("friedel_crafts_acylation_retro", "CC(=O)c1ccccc1", &["CCO"]);
let (_, route_status) = validate_route_steps(&[valid_step, invalid_step], &rules);
assert_eq!(route_status, RouteValidationStatus::Invalid);
}
#[test]
fn route_with_valid_and_not_evaluable_is_partially_validated_end_to_end() {
let rules = crate::chem_env::default_rules();
let valid_step = step(
"friedel_crafts_acylation_retro",
"CC(=O)c1ccccc1",
&["c1ccccc1", "CC(=O)Cl"],
);
let not_evaluable_step = step("no_such_rule", "CCO", &["CC", "O"]);
let (_, route_status) = validate_route_steps(&[valid_step, not_evaluable_step], &rules);
assert_eq!(route_status, RouteValidationStatus::PartiallyValidated);
}
}