use omgkit_core::{AtomFlags, BondOrder, MolBuilder};
use crate::valence::explicit_valence_nonstrict;
pub fn cleanup_organometallics(mol: &mut MolBuilder) -> usize {
if !(0..mol.num_atoms() as u32).any(|a| needs_fixing(mol, a)) {
return 0;
}
let mut order: Vec<u32> = (0..mol.num_atoms() as u32).collect();
order.sort_by_key(|&a| local_key(mol, a));
let mut changed = 0;
for a in order {
if !needs_fixing(mol, a) {
continue;
}
let Some(bond) = pick_metal_bond(mol, a) else {
continue;
};
if let Some(mut b) = mol.bond_mut(bond) {
b.set_order(BondOrder::Dative);
}
if mol.bonds()[bond as usize].begin != a {
mol.swap_bond_ends(bond).expect("键下标来自遍历");
}
changed += 1;
}
changed
}
fn needs_fixing(mol: &MolBuilder, a: u32) -> bool {
if !is_hypervalent_non_metal(mol, a) || !accepts_dative(mol.atoms()[a as usize].atomic_num) {
return false;
}
mol.neighbors(a).any(|(other, bi)| {
mol.bonds()[bi as usize].order == BondOrder::Single
&& is_metal(mol.atoms()[other as usize].atomic_num)
})
}
fn pick_metal_bond(mol: &MolBuilder, a: u32) -> Option<u32> {
mol.neighbors(a)
.filter(|&(other, bi)| {
mol.bonds()[bi as usize].order == BondOrder::Single
&& is_metal(mol.atoms()[other as usize].atomic_num)
})
.min_by_key(|&(other, _)| (dative_count(mol, other), local_key(mol, other)))
.map(|(_, bi)| bi)
}
fn dative_count(mol: &MolBuilder, a: u32) -> usize {
mol.neighbors(a)
.filter(|&(_, bi)| mol.bonds()[bi as usize].order == BondOrder::Dative)
.count()
}
fn local_key(mol: &MolBuilder, a: u32) -> (u8, i8, usize, i32) {
let at = mol.atoms()[a as usize];
(
at.atomic_num,
at.formal_charge,
mol.degree(a),
explicit_valence_nonstrict(mol, a),
)
}
fn accepts_dative(z: u8) -> bool {
!matches!(z, 1 | 2 | 9 | 10)
}
#[must_use]
pub fn is_metal(z: u8) -> bool {
!matches!(
z,
0 | 1
| 2
| 5
| 6
| 7
| 8
| 9
| 10
| 14
| 15
| 16
| 17
| 18
| 33
| 34
| 35
| 36
| 52
| 53
| 54
| 85
| 86
)
}
fn is_hypervalent_non_metal(mol: &MolBuilder, a: u32) -> bool {
let at = mol.atoms()[a as usize];
if is_metal(at.atomic_num) {
return false;
}
let eff = i32::from(at.atomic_num) - i32::from(at.formal_charge);
if eff <= 0 {
return false;
}
let Ok(eff) = u8::try_from(eff) else {
return false;
};
let Some(elem) = omgkit_core::element::by_atomic_num(eff) else {
return false;
};
let Some(&max_v) = elem.valences.last() else {
return false;
};
if max_v <= 0 {
return false;
}
let max_v = i32::from(max_v);
let ev = explicit_valence_nonstrict(mol, a);
let total_degree =
mol.degree(a) + usize::from(at.num_explicit_hs) + usize::from(at.num_implicit_hs);
ev > max_v || (ev == max_v && at.flags.contains(AtomFlags::AROMATIC) && total_degree == 4)
}
#[cfg(test)]
mod tests {
use omgkit_io::smiles;
use super::*;
use crate::clean_up;
fn after(smi: &str) -> MolBuilder {
let mut m = smiles::parse(smi).unwrap_or_else(|e| panic!("{smi}: {}", e.render()));
clean_up(&mut m);
cleanup_organometallics(&mut m);
m
}
#[test]
fn ferrocene_carbon_metal_bond_becomes_dative() {
let smi = "CN(C)C[C-]12C3=C4C5=C1[Fe++]23456789[C-]%10C6=C7C8=C9%10";
let m = after(smi);
let dative: Vec<_> = m
.bonds()
.iter()
.filter(|b| b.order == BondOrder::Dative)
.collect();
assert_eq!(dative.len(), 1, "应当只有一条键被改成配位键");
let d = dative[0];
assert!(
!is_metal(m.atoms()[d.begin as usize].atomic_num),
"begin 应当是给电子的非金属"
);
assert!(
is_metal(m.atoms()[d.end as usize].atomic_num),
"end 应当是金属"
);
}
#[test]
fn plain_organics_are_untouched() {
for smi in ["CCO", "c1ccccc1", "CC(=O)O", "N[C@@H](C)C(=O)O"] {
let mut m = smiles::parse(smi).unwrap();
clean_up(&mut m);
let before: Vec<_> = m.bonds().to_vec();
assert_eq!(cleanup_organometallics(&mut m), 0, "{smi}");
assert_eq!(m.bonds(), &before[..], "{smi}");
}
}
#[test]
fn non_hypervalent_metal_bonds_are_untouched() {
for smi in ["C[Mg]Br", "[Li]CCCC", "C[Pt](C)(C)C"] {
let m = after(smi);
assert!(
m.bonds().iter().all(|b| b.order != BondOrder::Dative),
"{smi}:碳没有超价,不该改"
);
}
}
#[test]
fn metal_predicate() {
for z in [1, 6, 7, 8, 9, 15, 16, 17, 35, 53] {
assert!(!is_metal(z), "原子序数 {z} 不该算金属");
}
for z in [3, 12, 26, 29, 78, 92] {
assert!(is_metal(z), "原子序数 {z} 应当算金属");
}
assert!(!is_metal(0), "通配原子不算金属");
}
#[test]
fn is_idempotent() {
let smi = "CN(C)C[C-]12C3=C4C5=C1[Fe++]23456789[C-]%10C6=C7C8=C9%10";
let mut m = smiles::parse(smi).unwrap();
clean_up(&mut m);
assert_eq!(cleanup_organometallics(&mut m), 1);
let once: Vec<_> = m.bonds().to_vec();
assert_eq!(cleanup_organometallics(&mut m), 0, "第二次不该再改");
assert_eq!(m.bonds(), &once[..]);
}
}