smiles-rs 0.2.0

A SMILES parser producing molecular graphs, with no_std support
Documentation
use geometric_traits::traits::SparseValuedMatrixRef;

use super::super::{
    Smiles, canonicalization_state_key,
    support::{assert_canonicalization_invariants, same_canonicalization_state},
};
use crate::{bond::Bond, parser::smiles_parser::parse_wildcard_smiles, smiles::WildcardAtoms};

fn wildcard_smiles(source: &str) -> Smiles<WildcardAtoms> {
    parse_wildcard_smiles(source).unwrap()
}

fn hidden_bond_order_digest(smiles: &Smiles<impl crate::smiles::SmilesAtomPolicy>) -> u64 {
    use core::hash::{Hash, Hasher};
    use std::collections::hash_map::DefaultHasher;

    let mut hasher = DefaultHasher::new();
    smiles
        .bond_matrix()
        .sparse_entries()
        .filter(|((row, column), _)| row < column)
        .for_each(|((row, column), entry)| (row, column, entry.order()).hash(&mut hasher));
    hasher.finish()
}

fn bond_count(smiles: &Smiles<impl crate::smiles::SmilesAtomPolicy>, bond: Bond) -> usize {
    smiles
        .bond_matrix()
        .sparse_entries()
        .filter(|((row, column), entry)| row < column && entry.bond() == bond)
        .count()
}

fn assert_orphan_single_bond_equivalence(
    marked: &Smiles<impl crate::smiles::SmilesAtomPolicy>,
    unmarked: &Smiles<impl crate::smiles::SmilesAtomPolicy>,
) {
    assert_canonicalization_invariants(marked);

    let marked_canonical = marked.canonicalize();
    let unmarked_canonical = unmarked.canonicalize();
    same_canonicalization_state(&marked_canonical, &unmarked_canonical);
    assert_eq!(
        marked.canonical_labeling().order(),
        unmarked.canonical_labeling().order(),
        "orphan direction must not change the canonical rank ordering"
    );
}

#[test]
fn canonicalize_handles_wildcard_quadruple_bond_regression() {
    let smiles = wildcard_smiles("*$*");

    assert_canonicalization_invariants(&smiles);
}

#[test]
fn double_bond_stereo_invariant_survives_component_reordering_regression() {
    let smiles = wildcard_smiles("*[*@]*.F/C=*/*");

    assert_canonicalization_invariants(&smiles);
}

#[test]
fn kekulization_invariant_holds_for_four_membered_aromatic_rings_regression() {
    for source in ["Co1ccc1O", "ccc1ccc1cc"] {
        assert_canonicalization_invariants(&Smiles::from_str(source).unwrap());
    }
}

#[test]
fn aromaticity_invariant_holds_for_radical_bracket_aromatic_atom_regression() {
    let smiles = Smiles::from_str("cc1ncN[n]1").unwrap();

    assert_canonicalization_invariants(&smiles);
}

#[test]
fn canonicalize_preserves_aromatic_triple_bond_order() {
    let smiles = Smiles::from_str("C1=CC#CC=C1").unwrap();
    let canonicalized = smiles.canonicalize();

    assert_eq!(bond_count(&canonicalized, Bond::Triple), 1);
    assert_canonicalization_invariants(&canonicalized);
}

#[test]
fn canonicalize_handles_aromatic_sulfur_chain_regression() {
    let smiles = Smiles::from_str("scCC").unwrap();

    assert_canonicalization_invariants(&smiles);
}

#[test]
fn canonicalize_handles_mixed_aromatic_phosphorus_wildcard_regression() {
    let smiles = wildcard_smiles("bsC3P1*sC3P1**p1C3P1**OC3p1C3Os*p1C3P1**OC3p1C3Os");

    assert_canonicalization_invariants(&smiles);
}

#[test]
fn canonicalize_handles_polywildcard_aromatic_oxygen_regression() {
    let smiles = wildcard_smiles("OO*c8Oc8OOOO**c8OOO*c8OOOO*CO*c8OOOO*c8OOOO**c8OOO*c8OOOO*COOOO");

    assert_canonicalization_invariants(&smiles);
}

#[test]
fn canonicalize_handles_aromatic_wildcard_triangle_regression() {
    let smiles = wildcard_smiles("P1*P1");

    assert_canonicalization_invariants(&smiles);
}

#[test]
fn canonicalize_handles_fuzz_crash_dfd86928_regression() {
    let smiles =
        wildcard_smiles("CBBBBBBBBBBBBBBBBBBBBBBB.OBBBBBBBBBBBBBBBBBBBB.OBBP1*P1**BBBBBBB");

    assert_canonicalization_invariants(&smiles);
}

#[test]
fn canonicalize_handles_fuzz_crash_partial_aromatic_diagnostics_regression() {
    let smiles = wildcard_smiles("bsp1C2P1**C3o1**p1*CPp*O14C3Po1**p1C2P1**C3o1$*p1C4P1**Op1C3Os");

    assert_canonicalization_invariants(&smiles);
}

