cosmolkit-core 0.3.0

Redesigned COSMolKit core with value-style molecule state and explicit topology operation contracts
Documentation
use std::collections::BTreeSet;

use super::{
    SubgraphPathError, extend_paths, find_all_paths_of_length_n, find_all_paths_of_lengths_m_to_n,
    rdkit_fp_bond_between_atoms,
};
use crate::{AtomSpec, BondOrder, BondSpec, Element, Molecule};

fn bond_membership(molecule: &Molecule, atom_path: &[usize]) -> Vec<bool> {
    let mut membership = vec![false; molecule.num_bonds()];
    for pair in atom_path.windows(2) {
        let bond = rdkit_fp_bond_between_atoms(molecule, pair[0], pair[1])
            .expect("enumerated path edge must exist");
        membership[bond] = true;
    }
    membership
}

#[test]
fn chain_paths_follow_atom_index_order_and_remove_reverse_bond_duplicates() {
    let molecule = Molecule::from_smiles("CCCC").expect("chain");

    assert_eq!(
        find_all_paths_of_length_n(&molecule, 3, false, false, -1, false).unwrap(),
        vec![vec![0, 1, 2], vec![1, 2, 3]]
    );
    assert_eq!(
        find_all_paths_of_length_n(&molecule, 4, false, false, -1, false).unwrap(),
        vec![vec![0, 1, 2, 3]]
    );
    assert_eq!(
        find_all_paths_of_length_n(&molecule, 3, true, false, -1, false).unwrap(),
        vec![vec![0, 1, 2]]
    );
}

#[test]
fn branch_paths_preserve_source_adjacency_scan_order() {
    let molecule = Molecule::from_smiles("CC(C)C").expect("branched molecule");

    assert_eq!(
        find_all_paths_of_length_n(&molecule, 3, false, false, -1, false).unwrap(),
        vec![vec![0, 1, 2], vec![0, 1, 3], vec![2, 1, 3]]
    );
}

#[test]
fn ring_paths_allow_only_the_final_closure_and_deduplicate_by_bond_set() {
    let molecule = Molecule::from_smiles("C1CCC1").expect("four-membered ring");
    let open_paths = find_all_paths_of_length_n(&molecule, 4, false, false, -1, false).unwrap();
    let closed_paths = find_all_paths_of_length_n(&molecule, 5, false, false, -1, false).unwrap();

    assert_eq!(open_paths.len(), 4);
    assert_eq!(open_paths[0], vec![0, 1, 2, 3]);
    assert_eq!(closed_paths, vec![vec![0, 1, 2, 3, 0]]);
    for path in &open_paths {
        assert_eq!(
            path.iter().copied().collect::<BTreeSet<_>>().len(),
            path.len()
        );
    }
    assert_eq!(
        closed_paths[0][..closed_paths[0].len() - 1]
            .iter()
            .copied()
            .collect::<BTreeSet<_>>()
            .len(),
        closed_paths[0].len() - 1
    );
}

#[test]
fn extend_paths_rejects_internal_repeats_but_accepts_a_final_ring_closure() {
    let triangle_adjacency = [0, 1, 1, 1, 0, 1, 1, 1, 0];
    let path = vec![vec![0, 1, 2]];

    assert!(
        extend_paths(&triangle_adjacency, 3, &path, 5, None)
            .unwrap()
            .is_empty()
    );
    assert_eq!(
        extend_paths(&triangle_adjacency, 3, &path, 4, None).unwrap(),
        vec![vec![0, 1, 2, 0]]
    );
}

#[test]
fn explicit_hydrogens_are_included_only_when_requested() {
    let mut builder = Molecule::builder();
    let carbon = builder.add_atom(AtomSpec::new(Element::C));
    for _ in 0..4 {
        let hydrogen = builder.add_atom(AtomSpec::new(Element::H));
        builder
            .add_bond(BondSpec::new(carbon, hydrogen, BondOrder::Single))
            .expect("methane C-H bond");
    }
    let molecule = builder.build().expect("explicit methane graph");
    assert_eq!(molecule.num_atoms(), 5);

    assert!(
        find_all_paths_of_length_n(&molecule, 2, false, false, -1, false)
            .unwrap()
            .is_empty()
    );
    let with_hydrogens = find_all_paths_of_length_n(&molecule, 2, false, true, -1, false).unwrap();
    assert_eq!(with_hydrogens.len(), 4);
    assert!(with_hydrogens.iter().all(|path| {
        path.iter()
            .any(|atom| molecule.atoms()[*atom].atomic_number() == 1)
    }));
}

#[test]
fn disconnected_and_rooted_paths_keep_source_seed_behavior() {
    let disconnected = Molecule::from_smiles("CC.CC").expect("disconnected molecule");
    assert_eq!(
        find_all_paths_of_length_n(&disconnected, 2, false, false, -1, false).unwrap(),
        vec![vec![0, 1], vec![2, 3]]
    );

    let chain = Molecule::from_smiles("CCCCC").expect("five-atom chain");
    assert_eq!(
        find_all_paths_of_length_n(&chain, 3, false, false, 2, false).unwrap(),
        vec![vec![2, 1, 0], vec![2, 3, 4]]
    );
    assert!(
        find_all_paths_of_length_n(&chain, 3, false, false, 99, false)
            .unwrap()
            .is_empty()
    );
}

#[test]
fn target_length_boundaries_and_range_keys_match_source_behavior() {
    let molecule = Molecule::from_smiles("CCC").expect("chain");
    assert_eq!(
        find_all_paths_of_length_n(&molecule, 1, false, false, -1, false).unwrap(),
        vec![vec![0], vec![1], vec![2]]
    );
    assert!(
        find_all_paths_of_length_n(&molecule, 0, false, false, -1, false)
            .unwrap()
            .is_empty()
    );

    let range = find_all_paths_of_lengths_m_to_n(&molecule, 1, 3, false, false, -1, false).unwrap();
    assert_eq!(range.keys().copied().collect::<Vec<_>>(), vec![1, 2, 3]);
    assert!(matches!(
        find_all_paths_of_lengths_m_to_n(&molecule, 3, 2, false, false, -1, false),
        Err(SubgraphPathError::InvalidArguments { .. })
    ));
}

#[test]
fn fused_ring_paths_are_legal_deterministic_and_bond_set_unique() {
    let molecule = Molecule::from_smiles("C1CCC2CCCCC2C1").expect("fused rings");
    let paths = find_all_paths_of_length_n(&molecule, 6, false, false, -1, false).unwrap();
    assert!(!paths.is_empty());
    assert_eq!(
        paths,
        find_all_paths_of_length_n(&molecule, 6, false, false, -1, false).unwrap()
    );

    let memberships = paths
        .iter()
        .map(|path| bond_membership(&molecule, path))
        .collect::<BTreeSet<_>>();
    assert_eq!(memberships.len(), paths.len());
    assert!(paths.iter().all(|path| {
        path.windows(2)
            .all(|pair| rdkit_fp_bond_between_atoms(&molecule, pair[0], pair[1]).is_some())
    }));
}

#[test]
fn only_shortest_paths_prunes_longer_routes_between_adjacent_ring_atoms() {
    let molecule = Molecule::from_smiles("C1CC1").expect("triangle");
    assert_eq!(
        find_all_paths_of_length_n(&molecule, 3, false, false, -1, false)
            .unwrap()
            .len(),
        3
    );
    assert!(
        find_all_paths_of_length_n(&molecule, 3, false, false, -1, true)
            .unwrap()
            .is_empty()
    );
}