Skip to main content

chematic_smarts/
query.rs

1//! QueryMolecule: typed representation of a SMARTS pattern graph.
2//!
3//! A `QueryMolecule` is a graph whose nodes carry `AtomQuery` conditions and
4//! whose edges carry `BondQuery` conditions. The VF2 matcher walks this graph
5//! and checks conditions against a target `Molecule`.
6
7/// A single primitive condition on an atom.
8#[derive(Debug, Clone, PartialEq)]
9pub enum AtomPrimitive {
10    /// `[#N]` — matches by atomic number.
11    AtomicNum(u8),
12    /// `C`, `N`, `O`, … — matches by element symbol.
13    Symbol(String),
14    /// `[a]` (true) or `[A]` (false) — aromatic / aliphatic.
15    Aromatic(bool),
16    /// `[+1]`, `[-1]`, … — formal charge.
17    Charge(i8),
18    /// `[H2]`, `[H]` — implicit hydrogen count.
19    HCount(u8),
20    /// `[D3]` — heavy-atom degree (number of bonds).
21    Degree(u8),
22    /// `[R]` (true) or `[!R]` (false) — ring membership.
23    RingMembership(bool),
24    /// `[r5]` — atom is in a ring of exactly N members.
25    RingSize(u8),
26    /// `*` — wildcard; matches any atom.
27    Wildcard,
28}
29
30/// Logical combination of atom primitives.
31#[derive(Debug, Clone, PartialEq)]
32pub enum AtomQuery {
33    Primitive(AtomPrimitive),
34    /// High-precedence AND (`&`) or juxtaposition inside brackets.
35    And(Box<AtomQuery>, Box<AtomQuery>),
36    /// OR (`,`).
37    Or(Box<AtomQuery>, Box<AtomQuery>),
38    /// NOT (`!`).
39    Not(Box<AtomQuery>),
40}
41
42/// A single primitive condition on a bond.
43#[derive(Debug, Clone, PartialEq)]
44pub enum BondPrimitive {
45    /// `-` single bond (also matches `/` and `\` stereo bonds).
46    Single,
47    /// `=` double bond.
48    Double,
49    /// `#` triple bond.
50    Triple,
51    /// `:` aromatic bond.
52    Aromatic,
53    /// `~` any bond.
54    Any,
55    /// `@` ring bond (both endpoints share at least one ring).
56    Ring,
57}
58
59/// Logical combination of bond primitives.
60#[derive(Debug, Clone, PartialEq)]
61pub enum BondQuery {
62    Primitive(BondPrimitive),
63    And(Box<BondQuery>, Box<BondQuery>),
64    Or(Box<BondQuery>, Box<BondQuery>),
65    Not(Box<BondQuery>),
66    /// Implicit bond: no bond specification between two atoms in SMARTS — matches any bond (`~`).
67    Any,
68}
69
70/// A node in a `QueryMolecule` graph.
71#[derive(Debug, Clone)]
72pub struct QueryAtom {
73    pub query: AtomQuery,
74}
75
76/// An edge in a `QueryMolecule` graph.
77#[derive(Debug, Clone)]
78pub struct QueryBond {
79    /// Index into `QueryMolecule::atoms` for one endpoint.
80    pub atom1: usize,
81    /// Index into `QueryMolecule::atoms` for the other endpoint.
82    pub atom2: usize,
83    pub query: BondQuery,
84}
85
86/// A query molecule built from a SMARTS string.
87///
88/// Stores atoms, bonds, and a per-atom adjacency list for fast neighbour lookup
89/// during VF2 matching.
90#[derive(Debug, Clone, Default)]
91pub struct QueryMolecule {
92    pub atoms: Vec<QueryAtom>,
93    pub bonds: Vec<QueryBond>,
94    /// `adj[i]` = `Vec<(bond_idx, neighbour_atom_idx)>`
95    pub adj: Vec<Vec<(usize, usize)>>,
96}
97
98impl QueryMolecule {
99    /// Create an empty `QueryMolecule`.
100    pub fn new() -> Self {
101        Self::default()
102    }
103
104    /// Add an atom with the given query condition. Returns the atom index.
105    pub fn add_atom(&mut self, query: AtomQuery) -> usize {
106        let idx = self.atoms.len();
107        self.atoms.push(QueryAtom { query });
108        self.adj.push(vec![]);
109        idx
110    }
111
112    /// Add an undirected bond between atoms `a` and `b` with the given query condition.
113    pub fn add_bond(&mut self, a: usize, b: usize, query: BondQuery) {
114        let bidx = self.bonds.len();
115        self.bonds.push(QueryBond { atom1: a, atom2: b, query });
116        self.adj[a].push((bidx, b));
117        self.adj[b].push((bidx, a));
118    }
119
120    /// Number of atoms in this query molecule.
121    pub fn atom_count(&self) -> usize {
122        self.atoms.len()
123    }
124}