1use 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
16pub type Groups = BTreeMap<Box<str>, AtomSelection>;
18
19#[derive(Clone, Debug)]
21pub struct Evaluation {
22 pub selection: AtomSelection,
24 pub warnings: Vec<Diagnostic>,
26}
27
28#[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 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 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 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
105struct 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 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 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 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 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 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
463fn 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;