#![forbid(unsafe_code)]
use std::collections::{HashMap, HashSet};
use chematic_core::{Atom, AtomIdx, BondIdx, BondOrder, Element, Molecule, MoleculeBuilder};
use chematic_perception::find_sssr;
pub fn murcko_scaffold(mol: &Molecule) -> Molecule {
let rings = find_sssr(mol);
if rings.ring_count() == 0 {
return MoleculeBuilder::new().build();
}
let mut scaffold_atoms: HashSet<AtomIdx> = rings
.rings()
.iter()
.flat_map(|r| r.iter().copied())
.collect();
loop {
let mut changed = false;
for i in 0..mol.atom_count() {
let idx = AtomIdx(i as u32);
if scaffold_atoms.contains(&idx) {
continue;
}
let scaffold_neighbors = mol
.neighbors(idx)
.filter(|(nb, _)| scaffold_atoms.contains(nb))
.count();
if scaffold_neighbors >= 2 {
scaffold_atoms.insert(idx);
changed = true;
}
}
if !changed { break; }
}
build_subgraph(mol, &scaffold_atoms)
}
pub fn generic_murcko_scaffold(mol: &Molecule) -> Molecule {
let scaffold = murcko_scaffold(mol);
if scaffold.atom_count() == 0 {
return scaffold;
}
let mut builder = MoleculeBuilder::new();
let mut remap: HashMap<AtomIdx, AtomIdx> = HashMap::new();
for i in 0..scaffold.atom_count() {
let new_idx = builder.add_atom(Atom::organic(Element::C));
remap.insert(AtomIdx(i as u32), new_idx);
}
for i in 0..scaffold.bond_count() {
let bond = scaffold.bond(BondIdx(i as u32));
if let (Some(&new_a), Some(&new_b)) = (remap.get(&bond.atom1), remap.get(&bond.atom2)) {
let _ = builder.add_bond(new_a, new_b, BondOrder::Single);
}
}
builder.build()
}
fn build_subgraph(mol: &Molecule, atom_set: &HashSet<AtomIdx>) -> Molecule {
let mut builder = MoleculeBuilder::new();
let mut remap: HashMap<AtomIdx, AtomIdx> = HashMap::new();
for i in 0..mol.atom_count() {
let old_idx = AtomIdx(i as u32);
if atom_set.contains(&old_idx) {
let new_idx = builder.add_atom(mol.atom(old_idx).clone());
remap.insert(old_idx, new_idx);
}
}
for i in 0..mol.bond_count() {
let bond = mol.bond(BondIdx(i as u32));
if let (Some(&new_a), Some(&new_b)) = (remap.get(&bond.atom1), remap.get(&bond.atom2)) {
let _ = builder.add_bond(new_a, new_b, bond.order);
}
}
builder.build()
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
#[test]
fn murcko_benzene_preserves_all_atoms() {
let mol = parse("c1ccccc1").unwrap();
let scaffold = murcko_scaffold(&mol);
assert_eq!(scaffold.atom_count(), 6);
}
#[test]
fn murcko_toluene_removes_methyl() {
let mol = parse("Cc1ccccc1").unwrap();
let scaffold = murcko_scaffold(&mol);
assert_eq!(scaffold.atom_count(), 6, "methyl group should be removed");
}
#[test]
fn murcko_ethylbenzene_removes_chain() {
let mol = parse("CCc1ccccc1").unwrap();
let scaffold = murcko_scaffold(&mol);
assert_eq!(scaffold.atom_count(), 6, "ethyl chain should be removed");
}
#[test]
fn murcko_acyclic_returns_empty() {
let mol = parse("CC").unwrap();
let scaffold = murcko_scaffold(&mol);
assert_eq!(scaffold.atom_count(), 0);
}
#[test]
fn generic_murcko_benzene_all_carbon_single() {
let mol = parse("c1ccccc1").unwrap();
let generic = generic_murcko_scaffold(&mol);
assert_eq!(generic.atom_count(), 6);
for i in 0..generic.atom_count() {
let atom = generic.atom(AtomIdx(i as u32));
assert_eq!(atom.element, Element::C, "all atoms should be carbon");
}
for i in 0..generic.bond_count() {
let bond = generic.bond(chematic_core::BondIdx(i as u32));
assert_eq!(
bond.order,
BondOrder::Single,
"all bonds should be Single"
);
}
}
#[test]
fn murcko_biphenyl_keeps_all_ring_atoms() {
let mol = parse("c1ccccc1c1ccccc1").unwrap();
let scaffold = murcko_scaffold(&mol);
assert!(
scaffold.atom_count() >= 12,
"biphenyl scaffold should have at least 12 atoms, got {}",
scaffold.atom_count()
);
}
}