#[test]
fn canonicalize_handles_fuzz_crash_component_order_budget_regression() {
    let smiles = wildcard_smiles(
        "Oooooooooooooooooooo.****.*ccOc**S*nnbsC3P1*soooooooooo.****.*ccOc**S*nnbsC3P1*sC3P1**p1C3P1**OC3p1C3Os*p1C3P2***S*nnnCnsnC3P1**p1C3P1**OC3p1C3Os*p1C3P2***S*nnnCns",
    );

    assert_canonicalization_invariants(&smiles);
}

#[test]
fn canonicalize_handles_fuzz_crash_new_regression_candidate() {
    let smiles = wildcard_smiles("***SCbs=3P1o*sC3P1**p1C3P1**OC3P1*=*OC3p1C3Os");
    assert_canonicalization_invariants(&smiles);
}

#[test]
fn canonicalize_handles_fuzz_crash_db6568a7_regression() {
    let smiles = wildcard_smiles(
        "*[*@]*.**.******[*@]*.**.**.*.**.**.******.**.****.*.*.**.**.***/**.**.****.**.***.**.**.*.**.**.******N**.****.**.**.**",
    );

    assert_canonicalization_invariants(&smiles);
}

#[test]
fn regression_orphan_directional_single_wildcard_minimized() {
    let marked = wildcard_smiles("*/C/CC");
    let unmarked = wildcard_smiles("*-C-CC");
    assert_orphan_single_bond_equivalence(&marked, &unmarked);
}

#[test]
fn regression_orphan_directional_single_aromatic_wildcard_minimized() {
    let marked = wildcard_smiles("*/c/cC");
    let unmarked = wildcard_smiles("*-c-cC");
    assert_orphan_single_bond_equivalence(&marked, &unmarked);
}

#[test]
fn regression_orphan_directional_single_reverse_traversal() {
    let forward = wildcard_smiles(r"*/C\CC");
    let reversed = wildcard_smiles(r"CC/C\*");
    let unmarked = wildcard_smiles("*-C-CC");
    assert_orphan_single_bond_equivalence(&forward, &unmarked);
    same_canonicalization_state(&forward.canonicalize(), &reversed.canonicalize());
}

#[test]
fn regression_orphan_directional_single_concrete_chain() {
    let marked = Smiles::from_str(r"C/F\Cl").unwrap();
    let unmarked = Smiles::from_str("C-F-Cl").unwrap();
    assert_orphan_single_bond_equivalence(&marked, &unmarked);
}

#[test]
fn regression_orphan_directional_single_sparse_annotation() {
    let marked = wildcard_smiles("CC/C/C");
    let unmarked = wildcard_smiles("CC-C-C");
    assert_orphan_single_bond_equivalence(&marked, &unmarked);
}

#[test]
fn regression_orphan_directional_single_branch() {
    let marked = wildcard_smiles("*(/C)*");
    let unmarked = wildcard_smiles("*(C)*");
    assert_orphan_single_bond_equivalence(&marked, &unmarked);
}

#[test]
fn regression_orphan_directional_ring_closure_single() {
    let marked = Smiles::from_str("C1CC/1").unwrap();
    let unmarked = Smiles::from_str("C1CC1").unwrap();
    assert_orphan_single_bond_equivalence(&marked, &unmarked);
}

#[test]
fn regression_orphan_directional_single_disconnected_components() {
    let marked = wildcard_smiles("*/C/CC.N");
    let unmarked = wildcard_smiles("*-C-CC.N");
    assert_orphan_single_bond_equivalence(&marked, &unmarked);

    let reordered = wildcard_smiles("N.*-C-CC");
    same_canonicalization_state(&marked.canonicalize(), &reordered.canonicalize());
}

#[test]
fn regression_orphan_directional_single_aromatic_concrete() {
    let marked = Smiles::from_str("C/c/cC").unwrap();
    let unmarked = Smiles::from_str("C-c-cC").unwrap();
    assert_orphan_single_bond_equivalence(&marked, &unmarked);
}

#[test]
fn regression_orphan_directional_single_tetrahedral_context() {
    let marked = Smiles::from_str("[C@H](/C)(F)Cl").unwrap();
    let unmarked = Smiles::from_str("[C@H](C)(F)Cl").unwrap();
    assert_orphan_single_bond_equivalence(&marked, &unmarked);
}

#[test]
fn regression_orphan_directional_single_isotope_context() {
    let marked = Smiles::from_str("[13CH3]/[12CH3]").unwrap();
    let unmarked = Smiles::from_str("[13CH3]-[12CH3]").unwrap();
    assert_orphan_single_bond_equivalence(&marked, &unmarked);
}

#[test]
fn canonicalize_handles_fuzz_crash_hex_wildcard_oxygen_regression() {
    let smiles = wildcard_smiles("******#8OOO*c8");

    assert_canonicalization_invariants(&smiles);
}

#[test]
fn canonicalize_handles_fuzz_crash_aromatic_boron_hydrogen_regression() {
    let smiles = Smiles::from_str("[H]bbbbbbbbbb").unwrap();

    assert_canonicalization_invariants(&smiles);
}

