use omgkit_core::{BondOrder, MolBuilder};
use crate::valence::explicit_valence_nonstrict;
pub fn clean_up(mol: &mut MolBuilder) {
nitrogens_cleanup(mol);
for i in 0..mol.num_atoms() as u32 {
match mol.atoms()[i as usize].atomic_num {
15 => phosphorus_cleanup(mol, i),
17 | 35 | 53 => halogen_cleanup(mol, i),
_ => {}
}
}
}
fn find_bond<F>(mol: &MolBuilder, atom: u32, mut pred: F) -> Option<(u32, u32)>
where
F: FnMut(&omgkit_core::BondData, u32) -> bool,
{
mol.neighbors(atom).find_map(|(other, bi)| {
let b = &mol.bonds()[bi as usize];
pred(b, other).then_some((bi, other))
})
}
fn nitrogens_cleanup(mol: &mut MolBuilder) {
let mut considered: Vec<u32> = Vec::new();
for i in 0..mol.num_atoms() as u32 {
let a = mol.atoms()[i as usize];
if a.atomic_num != 7 || a.formal_charge != 0 {
continue;
}
if explicit_valence_nonstrict(mol, i) != 5 {
continue;
}
considered.push(i);
let hit = find_bond(mol, i, |b, other| {
let o = mol.atoms()[other as usize];
o.atomic_num == 8 && o.formal_charge == 0 && b.order == BondOrder::Double
});
if let Some((bi, other)) = hit {
mol.bond_mut(bi)
.expect("下标来自遍历")
.set_order(BondOrder::Single);
mol.atom_mut(i).expect("下标来自遍历").formal_charge = 1;
mol.atom_mut(other).expect("邻居下标合法").formal_charge = -1;
}
}
for &i in &considered {
let hit = find_bond(mol, i, |b, other| {
let n = mol.atoms()[other as usize];
n.atomic_num == 7 && n.formal_charge == 0 && b.order == BondOrder::Triple
});
if let Some((bi, other)) = hit {
mol.bond_mut(bi)
.expect("下标来自遍历")
.set_order(BondOrder::Double);
mol.atom_mut(i).expect("下标来自遍历").formal_charge = 1;
mol.atom_mut(other).expect("邻居下标合法").formal_charge = -1;
}
}
}
fn phosphorus_cleanup(mol: &mut MolBuilder, idx: u32) {
if mol.atoms()[idx as usize].formal_charge != 0 {
return;
}
if explicit_valence_nonstrict(mol, idx) != 5 || mol.degree(idx) != 3 {
return;
}
let mut dbl_to_o: Option<(u32, u32)> = None;
let mut has_double_to_c_or_n = false;
for (other, bi) in mol.neighbors(idx) {
let b = mol.bonds()[bi as usize];
let nbr = mol.atoms()[other as usize];
if nbr.atomic_num == 8 && nbr.formal_charge == 0 && b.order == BondOrder::Double {
dbl_to_o = Some((bi, other));
} else if (nbr.atomic_num == 6 || nbr.atomic_num == 7)
&& b.order == BondOrder::Double
&& mol.degree(other) >= 2
{
has_double_to_c_or_n = true;
}
}
if let (true, Some((bi, o))) = (has_double_to_c_or_n, dbl_to_o) {
mol.atom_mut(o).expect("邻居下标合法").formal_charge = -1;
mol.bond_mut(bi)
.expect("下标来自遍历")
.set_order(BondOrder::Single);
mol.atom_mut(idx).expect("下标来自遍历").formal_charge = 1;
}
}
fn halogen_cleanup(mol: &mut MolBuilder, idx: u32) {
if mol.atoms()[idx as usize].formal_charge != 0 {
return;
}
let ev = explicit_valence_nonstrict(mol, idx);
if !matches!(ev, 3 | 5 | 7) {
return;
}
let all_o = mol
.neighbors(idx)
.all(|(o, _)| mol.atoms()[o as usize].atomic_num == 8);
if !all_o {
return;
}
let doubles: Vec<(u32, u32)> = mol
.neighbors(idx)
.filter(|&(_, bi)| mol.bonds()[bi as usize].order == BondOrder::Double)
.map(|(other, bi)| (bi, other))
.collect();
let charge = i8::try_from(doubles.len()).unwrap_or(i8::MAX);
for (bi, other) in doubles {
mol.bond_mut(bi)
.expect("下标来自遍历")
.set_order(BondOrder::Single);
mol.atom_mut(other).expect("邻居下标合法").formal_charge = -1;
}
mol.atom_mut(idx).expect("下标来自遍历").formal_charge = charge;
}
#[cfg(test)]
mod tests {
use omgkit_io::smiles;
use super::*;
use crate::valence::update_property_cache;
fn cleaned(smi: &str) -> (Vec<i8>, Vec<BondOrder>) {
let mut m = smiles::parse(smi).unwrap_or_else(|e| panic!("{}", e.render()));
clean_up(&mut m);
(
m.atoms().iter().map(|a| a.formal_charge).collect(),
m.bonds().iter().map(|b| b.order).collect(),
)
}
#[test]
fn nitro_group_becomes_zwitterion() {
let (q, o) = cleaned("CN(=O)=O");
assert_eq!(q, vec![0, 1, -1, 0], "N 应带正电,第一个 O 带负电");
assert_eq!(o[1], BondOrder::Single, "第一条 N=O 变单键");
assert_eq!(o[2], BondOrder::Double, "第二条保持双键");
}
#[test]
fn azide_becomes_zwitterion() {
let (q, o) = cleaned("CN=N#N");
assert_eq!(q, vec![0, 0, 1, -1]);
assert_eq!(o[2], BondOrder::Double, "N#N 变 N=N");
}
#[test]
fn charged_nitrogen_is_left_alone() {
let before = cleaned("O=[n+]1occcc1");
let mut m = smiles::parse("O=[n+]1occcc1").unwrap();
let q0: Vec<i8> = m.atoms().iter().map(|a| a.formal_charge).collect();
clean_up(&mut m);
let q1: Vec<i8> = m.atoms().iter().map(|a| a.formal_charge).collect();
assert_eq!(q0, q1, "带电氮不应被修改");
assert_eq!(before.0, q1);
}
#[test]
fn neutral_low_valence_nitrogen_untouched() {
let (q, o) = cleaned("CN(C)C");
assert_eq!(q, vec![0, 0, 0, 0]);
assert!(o.iter().all(|&x| x == BondOrder::Single));
}
#[test]
fn phosphorus_zwitterion() {
let (q, o) = cleaned("CC=P(=O)CC");
assert_eq!(q, vec![0, 0, 1, -1, 0, 0]);
assert_eq!(o[1], BondOrder::Double, "C=P 保持双键");
assert_eq!(o[2], BondOrder::Single, "P=O 变单键");
}
#[test]
fn phosphorus_requires_neighbor_degree_at_least_two() {
let (q, o) = cleaned("C=P(=O)CC");
assert!(q.iter().all(|&x| x == 0), "度数不足时不应修改电荷");
assert_eq!(o[0], BondOrder::Double);
assert_eq!(o[1], BondOrder::Double, "P=O 应保持双键");
}
#[test]
fn phosphorus_without_double_to_c_or_n_untouched() {
let (q, o) = cleaned("OP(=O)(O)O");
assert!(q.iter().all(|&x| x == 0));
assert_eq!(o[1], BondOrder::Double);
}
#[test]
fn perchloric_acid() {
let (q, o) = cleaned("OCl(=O)(=O)=O");
assert_eq!(q[1], 3, "Cl 带 +3");
assert_eq!(q.iter().filter(|&&x| x == -1).count(), 3, "三个负氧");
assert_eq!(o.iter().filter(|&&x| x == BondOrder::Double).count(), 0);
}
#[test]
fn fluorine_is_not_a_halogen_here() {
let (q, o) = cleaned("OF(=O)(=O)=O");
assert!(q.iter().all(|&x| x == 0), "F 不参与卤素修正");
assert_eq!(o.iter().filter(|&&x| x == BondOrder::Double).count(), 3);
}
#[test]
fn halogen_with_non_oxygen_neighbor_untouched() {
let (q, _) = cleaned("CCl(=O)=O");
assert!(q.iter().all(|&x| x == 0), "邻居里有碳则不动");
}
#[test]
fn cleanup_makes_overvalent_nitro_pass_valence_check() {
let smi = "CCCCOC(=O)c1c(cccc1N(=O)=O)C(=O)O";
let mut without = smiles::parse(smi).unwrap();
assert!(
update_property_cache(&mut without).is_err(),
"不做第 1 步时,五价中性氮应当被价键校验拒绝"
);
let mut with = smiles::parse(smi).unwrap();
clean_up(&mut with);
assert!(
update_property_cache(&mut with).is_ok(),
"做过第 1 步后应当通过"
);
}
#[test]
fn is_idempotent() {
for smi in ["CN(=O)=O", "CN=N#N", "C=P(=O)CC", "OCl(=O)(=O)=O", "CCO"] {
let mut once = smiles::parse(smi).unwrap();
clean_up(&mut once);
let mut twice = smiles::parse(smi).unwrap();
clean_up(&mut twice);
clean_up(&mut twice);
assert_eq!(once.atoms(), twice.atoms(), "{smi}: 原子不幂等");
assert_eq!(once.bonds(), twice.bonds(), "{smi}: 键不幂等");
}
}
}