use crate::{element, AtomFlags, BondFlags, BondOrder, MolBuilder};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValenceError {
pub atom: u32,
pub symbol: &'static str,
pub valence: i32,
pub kind: ValenceErrorKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ValenceErrorKind {
ExplicitValenceTooHigh,
AromaticValenceNotAllowed,
UnreasonableFormalCharge,
}
impl core::fmt::Display for ValenceError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let what = match self.kind {
ValenceErrorKind::ExplicitValenceTooHigh => "显式价超出允许范围",
ValenceErrorKind::AromaticValenceNotAllowed => "芳香原子的显式价不在允许的价态中",
ValenceErrorKind::UnreasonableFormalCharge => "形式电荷不合理",
};
write!(
f,
"原子 #{}({}):{},显式价 = {}",
self.atom, self.symbol, what, self.valence
)
}
}
impl std::error::Error for ValenceError {}
#[must_use]
pub fn is_aromatic_atom(mol: &MolBuilder, idx: u32) -> bool {
if mol.atoms()[idx as usize]
.flags
.contains(AtomFlags::AROMATIC)
{
return true;
}
mol.neighbors(idx).any(|(_, bi)| {
let b = mol.bonds()[bi as usize];
b.flags.contains(BondFlags::AROMATIC) || b.order == BondOrder::Aromatic
})
}
#[must_use]
pub fn valence_shift(z: u8, from: i8, to: i8) -> i32 {
let ovalens = valences_of(z);
if ovalens.len() == 1 && ovalens[0] == -1 {
return 0; }
let at = |q: i8| -> Option<i32> {
let eff = effective_atomic_num(z, q);
if eff == 0 {
return None;
}
let v = default_valence(eff);
if v < 0 {
None
} else {
Some(v)
}
};
match (at(from), at(to)) {
(Some(a), Some(b)) => b - a,
_ => 0,
}
}
fn effective_atomic_num(z: u8, charge: i8) -> u8 {
let max = (element::count() - 1) as i32;
(i32::from(z) - i32::from(charge)).clamp(0, max) as u8
}
fn can_be_hypervalent(z: u8, eff_z: u8) -> bool {
(eff_z > 16 && (z == 15 || z == 16)) || (eff_z > 34 && (z == 33 || z == 34))
}
fn valences_of(z: u8) -> &'static [i8] {
element::by_atomic_num(z).map_or(&[-1][..], |e| e.valences)
}
fn default_valence(z: u8) -> i32 {
valences_of(z).first().map_or(-1, |&v| i32::from(v))
}
fn last_valence(z: u8) -> i32 {
valences_of(z).last().map_or(-1, |&v| i32::from(v))
}
#[must_use]
pub fn explicit_valence_nonstrict(mol: &MolBuilder, idx: u32) -> i32 {
explicit_valence_of(mol, idx, false).unwrap_or(0)
}
pub fn explicit_valence_of(mol: &MolBuilder, idx: u32, strict: bool) -> Result<i32, ValenceError> {
let hs = f32::from(mol.atoms()[idx as usize].num_explicit_hs);
explicit_valence_with(mol, idx, hs, strict)
}
fn explicit_valence_with(
mol: &MolBuilder,
idx: u32,
extra_hs: f32,
strict: bool,
) -> Result<i32, ValenceError> {
let atom = mol.atoms()[idx as usize];
let z = atom.atomic_num;
let mut accum: f32 = mol
.neighbors(idx)
.map(|(_, bi)| mol.bonds()[bi as usize].valence_contribution_to(idx))
.sum();
accum += extra_hs;
let ovalens = valences_of(z);
let eff_z = if ovalens.len() > 1 || ovalens[0] != -1 {
effective_atomic_num(z, atom.formal_charge)
} else {
z
};
let dv = default_valence(eff_z);
let valens = valences_of(eff_z);
if dv >= 0 && accum > dv as f32 && is_aromatic_atom(mol, idx) {
let mut pval = dv;
for &val in valens {
let val = i32::from(val);
if val == -1 || val as f32 > accum {
break;
}
pval = val;
}
if accum - pval as f32 <= 1.5 {
accum = pval as f32;
}
}
accum += 0.1;
let res = accum.round() as i32;
if !strict {
return Ok(res);
}
let mut max_valence = last_valence(eff_z);
let mut offset = 0i32;
if can_be_hypervalent(z, eff_z) {
max_valence = last_valence(z);
offset -= i32::from(atom.formal_charge);
}
if z == 1 && atom.formal_charge == -1 {
max_valence = 2;
}
if max_valence >= 0 && last_valence(z) >= 0 && (res + offset) > max_valence {
return Err(ValenceError {
atom: idx,
symbol: element::by_atomic_num(z).map_or("?", |e| e.symbol),
valence: res,
kind: ValenceErrorKind::ExplicitValenceTooHigh,
});
}
Ok(res)
}
#[must_use]
pub fn implicit_hs_nonstrict(mol: &MolBuilder, idx: u32, ev: i32) -> u8 {
implicit_hs_of(mol, idx, ev, false).unwrap_or(0)
}
#[must_use]
pub fn total_valence_nonstrict(mol: &MolBuilder, idx: u32) -> i32 {
let ev = explicit_valence_nonstrict(mol, idx);
ev + i32::from(implicit_hs_nonstrict(mol, idx, ev))
}
pub fn implicit_hs_of(
mol: &MolBuilder,
idx: u32,
ev: i32,
strict: bool,
) -> Result<u8, ValenceError> {
if mol.atoms()[idx as usize]
.flags
.contains(AtomFlags::NO_IMPLICIT)
{
return Ok(0);
}
implicit_hs_inner(mol, idx, ev, strict)
}
fn implicit_hs_inner(
mol: &MolBuilder,
idx: u32,
ev: i32,
strict: bool,
) -> Result<u8, ValenceError> {
let atom = mol.atoms()[idx as usize];
let z = atom.atomic_num;
if z == 0 {
return Ok(0); }
let n_radicals = i32::from(atom.num_radical_electrons);
if ev == 0 && n_radicals == 0 && z == 1 {
return match atom.formal_charge {
1 | -1 => Ok(0),
0 => Ok(1),
_ if strict => Err(ValenceError {
atom: idx,
symbol: "H",
valence: ev,
kind: ValenceErrorKind::UnreasonableFormalCharge,
}),
_ => Ok(0),
};
}
let mut explicit_plus_rad = ev + n_radicals;
let ovalens = valences_of(z);
let mut eff_z = if ovalens.len() > 1 || ovalens[0] != -1 {
effective_atomic_num(z, atom.formal_charge)
} else {
z
};
if eff_z == 0 {
return Ok(0);
}
let dv = default_valence(eff_z);
if dv == -1 {
return Ok(0); }
if can_be_hypervalent(z, eff_z) {
eff_z = z;
explicit_plus_rad -= i32::from(atom.formal_charge);
}
let valens = valences_of(eff_z);
let res: i32 = if is_aromatic_atom(mol, idx) {
if explicit_plus_rad <= dv {
dv - explicit_plus_rad
} else {
let satisfied = valens
.iter()
.map(|&v| i32::from(v))
.take_while(|&v| v > 0)
.any(|v| explicit_plus_rad == v);
if !satisfied && strict {
return Err(ValenceError {
atom: idx,
symbol: element::by_atomic_num(z).map_or("?", |e| e.symbol),
valence: ev,
kind: ValenceErrorKind::AromaticValenceNotAllowed,
});
}
0
}
} else {
let found = valens
.iter()
.map(|&v| i32::from(v))
.take_while(|&v| v >= 0)
.find(|&v| explicit_plus_rad <= v);
match found {
Some(v) => v - explicit_plus_rad,
None => {
if strict && last_valence(eff_z) != -1 && last_valence(z) > 0 {
return Err(ValenceError {
atom: idx,
symbol: element::by_atomic_num(z).map_or("?", |e| e.symbol),
valence: ev,
kind: ValenceErrorKind::ExplicitValenceTooHigh,
});
}
0
}
}
};
Ok(u8::try_from(res.max(0)).unwrap_or(0))
}
#[must_use]
pub fn implicit_hs_for_bare_form(mol: &MolBuilder, idx: u32) -> Option<u8> {
let atom = mol.atoms()[idx as usize];
if atom.formal_charge != 0 || atom.num_radical_electrons != 0 || atom.isotope != 0 {
return None;
}
let ev = explicit_valence_with(mol, idx, 0.0, false).ok()?;
implicit_hs_inner(mol, idx, ev, false).ok()
}