use std::collections::HashMap;
use crate::chem::rings::{RingInfo, find_rings};
use crate::system::atomistic::{AtomId, Atomistic};
use crate::system::molgraph::PropValue;
pub struct MolContext<'m> {
pub mol: &'m Atomistic,
pub rings: RingInfo,
aromatic_atom: HashMap<AtomId, bool>,
h_count: HashMap<AtomId, u32>,
degree: HashMap<AtomId, u32>,
ring_bond_count: HashMap<AtomId, u32>,
labels: &'m HashMap<AtomId, String>,
}
static EMPTY_LABELS: std::sync::LazyLock<HashMap<AtomId, String>> =
std::sync::LazyLock::new(HashMap::new);
impl<'m> MolContext<'m> {
pub fn new(mol: &'m Atomistic) -> Self {
Self::with_labels(mol, &EMPTY_LABELS)
}
pub fn with_labels(mol: &'m Atomistic, labels: &'m HashMap<AtomId, String>) -> Self {
let rings = find_rings(mol);
let mut aromatic_atom = HashMap::new();
let mut h_count = HashMap::new();
let mut degree = HashMap::new();
for (id, atom) in mol.atoms() {
let explicit = match atom.get("is_aromatic") {
Some(PropValue::Int(v)) => Some(*v != 0),
Some(PropValue::F64(v)) => Some(*v != 0.0),
_ => None,
};
let arom = explicit.unwrap_or_else(|| {
mol.neighbor_bonds(id)
.any(|(_, order)| (order - 1.5).abs() < 1e-6)
});
aromatic_atom.insert(id, arom);
let h = mol
.neighbors(id)
.filter(|&nb| {
mol.get_atom(nb)
.is_ok_and(|a| a.get_str("element").is_some_and(element_is_hydrogen))
})
.count() as u32;
h_count.insert(id, h);
degree.insert(id, mol.neighbors(id).count() as u32);
}
let mut ring_bond_count: HashMap<AtomId, u32> =
mol.atoms().map(|(id, _)| (id, 0)).collect();
for (bid, bond) in mol.bonds() {
if rings.is_bond_in_ring(bid) {
for &end in &bond.nodes {
*ring_bond_count.entry(end).or_insert(0) += 1;
}
}
}
Self {
mol,
rings,
aromatic_atom,
h_count,
degree,
ring_bond_count,
labels,
}
}
fn is_aromatic(&self, id: AtomId) -> bool {
self.aromatic_atom.get(&id).copied().unwrap_or(false)
}
fn has_label(&self, id: AtomId, label: &str) -> bool {
self.labels.get(&id).map(String::as_str) == Some(label)
}
fn h_count(&self, id: AtomId) -> u32 {
self.h_count.get(&id).copied().unwrap_or(0)
}
fn degree(&self, id: AtomId) -> u32 {
self.degree.get(&id).copied().unwrap_or(0)
}
fn ring_bond_count(&self, id: AtomId) -> u32 {
self.ring_bond_count.get(&id).copied().unwrap_or(0)
}
}
fn element_is_hydrogen(sym: &str) -> bool {
sym.eq_ignore_ascii_case("h")
}
fn atom_symbol(mol: &Atomistic, id: AtomId) -> String {
mol.get_atom(id)
.ok()
.and_then(|a| a.get_str("element").map(str::to_owned))
.unwrap_or_default()
}
fn atom_charge(mol: &Atomistic, id: AtomId) -> i32 {
match mol
.get_atom(id)
.ok()
.and_then(|a| a.get("formal_charge").cloned())
{
Some(PropValue::Int(v)) => v,
Some(PropValue::F64(v)) => v.round() as i32,
_ => 0,
}
}
#[derive(Debug, Clone)]
pub enum AtomPrimitive {
Any,
AnyAromatic,
AnyAliphatic,
AliphaticElement(u8),
AromaticElement(u8),
AtomicNum(u8),
TotalH(u32),
TotalConnections(u32),
Degree(u32),
RingMembership(Option<u32>),
RingSize(Option<u32>),
RingSizeRange { lo: u32, hi: Option<u32> },
RingBondCount(u32),
Charge(i32),
HasContextLabel(String),
}
#[derive(Debug, Clone)]
pub enum AtomQuery {
Prim(AtomPrimitive),
Recursive(usize),
Not(Box<AtomQuery>),
And(Vec<AtomQuery>),
Or(Vec<AtomQuery>),
}
impl AtomPrimitive {
fn eval(&self, ctx: &MolContext, id: AtomId) -> bool {
let mol = ctx.mol;
match self {
AtomPrimitive::Any => true,
AtomPrimitive::AnyAromatic => ctx.is_aromatic(id),
AtomPrimitive::AnyAliphatic => !ctx.is_aromatic(id),
AtomPrimitive::AliphaticElement(z) => {
!ctx.is_aromatic(id) && symbol_matches_z(&atom_symbol(mol, id), *z)
}
AtomPrimitive::AromaticElement(z) => {
ctx.is_aromatic(id) && symbol_matches_z(&atom_symbol(mol, id), *z)
}
AtomPrimitive::AtomicNum(z) => symbol_matches_z(&atom_symbol(mol, id), *z),
AtomPrimitive::TotalH(n) => ctx.h_count(id) == *n,
AtomPrimitive::TotalConnections(n) => ctx.degree(id) == *n,
AtomPrimitive::Degree(n) => ctx.degree(id) == *n,
AtomPrimitive::RingMembership(None) => ctx.rings.is_atom_in_ring(id),
AtomPrimitive::RingMembership(Some(n)) => ctx.rings.num_atom_rings(id) as u32 == *n,
AtomPrimitive::RingSize(None) => ctx.rings.is_atom_in_ring(id),
AtomPrimitive::RingSize(Some(n)) => {
ctx.rings.smallest_ring_containing_atom(id) == Some(*n as usize)
}
AtomPrimitive::RingSizeRange { lo, hi } => {
let size = ctx
.rings
.smallest_ring_containing_atom(id)
.map_or(0, |s| s as u32);
size >= *lo && hi.is_none_or(|h| size <= h)
}
AtomPrimitive::RingBondCount(n) => ctx.ring_bond_count(id) == *n,
AtomPrimitive::Charge(c) => atom_charge(mol, id) == *c,
AtomPrimitive::HasContextLabel(label) => ctx.has_label(id, label),
}
}
}
fn symbol_matches_z(sym: &str, z: u8) -> bool {
crate::system::element::Element::by_symbol(sym).map(|e| e.z()) == Some(z)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BondPrimitive {
Single,
Double,
Triple,
Aromatic,
Any,
InRing,
SingleOrAromatic,
}
#[derive(Debug, Clone)]
pub enum BondQuery {
Prim(BondPrimitive),
Not(Box<BondQuery>),
And(Vec<BondQuery>),
Or(Vec<BondQuery>),
}
pub struct BondFacts {
pub order: f64,
pub aromatic: bool,
pub in_ring: bool,
}
impl BondPrimitive {
fn eval(&self, f: &BondFacts) -> bool {
let is_single = !f.aromatic && (f.order - 1.0).abs() < 1e-6;
let is_double = !f.aromatic && (f.order - 2.0).abs() < 1e-6;
let is_triple = !f.aromatic && (f.order - 3.0).abs() < 1e-6;
match self {
BondPrimitive::Single => is_single,
BondPrimitive::Double => is_double,
BondPrimitive::Triple => is_triple,
BondPrimitive::Aromatic => f.aromatic,
BondPrimitive::Any => true,
BondPrimitive::InRing => f.in_ring,
BondPrimitive::SingleOrAromatic => is_single || f.aromatic,
}
}
}
impl BondQuery {
pub fn eval(&self, f: &BondFacts) -> bool {
match self {
BondQuery::Prim(p) => p.eval(f),
BondQuery::Not(inner) => !inner.eval(f),
BondQuery::And(items) => items.iter().all(|q| q.eval(f)),
BondQuery::Or(items) => items.iter().any(|q| q.eval(f)),
}
}
}
pub trait RecursiveEval {
fn eval_recursive(&self, sub_index: usize, ctx: &MolContext, id: AtomId) -> bool;
}
impl AtomQuery {
pub fn eval(&self, ctx: &MolContext, id: AtomId, rec: &dyn RecursiveEval) -> bool {
match self {
AtomQuery::Prim(p) => p.eval(ctx, id),
AtomQuery::Recursive(idx) => rec.eval_recursive(*idx, ctx, id),
AtomQuery::Not(inner) => !inner.eval(ctx, id, rec),
AtomQuery::And(items) => items.iter().all(|q| q.eval(ctx, id, rec)),
AtomQuery::Or(items) => items.iter().any(|q| q.eval(ctx, id, rec)),
}
}
pub fn collect_context_labels(&self, out: &mut Vec<String>) {
match self {
AtomQuery::Prim(AtomPrimitive::HasContextLabel(l)) => out.push(l.clone()),
AtomQuery::Prim(_) | AtomQuery::Recursive(_) => {}
AtomQuery::Not(inner) => inner.collect_context_labels(out),
AtomQuery::And(items) | AtomQuery::Or(items) => {
for q in items {
q.collect_context_labels(out);
}
}
}
}
}