use std::collections::BTreeMap;
use yomitoki::{AnalysisConfig, FindingCode, SuggestionCode, SynthesizabilityReport};
fn finding_code_multiset(report: &SynthesizabilityReport) -> BTreeMap<FindingCode, usize> {
let mut counts = BTreeMap::new();
for finding in &report.findings {
*counts.entry(finding.code).or_insert(0) += 1;
}
counts
}
fn suggestion_code_multiset(report: &SynthesizabilityReport) -> BTreeMap<SuggestionCode, usize> {
let mut counts = BTreeMap::new();
for suggestion in &report.suggestions {
*counts.entry(suggestion.code).or_insert(0) += 1;
}
counts
}
fn explanation_multiset(report: &SynthesizabilityReport) -> BTreeMap<String, usize> {
let mut counts = BTreeMap::new();
for finding in &report.findings {
*counts.entry(finding.explanation.clone()).or_insert(0) += 1;
}
counts
}
fn assert_equivalent_reports(a: &SynthesizabilityReport, b: &SynthesizabilityReport, label: &str) {
let eps = 1e-9;
assert!(
(a.overall.difficulty.value() - b.overall.difficulty.value()).abs() < eps,
"{label}: difficulty differs ({} vs {})",
a.overall.difficulty.value(),
b.overall.difficulty.value()
);
assert!(
(a.overall.synthesizability.value() - b.overall.synthesizability.value()).abs() < eps,
"{label}: synthesizability differs"
);
assert!(
(a.overall.confidence.value() - b.overall.confidence.value()).abs() < eps,
"{label}: confidence differs"
);
assert_eq!(
a.overall.verdict, b.overall.verdict,
"{label}: verdict differs"
);
assert_eq!(
finding_code_multiset(a),
finding_code_multiset(b),
"{label}: finding codes differ"
);
assert_eq!(
explanation_multiset(a),
explanation_multiset(b),
"{label}: finding explanations differ"
);
assert_eq!(
a.findings.len(),
b.findings.len(),
"{label}: finding count differs"
);
assert_eq!(
suggestion_code_multiset(a),
suggestion_code_multiset(b),
"{label}: suggestion codes differ"
);
}
#[test]
fn atom_reordering_does_not_change_scores_or_findings() {
let config = AnalysisConfig::default();
let mol = chematic::smiles::parse("C1CC2CCC1C2").expect("valid SMILES");
let baseline = yomitoki::analyze(&mol, &config).expect("analyze succeeds");
for seed in [1u64, 2, 3, 42] {
let reordered_smiles = chematic::smiles::random_smiles(&mol, seed);
let reordered_mol = chematic::smiles::parse(&reordered_smiles).unwrap_or_else(|e| {
panic!("random_smiles produced unparseable output {reordered_smiles:?}: {e}")
});
let reordered_report =
yomitoki::analyze(&reordered_mol, &config).expect("analyze succeeds");
assert_equivalent_reports(&baseline, &reordered_report, &format!("seed {seed}"));
}
}
#[test]
fn atom_reordering_does_not_change_functional_group_findings() {
let config = AnalysisConfig::default();
let mol = chematic::smiles::parse("CC(=O)Oc1ccccc1C(=O)O").expect("aspirin");
let baseline = yomitoki::analyze(&mol, &config).expect("analyze succeeds");
for seed in [1u64, 2, 3, 42] {
let reordered_smiles = chematic::smiles::random_smiles(&mol, seed);
let reordered_mol = chematic::smiles::parse(&reordered_smiles).unwrap_or_else(|e| {
panic!("random_smiles produced unparseable output {reordered_smiles:?}: {e}")
});
let reordered_report =
yomitoki::analyze(&reordered_mol, &config).expect("analyze succeeds");
assert_equivalent_reports(&baseline, &reordered_report, &format!("seed {seed}"));
}
}
#[test]
fn canonical_vs_original_smiles_produce_the_same_report() {
let config = AnalysisConfig::default();
let original = yomitoki::analyze_smiles("C1CC2CCC1C2", &config).expect("valid SMILES");
let mol = chematic::smiles::parse("C1CC2CCC1C2").expect("valid SMILES");
let canonical = chematic::smiles::canonical_smiles(&mol);
let from_canonical = yomitoki::analyze_smiles(&canonical, &config).expect("valid SMILES");
assert_equivalent_reports(&original, &from_canonical, "canonical vs. original");
}
#[test]
fn repeated_analysis_of_the_same_input_is_bit_identical() {
let config = AnalysisConfig::default();
let first = yomitoki::analyze_smiles("CC(=O)Oc1ccccc1C(=O)O", &config).expect("aspirin");
let second = yomitoki::analyze_smiles("CC(=O)Oc1ccccc1C(=O)O", &config).expect("aspirin");
assert_eq!(first, second);
}