use omgkit_core::{BondDirection, BondOrder, ChiralTag};
use super::expr::{AtomExpr, AtomPrim, BondExpr, BondPrim};
use super::QueryMol;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AtomProps {
pub atomic_num: u8,
pub aromatic: bool,
pub charge: i32,
pub isotope: u16,
pub degree: u32,
pub total_hs: u32,
pub implicit_hs: u32,
pub valence: u32,
pub ring_count: u32,
pub min_ring_size: u32,
pub ring_bonds: u32,
pub chiral_tag: ChiralTag,
pub atom_map: u16,
}
impl AtomProps {
#[must_use]
pub fn total_degree(&self) -> u32 {
self.degree + self.total_hs
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BondProps {
pub order: BondOrder,
pub in_ring: bool,
pub direction: BondDirection,
pub dative_forward: bool,
}
impl Default for BondProps {
fn default() -> Self {
Self {
order: BondOrder::Unspecified,
in_ring: false,
direction: BondDirection::None,
dative_forward: true,
}
}
}
pub fn atom_matches(
expr: &AtomExpr,
props: &AtomProps,
recursive: &mut dyn FnMut(&QueryMol) -> bool,
) -> bool {
match expr {
AtomExpr::Prim(p) => prim_matches(p, props, recursive),
AtomExpr::Not(e) => !atom_matches(e, props, recursive),
AtomExpr::And(parts) => parts.iter().all(|e| atom_matches(e, props, recursive)),
AtomExpr::Or(parts) => parts.iter().any(|e| atom_matches(e, props, recursive)),
}
}
fn prim_matches(p: &AtomPrim, a: &AtomProps, recursive: &mut dyn FnMut(&QueryMol) -> bool) -> bool {
match p {
AtomPrim::Any => true,
AtomPrim::Aromatic => a.aromatic,
AtomPrim::Aliphatic => !a.aromatic,
AtomPrim::Element { z, aromatic } => {
a.atomic_num == *z && aromatic.map_or(true, |want| want == a.aromatic)
}
AtomPrim::Degree(n) => a.degree == *n,
AtomPrim::TotalDegree(n) => a.total_degree() == *n,
AtomPrim::TotalHs(n) => a.total_hs == *n,
AtomPrim::ImplicitHs(n) => a.implicit_hs == *n,
AtomPrim::Valence(n) => a.valence == *n,
AtomPrim::RingCount(n) => n.map_or(a.ring_count > 0, |k| a.ring_count == k),
AtomPrim::RingBondCount(n) => n.map_or(a.ring_bonds > 0, |k| a.ring_bonds == k),
AtomPrim::RingSize(n) => n.map_or(a.min_ring_size > 0, |k| a.min_ring_size == k),
AtomPrim::Charge(c) => a.charge == *c,
AtomPrim::Isotope(i) => a.isotope == *i,
AtomPrim::AtomMap(_) => true,
AtomPrim::Chirality(_) => true,
AtomPrim::Recursive(sub) => recursive(sub),
}
}
#[must_use]
pub fn allowed_elements(expr: &AtomExpr) -> Option<std::collections::BTreeSet<u8>> {
match expr {
AtomExpr::Prim(AtomPrim::Element { z, .. }) => Some([*z].into_iter().collect()),
AtomExpr::Prim(_) => None,
AtomExpr::Not(_) => None,
AtomExpr::And(parts) => {
let mut acc: Option<std::collections::BTreeSet<u8>> = None;
for p in parts {
if let Some(s) = allowed_elements(p) {
acc = Some(match acc {
None => s,
Some(a) => a.intersection(&s).copied().collect(),
});
}
}
acc
}
AtomExpr::Or(parts) => {
let mut acc = std::collections::BTreeSet::new();
for p in parts {
acc.extend(allowed_elements(p)?);
}
Some(acc)
}
}
}
#[must_use]
pub fn bond_matches(expr: &BondExpr, props: &BondProps) -> bool {
match expr {
BondExpr::Prim(p) => bond_prim_matches(*p, props),
BondExpr::Not(e) => !bond_matches(e, props),
BondExpr::And(parts) => parts.iter().all(|e| bond_matches(e, props)),
BondExpr::Or(parts) => parts.iter().any(|e| bond_matches(e, props)),
}
}
fn bond_prim_matches(p: BondPrim, b: &BondProps) -> bool {
match p {
BondPrim::Any => true,
BondPrim::Single => b.order == BondOrder::Single,
BondPrim::Double => b.order == BondOrder::Double,
BondPrim::Triple => b.order == BondOrder::Triple,
BondPrim::Quadruple => b.order == BondOrder::Quadruple,
BondPrim::Aromatic => b.order == BondOrder::Aromatic,
BondPrim::InRing => b.in_ring,
BondPrim::UpRight | BondPrim::DownRight => {
matches!(b.order, BondOrder::Single | BondOrder::Aromatic)
}
BondPrim::Dative => b.order == BondOrder::Dative && b.dative_forward,
BondPrim::DativeReversed => b.order == BondOrder::Dative && !b.dative_forward,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::smarts;
fn no_recursion(_: &QueryMol) -> bool {
panic!("这条模式含递归 SMARTS,但测试没有提供求值器");
}
fn matches(pat: &str, props: &AtomProps) -> bool {
let q = smarts::parse(pat).unwrap_or_else(|e| panic!("{pat}: {}", e.render()));
atom_matches(&q.atoms[0], props, &mut no_recursion)
}
fn naphthalene_ch() -> AtomProps {
AtomProps {
atomic_num: 6,
aromatic: true,
degree: 2,
total_hs: 1,
implicit_hs: 1,
valence: 4,
ring_count: 1,
min_ring_size: 6,
ring_bonds: 2,
..AtomProps::default()
}
}
fn naphthalene_fusion() -> AtomProps {
AtomProps {
atomic_num: 6,
aromatic: true,
degree: 3,
total_hs: 0,
implicit_hs: 0,
valence: 4,
ring_count: 2,
min_ring_size: 6,
ring_bonds: 3,
..AtomProps::default()
}
}
#[test]
fn primitive_semantics() {
let ch = naphthalene_ch();
let fu = naphthalene_fusion();
assert!(matches("[c]", &ch) && matches("[c]", &fu));
assert!(!matches("[C]", &ch), "大写 C 要求脂肪碳");
assert!(matches("[a]", &ch) && !matches("[A]", &ch));
assert!(matches("[#6]", &ch), "#6 不限芳香性");
assert!(matches("[R1]", &ch) && !matches("[R1]", &fu));
assert!(matches("[R2]", &fu) && !matches("[R2]", &ch));
assert!(
matches("[R]", &ch) && matches("[R]", &fu),
"裸 R = 在任意环中"
);
assert!(matches("[r6]", &ch) && matches("[r6]", &fu));
assert!(!matches("[r5]", &ch));
assert!(matches("[x2]", &ch) && matches("[x3]", &fu));
assert!(matches("[D2]", &ch) && matches("[D3]", &fu));
assert!(matches("[X3]", &ch) && matches("[X3]", &fu), "度 + 总氢");
assert!(matches("[H1]", &ch) && matches("[H0]", &fu));
assert!(matches("[h1]", &ch) && matches("[h0]", &fu));
assert!(matches("[v4]", &ch));
}
#[test]
fn logic_evaluation() {
let ch = naphthalene_ch();
assert!(matches("[c,n]", &ch));
assert!(!matches("[n,o]", &ch));
assert!(matches("[c;R1]", &ch));
assert!(!matches("[c;R2]", &ch));
assert!(matches("[!n]", &ch));
assert!(matches("[!C;!N]", &ch), "既非脂肪碳也非脂肪氮");
assert!(!matches("[c,n;H0]", &ch));
assert!(matches("[c,n&H0]", &ch), "优先级不同,结果就不同");
}
#[test]
fn bare_versus_numbered() {
let acyclic = AtomProps {
atomic_num: 6,
degree: 1,
total_hs: 3,
implicit_hs: 3,
valence: 4,
..AtomProps::default()
};
assert!(!matches("[R]", &acyclic), "不在环中");
assert!(!matches("[r]", &acyclic));
assert!(!matches("[x]", &acyclic));
assert!(matches("[R0]", &acyclic), "R0 = 不属于任何环");
assert!(matches("[D1]", &acyclic) && matches("[X4]", &acyclic));
}
#[test]
fn bond_evaluation() {
let single = BondProps {
order: BondOrder::Single,
..BondProps::default()
};
let ring_double = BondProps {
order: BondOrder::Double,
in_ring: true,
..BondProps::default()
};
let q = |pat: &str| {
let m = smarts::parse(pat).unwrap_or_else(|e| panic!("{pat}: {}", e.render()));
m.bonds[0].clone()
};
assert!(bond_matches(&q("C-C"), &single));
assert!(!bond_matches(&q("C=C"), &single));
assert!(bond_matches(&q("C~C"), &single), "~ 匹配一切");
assert!(bond_matches(&q("C=@C"), &ring_double), "环内双键");
assert!(!bond_matches(&q("C=!@C"), &ring_double));
assert!(
bond_matches(
&q("C=!@C"),
&BondProps {
order: BondOrder::Double,
in_ring: false,
..BondProps::default()
}
),
"非环双键"
);
assert!(bond_matches(&q("CC"), &single));
assert!(bond_matches(
&q("CC"),
&BondProps {
order: BondOrder::Aromatic,
..BondProps::default()
}
));
assert!(!bond_matches(&q("CC"), &ring_double));
let fwd = BondProps {
order: BondOrder::Dative,
dative_forward: true,
..BondProps::default()
};
let rev = BondProps {
dative_forward: false,
..fwd
};
assert!(bond_matches(&q("N->[Cu]"), &fwd));
assert!(!bond_matches(&q("N->[Cu]"), &rev));
assert!(bond_matches(&q("[Cu]<-N"), &rev));
assert!(!bond_matches(&q("[Cu]<-N"), &fwd));
}
#[test]
fn atom_map_is_a_label_not_a_condition() {
let plain = AtomProps {
atomic_num: 6,
..AtomProps::default()
};
let mapped = AtomProps {
atom_map: 7,
..plain
};
assert!(matches("[C:1]", &plain));
assert!(matches("[C:99]", &plain));
assert!(matches("[C:1]", &mapped));
assert!(matches("[C:0]", &mapped));
}
#[test]
fn element_constraint_inference() {
let el = |s: &str| {
let q = smarts::parse(s).unwrap_or_else(|e| panic!("{s}: {}", e.render()));
allowed_elements(&q.atoms[0])
};
assert_eq!(el("[C]"), Some([6].into_iter().collect()));
assert_eq!(el("[c]"), Some([6].into_iter().collect()), "芳香碳也是碳");
assert_eq!(el("[#7]"), Some([7].into_iter().collect()));
assert_eq!(el("[C,N]"), Some([6, 7].into_iter().collect()), "或取并");
assert_eq!(el("[C;H3]"), Some([6].into_iter().collect()), "与取交");
assert_eq!(
el("[C,N;H3]"),
Some([6, 7].into_iter().collect()),
"与的一支没有元素约束时,取另一支的"
);
assert_eq!(el("[#6;#7]"), Some([].into_iter().collect()), "矛盾即空集");
assert_eq!(el("[*]"), None);
assert_eq!(el("[R1]"), None);
assert_eq!(el("[!C]"), None, "补集不推 —— 芳香碳仍可能被 [!C] 接受");
assert_eq!(el("[C,R1]"), None, "或的一支没约束,整体就没有");
assert_eq!(el("[a]"), None);
}
#[test]
fn recursive_goes_through_the_callback() {
let q = smarts::parse("[$(CC)]").unwrap();
let props = AtomProps::default();
let mut called = 0;
{
let mut yes = |sub: &QueryMol| {
called += 1;
assert_eq!(sub.num_atoms(), 2, "子模式应当是 CC");
true
};
assert!(atom_matches(&q.atoms[0], &props, &mut yes));
}
assert_eq!(called, 1, "闭包应当被调用一次");
let mut no = |_: &QueryMol| false;
assert!(!atom_matches(&q.atoms[0], &props, &mut no));
}
}