use omgkit_core::{element, AtomFlags, BondFlags, Hybridization, MolBuilder};
use crate::aromaticity::count_pi_electrons;
use crate::valence::total_valence_nonstrict;
pub fn set_conjugation(mol: &mut MolBuilder) {
for bi in 0..mol.num_bonds() as u32 {
let aromatic = mol.bonds()[bi as usize].flags.contains(BondFlags::AROMATIC);
if let Some(mut b) = mol.bond_mut(bi) {
b.flags_mut().set(BondFlags::CONJUGATED, aromatic);
}
}
let candidate: Vec<bool> = (0..mol.num_atoms() as u32)
.map(|i| is_conjugation_candidate(mol, i))
.collect();
for i in 0..mol.num_atoms() as u32 {
mark_conjugated_around(mol, i, &candidate);
}
}
pub fn set_hybridization(mol: &mut MolBuilder) {
for i in 0..mol.num_atoms() as u32 {
let h = hybridization_of(mol, i);
if let Some(a) = mol.atom_mut(i) {
a.hybridization = h;
}
}
}
fn total_degree(mol: &MolBuilder, idx: u32) -> i32 {
let a = mol.atoms()[idx as usize];
mol.degree(idx) as i32 + i32::from(a.num_explicit_hs) + i32::from(a.num_implicit_hs)
}
fn is_conjugation_candidate(mol: &MolBuilder, idx: u32) -> bool {
let atom = mol.atoms()[idx as usize];
let z = atom.atomic_num;
let valences = element::by_atomic_num(z).map_or(&[-1i8][..], |e| e.valences);
let min_valence = valences.first().map_or(-1, |&v| i32::from(v));
if atom.formal_charge == 0
&& min_valence >= 0
&& total_valence_nonstrict(mol, idx) > min_valence
{
return false;
}
let n_outer = i32::from(element::by_atomic_num(z).map_or(0, |e| e.outer_electrons));
let row_ok =
z <= 10 || (n_outer != 5 && n_outer != 6) || (n_outer == 6 && total_degree(mol, idx) < 2);
row_ok && count_pi_electrons(mol, idx).is_some_and(|n| n > 0)
}
fn mark_conjugated_around(mol: &mut MolBuilder, idx: u32, candidate: &[bool]) {
if !candidate[idx as usize] {
return;
}
let sbo = mol.degree(idx) as i32
+ i32::from(mol.atoms()[idx as usize].num_explicit_hs)
+ i32::from(mol.atoms()[idx as usize].num_implicit_hs);
if !(2..=3).contains(&sbo) {
return;
}
let nbrs: Vec<(u32, u32)> = mol.neighbors(idx).collect();
let mut to_mark: Vec<u32> = Vec::new();
for &(other1, b1) in &nbrs {
if mol.bonds()[b1 as usize].valence_contribution_to(idx) < 1.5
|| !candidate[other1 as usize]
{
continue;
}
for &(other2, b2) in &nbrs {
if b1 == b2 {
continue;
}
if total_degree(mol, other2) > 3 || !candidate[other2 as usize] {
continue;
}
to_mark.push(b1);
to_mark.push(b2);
}
}
for b in to_mark {
if let Some(mut bond) = mol.bond_mut(b) {
bond.flags_mut().insert(BondFlags::CONJUGATED);
}
}
}
fn has_conjugated_bond(mol: &MolBuilder, idx: u32) -> bool {
mol.neighbors(idx).any(|(_, bi)| {
mol.bonds()[bi as usize]
.flags
.contains(BondFlags::CONJUGATED)
})
}
fn orbital_count(mol: &MolBuilder, idx: u32) -> i32 {
let atom = mol.atoms()[idx as usize];
let mut deg = total_degree(mol, idx);
for (_, bi) in mol.neighbors(idx) {
let b = mol.bonds()[bi as usize];
if b.order == omgkit_core::BondOrder::Unspecified
|| (b.order == omgkit_core::BondOrder::Dative && b.end != idx)
{
deg -= 1;
}
}
if atom.atomic_num <= 1 {
return deg;
}
let n_outer =
i32::from(element::by_atomic_num(atom.atomic_num).map_or(0, |e| e.outer_electrons));
let total_valence = total_valence_nonstrict(mol, idx);
let charge = i32::from(atom.formal_charge);
let free_electrons = n_outer - (total_valence + charge);
if total_valence + n_outer - charge < 8 {
let n_radicals = i32::from(atom.num_radical_electrons);
deg + (free_electrons - n_radicals) / 2 + n_radicals
} else {
deg + free_electrons / 2
}
}
fn hybridization_of(mol: &MolBuilder, idx: u32) -> Hybridization {
let atom = mol.atoms()[idx as usize];
if atom.atomic_num == 0 {
return Hybridization::Unspecified;
}
let deg = total_degree(mol, idx);
match atom.chiral_tag {
omgkit_core::ChiralTag::SquarePlanar if (2..=4).contains(°) => {
return Hybridization::Sp2d
}
omgkit_core::ChiralTag::TrigonalBipyramidal if (2..=5).contains(°) => {
return Hybridization::Sp3d
}
omgkit_core::ChiralTag::Octahedral if (2..=6).contains(°) => {
return Hybridization::Sp3d2
}
t if t.is_tetrahedral() && deg == 4 => return Hybridization::Sp3,
_ => {}
}
let norbs = if atom.atomic_num < 89 {
orbital_count(mol, idx)
} else {
total_degree(mol, idx)
};
match norbs {
0 | 1 => Hybridization::S,
2 => Hybridization::Sp,
3 => Hybridization::Sp2,
4 => {
if total_degree(mol, idx) > 3 || !has_conjugated_bond(mol, idx) {
Hybridization::Sp3
} else {
Hybridization::Sp2
}
}
5 => Hybridization::Sp3d,
6 => Hybridization::Sp3d2,
_ => Hybridization::Unspecified,
}
}
#[must_use]
pub fn atom_is_conjugated(mol: &MolBuilder, idx: u32) -> bool {
has_conjugated_bond(mol, idx)
}
pub fn mark_conjugated_atoms(mol: &mut MolBuilder) {
for i in 0..mol.num_atoms() as u32 {
let c = has_conjugated_bond(mol, i);
if let Some(a) = mol.atom_mut(i) {
a.flags.set(AtomFlags::CONJUGATED, c);
}
}
}
#[cfg(test)]
mod tests {
use omgkit_io::smiles;
use super::*;
use crate::valence::update_property_cache;
use crate::{assign_radicals, clean_up, kekulize, perceive_rings, set_aromaticity};
fn pipeline(smi: &str) -> MolBuilder {
let mut m = smiles::parse(smi).unwrap_or_else(|e| panic!("{}", e.render()));
clean_up(&mut m);
update_property_cache(&mut m).expect("价键校验应通过");
let _ = perceive_rings(&mut m);
kekulize(&mut m).expect("应能 kekulize");
assign_radicals(&mut m);
set_aromaticity(&mut m);
set_conjugation(&mut m);
set_hybridization(&mut m);
m
}
fn hybrids(smi: &str) -> Vec<Hybridization> {
pipeline(smi)
.atoms()
.iter()
.map(|a| a.hybridization)
.collect()
}
fn conjugated(smi: &str) -> Vec<bool> {
pipeline(smi)
.bonds()
.iter()
.map(|b| b.flags.contains(BondFlags::CONJUGATED))
.collect()
}
#[test]
fn simple_hybridizations() {
assert_eq!(hybrids("CC")[0], Hybridization::Sp3, "乙烷");
assert_eq!(hybrids("C=C")[0], Hybridization::Sp2, "乙烯");
assert_eq!(hybrids("C#C")[0], Hybridization::Sp, "乙炔");
assert_eq!(hybrids("CO")[1], Hybridization::Sp3, "甲醇的氧");
assert_eq!(hybrids("C#N")[1], Hybridization::Sp, "氰基的氮");
}
#[test]
fn aromatic_atoms_are_sp2() {
assert!(hybrids("c1ccccc1").iter().all(|&h| h == Hybridization::Sp2));
assert!(hybrids("c1ccncc1").iter().all(|&h| h == Hybridization::Sp2));
}
#[test]
fn aromatic_bonds_are_conjugated() {
assert!(conjugated("c1ccccc1").iter().all(|&c| c));
assert!(conjugated("c1ccc2ccccc2c1").iter().all(|&c| c));
}
#[test]
fn butadiene_is_fully_conjugated() {
assert_eq!(conjugated("C=CC=C"), vec![true, true, true]);
}
#[test]
fn isolated_double_bonds_are_not_conjugated() {
assert_eq!(conjugated("CC=CC"), vec![false, false, false], "2-丁烯");
assert_eq!(
conjugated("C=CCC=C"),
vec![false, false, false, false],
"1,4-戊二烯:中间的 sp3 碳打断共轭"
);
assert_eq!(conjugated("CCO"), vec![false, false]);
}
#[test]
fn triple_bonds_conjugate() {
assert_eq!(conjugated("C#CC#C"), vec![true, true, true], "丁二炔");
assert_eq!(conjugated("C=CC#N"), vec![true, true, true], "丙烯腈");
}
#[test]
fn heavy_elements_do_not_extend_conjugation() {
let c = conjugated("Pc1ccccc1");
assert!(!c[0], "C—P 键不应共轭,实际 {c:?}");
assert!(c[1..].iter().all(|&x| x), "苯环内部仍应全部共轭");
assert_eq!(hybrids("Pc1ccccc1")[0], Hybridization::Sp3, "磷应为 sp³");
}
#[test]
fn sp3_downgrade_requires_low_total_degree() {
let h = hybrids("CP1(C)=CC=CN=C1C");
assert_eq!(h[1], Hybridization::Sp3, "磷总度数为 4,不应降为 sp²");
}
#[test]
fn carboxyl_is_conjugated() {
let c = conjugated("CC(=O)O");
assert!(c[1] && c[2], "C=O 与 C—O 都应共轭,实际 {c:?}");
}
#[test]
fn is_idempotent() {
for smi in ["c1ccccc1", "C=CC=C", "CC(=O)O", "CCO"] {
let mut m = pipeline(smi);
let once: Vec<_> = m.atoms().iter().map(|a| a.hybridization).collect();
let once_b: Vec<_> = m.bonds().iter().map(|b| b.flags).collect();
set_conjugation(&mut m);
set_hybridization(&mut m);
let twice: Vec<_> = m.atoms().iter().map(|a| a.hybridization).collect();
let twice_b: Vec<_> = m.bonds().iter().map(|b| b.flags).collect();
assert_eq!(once, twice, "{smi}: 杂化不幂等");
assert_eq!(once_b, twice_b, "{smi}: 共轭不幂等");
}
}
#[test]
fn dummy_atoms_have_no_hybridization() {
assert_eq!(hybrids("C[*]")[1], Hybridization::Unspecified);
}
}