#![forbid(unsafe_code)]
use std::collections::{HashMap, VecDeque};
use chematic_core::{AtomIdx, BondIdx, Element, Molecule, MoleculeBuilder};
fn connected_components(mol: &Molecule) -> Vec<Vec<AtomIdx>> {
let n = mol.atom_count();
let mut visited = vec![false; n];
let mut components: Vec<Vec<AtomIdx>> = Vec::new();
for start in 0..n {
if visited[start] {
continue;
}
visited[start] = true;
let mut component = Vec::new();
let mut queue: VecDeque<AtomIdx> = VecDeque::new();
queue.push_back(AtomIdx(start as u32));
while let Some(current) = queue.pop_front() {
component.push(current);
for (neighbor, _) in mol.neighbors(current) {
let ni = neighbor.0 as usize;
if !visited[ni] {
visited[ni] = true;
queue.push_back(neighbor);
}
}
}
components.push(component);
}
components.sort_by(|a, b| b.len().cmp(&a.len()));
components
}
fn copy_bonds(mol: &Molecule, builder: &mut MoleculeBuilder, remap: &HashMap<AtomIdx, AtomIdx>) {
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);
}
}
}
pub fn largest_fragment(mol: &Molecule) -> Molecule {
if mol.atom_count() == 0 {
return MoleculeBuilder::new().build();
}
let components = connected_components(mol);
let largest = &components[0];
let mut remap: HashMap<AtomIdx, AtomIdx> = HashMap::new();
let mut builder = MoleculeBuilder::new();
for &old_idx in largest {
let new_idx = builder.add_atom(mol.atom(old_idx).clone());
remap.insert(old_idx, new_idx);
}
copy_bonds(mol, &mut builder, &remap);
builder.build()
}
pub fn neutralize_charges(mol: &Molecule) -> Molecule {
let mut modifications: HashMap<AtomIdx, (i8, Option<u8>)> = HashMap::new();
for i in 0..mol.atom_count() {
let idx = AtomIdx(i as u32);
let atom = mol.atom(idx);
let h = atom.hydrogen_count.unwrap_or(0);
match (atom.element, atom.charge) {
(Element::O, -1) => {
let has_c_neighbor =
mol.neighbors(idx).any(|(nb, _)| mol.atom(nb).element == Element::C);
if has_c_neighbor {
modifications.insert(idx, (0, Some(h + 1)));
}
}
(Element::N, 1) | (Element::O, 1) => {
if h > 0 {
modifications.insert(idx, (0, Some(h - 1)));
}
}
_ => {}
}
}
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);
let mut atom = mol.atom(old_idx).clone();
if let Some(&(new_charge, new_h)) = modifications.get(&old_idx) {
atom.charge = new_charge;
atom.hydrogen_count = new_h;
}
let new_idx = builder.add_atom(atom);
remap.insert(old_idx, new_idx);
}
copy_bonds(mol, &mut builder, &remap);
builder.build()
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
#[test]
fn largest_fragment_two_fragments_picks_larger() {
let mol = parse("CC.CCC").unwrap();
let result = largest_fragment(&mol);
assert_eq!(result.atom_count(), 3, "should keep propane (3 C)");
}
#[test]
fn largest_fragment_single_fragment_unchanged() {
let mol = parse("CC").unwrap();
let result = largest_fragment(&mol);
assert_eq!(result.atom_count(), 2);
}
#[test]
fn largest_fragment_keeps_benzene_over_ethane() {
let mol = parse("CC.c1ccccc1").unwrap();
let result = largest_fragment(&mol);
assert_eq!(result.atom_count(), 6, "should keep benzene (6 atoms)");
}
#[test]
fn largest_fragment_ionic_pair_keeps_one_atom() {
let mol = parse("[Na+].[Cl-]").unwrap();
let result = largest_fragment(&mol);
assert_eq!(result.atom_count(), 1);
}
#[test]
fn neutralize_neutral_molecule_unchanged() {
let mol = parse("CC").unwrap();
let result = neutralize_charges(&mol);
for i in 0..result.atom_count() {
let atom = result.atom(AtomIdx(i as u32));
assert_eq!(atom.charge, 0, "all atoms should remain neutral");
}
}
#[test]
fn neutralize_acetate_oxygen() {
let mol = parse("CC(=O)[O-]").unwrap();
let result = neutralize_charges(&mol);
let neutralized_o = (0..result.atom_count())
.map(|i| result.atom(AtomIdx(i as u32)))
.find(|a| a.element == Element::O && a.hydrogen_count == Some(1));
assert!(
neutralized_o.is_some(),
"neutralized [O-] should have hydrogen_count == Some(1)"
);
assert_eq!(
neutralized_o.unwrap().charge,
0,
"neutralized [O-] should have charge == 0"
);
}
}