#[test]
fn canonicalize_handles_fuzz_crash_square_planar_platinum_regression() {
    let smiles = Smiles::from_str("[NH3][Pt@SP1]([NH3])(Cl)Cl").unwrap();

    assert_canonicalization_invariants(&smiles);
}

#[test]
fn canonicalize_reaches_fixed_point_for_pubchem_cid_10888631_regression() {
    let smiles =
        Smiles::from_str("C1C2[C@@H]3[C@H]4[C@H]5[C@H]6C5CC4[C@@H]6[C@H]2[C@@H]7[C@@H]3C71")
            .unwrap();

    let canonicalized = smiles.canonicalize();
    assert!(canonicalized.is_canonical(), "canonicalize() did not reach a canonical fixed point");

    let recanonicalized = canonicalized.canonicalize();
    same_canonicalization_state(&canonicalized, &recanonicalized);
}

#[test]
fn canonicalize_reaches_fixed_point_for_pubchem_cid_101510359_regression() {
    let smiles = Smiles::from_str("CC(C)(C)C1=CC(=CC(=C1)C2=C3C=CC(=C(C4=NC5=CC6=C(C=C(N6)C(=C7C=CC2=N7)C8=CC(=CC(=C8)C(C)(C)C)C(C)(C)C)C9=C1C=CC(=C(C2=NC(=C(C6=CC=C(N6)C(=C6C=CC9=N6)C6=CC(=CC(=C6)C(C)(C)C)C(C)(C)C)C6=CC(=CC(=C6)C(C)(C)C)C(C)(C)C)C=C2)C2=C6C=C7C(=CC(=N7)C(=C7C=CC(=C(C8=NC(=C(C(=C2)N6)C2=CC(=CC(=C2)C(C)(C)C)C(C)(C)C)C=C8)C2=CC(=CC(=C2)C(C)(C)C)C(C)(C)C)N7)C2=CC(=CC(=C2)C(C)(C)C)C(C)(C)C)C2=C6C=CC(=C(C5=C4)C4=NC(=C(C5=CC=C(N5)C(=C5C=CC2=N5)C2=CC(=CC(=C2)C(C)(C)C)C(C)(C)C)C2=CC(=CC(=C2)C(C)(C)C)C(C)(C)C)C=C4)N6)N1)C1=CC(=CC(=C1)C(C)(C)C)C(C)(C)C)N3)C(C)(C)C").unwrap();

    let canonicalized = smiles.canonicalize();
    let recanonicalized = canonicalized.canonicalize();

    same_canonicalization_state(&canonicalized, &recanonicalized);
}

#[test]
fn canonicalization_spelling_normal_form_preserves_semantic_implicit_hydrogens() {
    let aromaticized = Smiles::from_str("C1=CC=CC=C1")
        .unwrap()
        .perceive_aromaticity()
        .unwrap()
        .into_aromaticized();
    let rewritten = aromaticized.canonicalization_spelling_normal_form();

    assert_eq!(rewritten.implicit_hydrogen_counts(), aromaticized.implicit_hydrogen_counts());
    assert_eq!(rewritten.implicit_hydrogen_cache, aromaticized.implicit_hydrogen_counts());
}

#[test]
fn canonicalization_step_does_not_hide_bond_order_drift_for_pubchem_cid_101510359() {
    let smiles = Smiles::from_str("CC(C)(C)C1=CC(=CC(=C1)C2=C3C=CC(=C(C4=NC5=CC6=C(C=C(N6)C(=C7C=CC2=N7)C8=CC(=CC(=C8)C(C)(C)C)C(C)(C)C)C9=C1C=CC(=C(C2=NC(=C(C6=CC=C(N6)C(=C6C=CC9=N6)C6=CC(=CC(=C6)C(C)(C)C)C(C)(C)C)C6=CC(=CC(=C6)C(C)(C)C)C(C)(C)C)C=C2)C2=C6C=C7C(=CC(=N7)C(=C7C=CC(=C(C8=NC(=C(C(=C2)N6)C2=CC(=CC(=C2)C(C)(C)C)C(C)(C)C)C=C8)C2=CC(=CC(=C2)C(C)(C)C)C(C)(C)C)N7)C2=CC(=CC(=C2)C(C)(C)C)C(C)(C)C)C2=C6C=CC(=C(C5=C4)C4=NC(=C(C5=CC=C(N5)C(=C5C=CC2=N5)C2=CC(=CC(=C2)C(C)(C)C)C(C)(C)C)C2=CC(=CC(=C2)C(C)(C)C)C(C)(C)C)C=C4)N6)N1)C1=CC(=CC(=C1)C(C)(C)C)C(C)(C)C)N3)C(C)(C)C").unwrap();

    let entry = smiles.canonicalize();
    let direct_step = entry.canonicalization_step();

    assert_eq!(canonicalization_state_key(&entry), canonicalization_state_key(&direct_step));
    assert_eq!(hidden_bond_order_digest(&entry), hidden_bond_order_digest(&direct_step));
}