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}