Skip to main content

molframe_query/execution/
eval.rs

1//! Compiled query evaluation over immutable structures.
2
3use crate::ast::{Expr, GeometricExpr, SameKey};
4use crate::plan::{PhysicalExpr, PhysicalQuery};
5use crate::spatial::{GeometricRequest, SpatialResolver};
6use molframe_chem::SmartsDataError;
7use molframe_core::contract::AnalysisPolicy;
8use molframe_core::diagnostic::{Code, Diagnostic};
9use molframe_core::selection::AtomSelection;
10use molframe_core::structure::Structure;
11use std::collections::BTreeMap;
12
13#[path = "eval_query.rs"]
14mod query;
15
16/// Named selections supplied to `group <name>` expressions.
17pub type Groups = BTreeMap<Box<str>, AtomSelection>;
18
19/// A selected atom set and non-fatal findings produced while resolving it.
20#[derive(Clone, Debug)]
21pub struct Evaluation {
22    /// The selected atom indices.
23    pub selection: AtomSelection,
24    /// Ambiguities and absent symbols worth reporting.
25    pub warnings: Vec<Diagnostic>,
26}
27
28/// A parsed and reusable selection plan.
29#[derive(Clone, Debug)]
30pub struct Query {
31    pub(crate) expr: Expr,
32    pub(crate) warnings: Vec<Diagnostic>,
33    pub(crate) source: Box<str>,
34}
35
36impl PhysicalQuery {
37    /// Executes this structure-bound plan over the complete topology.
38    ///
39    /// # Errors
40    ///
41    /// Returns diagnostics when data required by a predicate is unavailable.
42    pub fn evaluate(
43        &self,
44        structure: &Structure,
45        policy: &AnalysisPolicy,
46        groups: &Groups,
47        spatial: Option<&dyn SpatialResolver>,
48    ) -> Result<Evaluation, Vec<Diagnostic>> {
49        self.evaluate_in(
50            structure,
51            &AtomSelection::All(structure.atom_count()),
52            policy,
53            groups,
54            spatial,
55        )
56    }
57
58    /// Executes inside a current view; `global` logical fallbacks can escape it.
59    ///
60    /// # Errors
61    ///
62    /// Returns the same diagnostics as [`PhysicalQuery::evaluate`].
63    pub fn evaluate_in(
64        &self,
65        structure: &Structure,
66        universe: &AtomSelection,
67        policy: &AnalysisPolicy,
68        groups: &Groups,
69        spatial: Option<&dyn SpatialResolver>,
70    ) -> Result<Evaluation, Vec<Diagnostic>> {
71        // The universe's own shape bounds every position it can name, so one
72        // comparison replaces a walk over the atoms it contains. Asking about
73        // each atom instead made every evaluation read the whole structure
74        // before doing any work, which is what an `all` or `none` selection
75        // cannot afford: their evaluation is otherwise constant.
76        let atom_count = structure.atom_count();
77        let bound = universe.position_bound();
78        if bound > atom_count {
79            return Err(vec![
80                Diagnostic::new(Code::E6009).with_context("atom", bound.to_string()),
81            ]);
82        }
83
84        let mut context = Context {
85            structure,
86            policy,
87            groups,
88            spatial,
89            warnings: self.warnings.clone(),
90        };
91
92        let selection = context
93            .eval_physical(&self.expr, universe)
94            .map_err(|finding| vec![finding])?;
95
96        context.warnings.sort_by_key(Diagnostic::code);
97
98        Ok(Evaluation {
99            selection,
100            warnings: context.warnings,
101        })
102    }
103}
104
105/// Immutable evaluation dependencies plus evaluation-local warning state.
106struct Context<'a> {
107    structure: &'a Structure,
108    policy: &'a AnalysisPolicy,
109    groups: &'a Groups,
110    spatial: Option<&'a dyn SpatialResolver>,
111    warnings: Vec<Diagnostic>,
112}
113
114impl Context<'_> {
115    /// Evaluates a lowered physical expression inside `universe`.
116    ///
117    /// Constant branches and pre-resolved memberships avoid falling back to the
118    /// logical evaluator. Allocation is limited to selections produced by
119    /// individual operators.
120    fn eval_physical(
121        &mut self,
122        expr: &PhysicalExpr,
123        universe: &AtomSelection,
124    ) -> Result<AtomSelection, Diagnostic> {
125        match expr {
126            PhysicalExpr::All => Ok(universe.clone()),
127            PhysicalExpr::None => Ok(AtomSelection::Empty),
128            PhysicalExpr::And(left, right) => {
129                let first = self.eval_physical(left, universe)?;
130
131                if first.is_empty() {
132                    return Ok(first);
133                }
134
135                let second = self.eval_physical(right, &first)?;
136                Ok(first.intersect(&second))
137            }
138            PhysicalExpr::Or(left, right) => {
139                let left = self.eval_physical(left, universe)?;
140                let right = self.eval_physical(right, universe)?;
141
142                Ok(left.union(&right))
143            }
144            PhysicalExpr::Not(target) => {
145                let selected = self.eval_physical(target, universe)?;
146                Ok(universe.difference(&selected))
147            }
148            PhysicalExpr::ResolvedMembership { column, symbols } => {
149                crate::predicate::membership_symbols(self.structure, universe, *column, symbols)
150            }
151            PhysicalExpr::Logical(logical) => self.eval(logical, universe),
152        }
153    }
154
155    /// Evaluates a logical expression inside `universe`.
156    ///
157    /// Boolean operations short-circuit empty conjunctions while preserving
158    /// `global` semantics and the original diagnostic behavior.
159    fn eval(&mut self, expr: &Expr, universe: &AtomSelection) -> Result<AtomSelection, Diagnostic> {
160        match expr {
161            Expr::All => Ok(universe.clone()),
162            Expr::None => Ok(AtomSelection::Empty),
163            Expr::And(left, right) => {
164                let first = self.eval(left, universe)?;
165
166                if first.is_empty() {
167                    return Ok(first);
168                }
169
170                let second = self.eval(right, &first)?;
171                Ok(first.intersect(&second))
172            }
173            Expr::Or(left, right) => {
174                let left = self.eval(left, universe)?;
175                let right = self.eval(right, universe)?;
176
177                Ok(left.union(&right))
178            }
179            Expr::Not(target) => {
180                let selected = self.eval(target, universe)?;
181                Ok(universe.difference(&selected))
182            }
183            Expr::Global(target) => {
184                let all = AtomSelection::All(self.structure.atom_count());
185                self.eval(target, &all)
186            }
187            Expr::ByResidue(target) => {
188                let selected = self.eval(target, universe)?;
189
190                Ok(crate::expand::same_residue(
191                    self.structure,
192                    universe,
193                    &selected,
194                ))
195            }
196            Expr::Same { key, target } => {
197                let selected = self.eval(target, universe)?;
198                self.same(key, universe, &selected)
199            }
200            Expr::Bonded { depth, target } => {
201                let selected = self.eval(target, universe)?;
202
203                crate::connectivity::bonded(self.structure, universe, &selected, *depth)
204            }
205            Expr::Geometric(geometric) => self.geometric(geometric, universe),
206            Expr::Comparison {
207                column,
208                operator,
209                value,
210                absolute,
211            } => crate::predicate::comparison(
212                self.structure,
213                universe,
214                *column,
215                *operator,
216                *value,
217                *absolute,
218                self.policy,
219            ),
220            Expr::Membership { column, values } => {
221                let selected = crate::predicate::membership(
222                    self.structure,
223                    universe,
224                    *column,
225                    values,
226                    self.policy,
227                )?;
228
229                if selected.is_empty() {
230                    self.warnings.push(Diagnostic::new(Code::W4003));
231                }
232
233                Ok(selected)
234            }
235            Expr::Group(name) => {
236                if let Some(group) = self.groups.get(name) {
237                    Ok(universe.intersect(group))
238                } else {
239                    self.warnings
240                        .push(Diagnostic::new(Code::W4003).with_context("group", name.to_string()));
241                    Ok(AtomSelection::Empty)
242                }
243            }
244            Expr::Atom {
245                segment,
246                residue,
247                name,
248            } => crate::predicate::atom_selector(
249                self.structure,
250                universe,
251                segment,
252                *residue,
253                name,
254                self.policy,
255            ),
256            Expr::Macro(macro_name) => crate::macros::macro_selection(
257                self.structure,
258                universe,
259                *macro_name,
260                &mut self.warnings,
261            ),
262            Expr::Chirality(configuration) => {
263                crate::macros::chirality_selection(self.structure, universe, configuration)
264            }
265            Expr::Smarts { pattern, .. } => smarts_selection(pattern, self.structure, universe),
266        }
267    }
268
269    /// Expands `selected` according to a `same` relation.
270    ///
271    /// Delegates to specialized hierarchy, connectivity, or column expansion
272    /// implementations while preserving `universe`.
273    fn same(
274        &self,
275        key: &SameKey,
276        universe: &AtomSelection,
277        selected: &AtomSelection,
278    ) -> Result<AtomSelection, Diagnostic> {
279        match key {
280            SameKey::Residue => Ok(crate::expand::same_residue(
281                self.structure,
282                universe,
283                selected,
284            )),
285            SameKey::Chain => Ok(crate::expand::same_chain(
286                self.structure,
287                universe,
288                selected,
289            )),
290            SameKey::Model => {
291                if selected.is_empty() {
292                    Ok(AtomSelection::Empty)
293                } else {
294                    Ok(universe.clone())
295                }
296            }
297            SameKey::Entity => Ok(crate::expand::same_entity(
298                self.structure,
299                universe,
300                selected,
301            )),
302            SameKey::Column(column) => crate::predicate::same_column(
303                self.structure,
304                universe,
305                selected,
306                *column,
307                self.policy,
308            ),
309            SameKey::Fragment => {
310                crate::connectivity::same_fragment(self.structure, universe, selected)
311            }
312            SameKey::Segment => crate::predicate::same_column(
313                self.structure,
314                universe,
315                selected,
316                crate::ast::Column::SegmentId,
317                self.policy,
318            ),
319        }
320    }
321
322    /// Resolves a geometric expression with the configured spatial backend.
323    ///
324    /// Target expressions are evaluated exactly once. Returns `E4003` when no
325    /// spatial resolver is available.
326    fn geometric(
327        &mut self,
328        expr: &GeometricExpr,
329        universe: &AtomSelection,
330    ) -> Result<AtomSelection, Diagnostic> {
331        let Some(spatial) = self.spatial else {
332            return Err(missing("spatial resolver"));
333        };
334
335        match expr {
336            GeometricExpr::Within { radius, target } => {
337                let target = self.eval(target, universe)?;
338
339                spatial.resolve(GeometricRequest::Within {
340                    universe,
341                    target: &target,
342                    radius: *radius,
343                    include_target: true,
344                })
345            }
346            GeometricExpr::Beyond { radius, target } => {
347                let target = self.eval(target, universe)?;
348
349                spatial.resolve(GeometricRequest::Beyond {
350                    universe,
351                    target: &target,
352                    radius: *radius,
353                })
354            }
355            GeometricExpr::Around { radius, target } => {
356                let target = self.eval(target, universe)?;
357
358                spatial.resolve(GeometricRequest::Within {
359                    universe,
360                    target: &target,
361                    radius: *radius,
362                    include_target: false,
363                })
364            }
365            GeometricExpr::SphereZone { radius, target } => {
366                let target = self.eval(target, universe)?;
367
368                spatial.resolve(GeometricRequest::SphereZone {
369                    universe,
370                    target: &target,
371                    radius: *radius,
372                })
373            }
374            GeometricExpr::SphereLayer {
375                inner,
376                outer,
377                target,
378            } => {
379                let target = self.eval(target, universe)?;
380
381                spatial.resolve(GeometricRequest::SphereLayer {
382                    universe,
383                    target: &target,
384                    inner: *inner,
385                    outer: *outer,
386                })
387            }
388            GeometricExpr::IsoLayer {
389                inner,
390                outer,
391                target,
392            } => {
393                let target = self.eval(target, universe)?;
394
395                spatial.resolve(GeometricRequest::IsoLayer {
396                    universe,
397                    target: &target,
398                    inner: *inner,
399                    outer: *outer,
400                })
401            }
402            GeometricExpr::CylinderZone { .. } | GeometricExpr::CylinderLayer { .. } => {
403                self.geometric_cylinder(expr, universe, spatial)
404            }
405            GeometricExpr::Point { point, radius } => spatial.resolve(GeometricRequest::Point {
406                universe,
407                point: *point,
408                radius: *radius,
409            }),
410        }
411    }
412
413    /// Resolves cylinder-specific geometric expressions.
414    ///
415    /// This helper keeps the primary geometric dispatcher compact while
416    /// evaluating the cylinder target exactly once.
417    fn geometric_cylinder(
418        &mut self,
419        expr: &GeometricExpr,
420        universe: &AtomSelection,
421        spatial: &dyn SpatialResolver,
422    ) -> Result<AtomSelection, Diagnostic> {
423        match expr {
424            GeometricExpr::CylinderZone {
425                radius,
426                z_max,
427                z_min,
428                target,
429            } => {
430                let target = self.eval(target, universe)?;
431
432                spatial.resolve(GeometricRequest::CylinderZone {
433                    universe,
434                    target: &target,
435                    radius: *radius,
436                    z_max: *z_max,
437                    z_min: *z_min,
438                })
439            }
440            GeometricExpr::CylinderLayer {
441                inner,
442                outer,
443                z_max,
444                z_min,
445                target,
446            } => {
447                let target = self.eval(target, universe)?;
448
449                spatial.resolve(GeometricRequest::CylinderLayer {
450                    universe,
451                    target: &target,
452                    inner: *inner,
453                    outer: *outer,
454                    z_max: *z_max,
455                    z_min: *z_min,
456                })
457            }
458            _ => Err(Diagnostic::new(Code::E9001)),
459        }
460    }
461}
462
463/// Builds the standard missing-data diagnostic for an evaluation dependency.
464///
465/// Runtime and space are `O(1)` apart from diagnostic-owned context storage.
466fn missing(data: &'static str) -> Diagnostic {
467    Diagnostic::new(Code::E4003).with_context("required", data)
468}
469
470fn missing_smarts_data(error: SmartsDataError) -> Diagnostic {
471    let required = match error {
472        SmartsDataError::Connectivity => "resolved connectivity",
473        SmartsDataError::Aromaticity => "CCD aromaticity",
474        SmartsDataError::FormalCharge => "CCD formal charges",
475        SmartsDataError::Stereochemistry => "CCD stereochemistry",
476    };
477    missing(required)
478}
479
480fn smarts_selection(
481    pattern: &molframe_chem::SmartsPattern,
482    structure: &Structure,
483    universe: &AtomSelection,
484) -> Result<AtomSelection, Diagnostic> {
485    let selected: AtomSelection = pattern
486        .find_structure_matches(structure)
487        .map_err(missing_smarts_data)?
488        .into_iter()
489        .flat_map(|matched| matched.atom_indices.into_vec())
490        .filter_map(|atom| u32::try_from(atom).ok())
491        .collect();
492    Ok(universe.intersect(&selected))
493}
494
495#[cfg(test)]
496#[path = "eval_tests.rs"]
497mod tests;