1use crate::ast::{Expr, SameKey};
4use crate::plan::{PhysicalExpr, PhysicalQuery};
5use crate::spatial::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
13mod geometric;
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
105fn narrowed<'a>(
118 atom_local: bool,
119 excludes: bool,
120 universe: &'a AtomSelection,
121 left: &'a AtomSelection,
122) -> &'a AtomSelection {
123 if atom_local || excludes {
124 left
125 } else {
126 universe
127 }
128}
129
130struct Context<'a> {
132 structure: &'a Structure,
133 policy: &'a AnalysisPolicy,
134 groups: &'a Groups,
135 spatial: Option<&'a dyn SpatialResolver>,
136 warnings: Vec<Diagnostic>,
137}
138
139impl Context<'_> {
140 fn eval_physical(
146 &mut self,
147 expr: &PhysicalExpr,
148 universe: &AtomSelection,
149 ) -> Result<AtomSelection, Diagnostic> {
150 match expr {
151 PhysicalExpr::All => Ok(universe.clone()),
152 PhysicalExpr::None => Ok(AtomSelection::Empty),
153 PhysicalExpr::And(left, right) => {
154 let first = self.eval_physical(left, universe)?;
155
156 if first.is_empty() {
157 return Ok(first);
158 }
159
160 let second = self.eval_physical(
161 right,
162 narrowed(
163 crate::plan::atom_local(right),
164 crate::plan::excludes(left),
165 universe,
166 &first,
167 ),
168 )?;
169 Ok(first.intersect(&second))
170 }
171 PhysicalExpr::Or(left, right) => {
172 let left = self.eval_physical(left, universe)?;
173 let right = self.eval_physical(right, universe)?;
174
175 Ok(left.union(&right))
176 }
177 PhysicalExpr::Not(target) => {
178 let selected = self.eval_physical(target, universe)?;
179 Ok(universe.difference(&selected))
180 }
181 PhysicalExpr::ResolvedMembership { column, symbols } => {
182 crate::predicate::membership_symbols(self.structure, universe, *column, symbols)
183 }
184 PhysicalExpr::Logical(logical) => self.eval(logical, universe),
185 }
186 }
187
188 fn eval(&mut self, expr: &Expr, universe: &AtomSelection) -> Result<AtomSelection, Diagnostic> {
193 match expr {
194 Expr::All => Ok(universe.clone()),
195 Expr::None => Ok(AtomSelection::Empty),
196 Expr::And(left, right) => {
197 let first = self.eval(left, universe)?;
198
199 if first.is_empty() {
200 return Ok(first);
201 }
202
203 let second = self.eval(
204 right,
205 narrowed(
206 crate::plan::logical_atom_local(right),
207 crate::plan::logical_excludes(left),
208 universe,
209 &first,
210 ),
211 )?;
212 Ok(first.intersect(&second))
213 }
214 Expr::Or(left, right) => {
215 let left = self.eval(left, universe)?;
216 let right = self.eval(right, universe)?;
217
218 Ok(left.union(&right))
219 }
220 Expr::Not(target) => {
221 let selected = self.eval(target, universe)?;
222 Ok(universe.difference(&selected))
223 }
224 Expr::Global(target) => {
225 let all = AtomSelection::All(self.structure.atom_count());
226 self.eval(target, &all)
227 }
228 Expr::ByResidue(target) => {
229 let selected = self.eval(target, universe)?;
230
231 Ok(crate::expand::same_residue(
232 self.structure,
233 universe,
234 &selected,
235 ))
236 }
237 Expr::Same { key, target } => {
238 let selected = self.eval(target, universe)?;
239 self.same(key, universe, &selected)
240 }
241 Expr::Bonded { depth, target } => {
242 let selected = self.eval(target, universe)?;
243
244 crate::connectivity::bonded(self.structure, universe, &selected, *depth)
245 }
246 Expr::Geometric(geometric) => self.geometric(geometric, universe),
247 Expr::Comparison {
248 column,
249 operator,
250 value,
251 absolute,
252 } => crate::predicate::comparison(
253 self.structure,
254 universe,
255 *column,
256 *operator,
257 *value,
258 *absolute,
259 self.policy,
260 ),
261 Expr::Membership { column, values } => self.membership(*column, values, universe),
262 Expr::Group(name) => Ok(self.group(name, universe)),
263 Expr::Atom {
264 segment,
265 residue,
266 name,
267 } => crate::predicate::atom_selector(
268 self.structure,
269 universe,
270 segment,
271 *residue,
272 name,
273 self.policy,
274 ),
275 Expr::Macro(macro_name) => crate::macros::macro_selection(
276 self.structure,
277 universe,
278 *macro_name,
279 &mut self.warnings,
280 ),
281 Expr::Chirality(configuration) => {
282 crate::macros::chirality_selection(self.structure, universe, configuration)
283 }
284 Expr::Smarts { pattern, .. } => smarts_selection(pattern, self.structure, universe),
285 }
286 }
287
288 fn membership(
291 &mut self,
292 column: crate::ast::Column,
293 values: &[Box<str>],
294 universe: &AtomSelection,
295 ) -> Result<AtomSelection, Diagnostic> {
296 let selected =
297 crate::predicate::membership(self.structure, universe, column, values, self.policy)?;
298 if selected.is_empty() {
299 self.warnings.push(Diagnostic::new(Code::W4003));
300 }
301 Ok(selected)
302 }
303
304 fn group(&mut self, name: &str, universe: &AtomSelection) -> AtomSelection {
307 if let Some(group) = self.groups.get(name) {
308 universe.intersect(group)
309 } else {
310 self.warnings
311 .push(Diagnostic::new(Code::W4003).with_context("group", name.to_string()));
312 AtomSelection::Empty
313 }
314 }
315
316 fn same(
321 &self,
322 key: &SameKey,
323 universe: &AtomSelection,
324 selected: &AtomSelection,
325 ) -> Result<AtomSelection, Diagnostic> {
326 match key {
327 SameKey::Residue => Ok(crate::expand::same_residue(
328 self.structure,
329 universe,
330 selected,
331 )),
332 SameKey::Chain => Ok(crate::expand::same_chain(
333 self.structure,
334 universe,
335 selected,
336 )),
337 SameKey::Model => {
338 if selected.is_empty() {
339 Ok(AtomSelection::Empty)
340 } else {
341 Ok(universe.clone())
342 }
343 }
344 SameKey::Entity => Ok(crate::expand::same_entity(
345 self.structure,
346 universe,
347 selected,
348 )),
349 SameKey::Column(column) => crate::predicate::same_column(
350 self.structure,
351 universe,
352 selected,
353 *column,
354 self.policy,
355 ),
356 SameKey::Fragment => {
357 crate::connectivity::same_fragment(self.structure, universe, selected)
358 }
359 SameKey::Segment => crate::predicate::same_column(
360 self.structure,
361 universe,
362 selected,
363 crate::ast::Column::SegmentId,
364 self.policy,
365 ),
366 }
367 }
368}
369
370fn missing(data: &'static str) -> Diagnostic {
374 Diagnostic::new(Code::E4003).with_context("required", data)
375}
376
377fn missing_smarts_data(error: SmartsDataError) -> Diagnostic {
378 let required = match error {
379 SmartsDataError::Connectivity => "resolved connectivity",
380 SmartsDataError::Aromaticity => "CCD aromaticity",
381 SmartsDataError::FormalCharge => "CCD formal charges",
382 SmartsDataError::Stereochemistry => "CCD stereochemistry",
383 };
384 missing(required)
385}
386
387fn smarts_selection(
388 pattern: &molframe_chem::SmartsPattern,
389 structure: &Structure,
390 universe: &AtomSelection,
391) -> Result<AtomSelection, Diagnostic> {
392 let selected: AtomSelection = pattern
393 .find_structure_matches(structure)
394 .map_err(missing_smarts_data)?
395 .into_iter()
396 .flat_map(|matched| matched.atom_indices.into_vec())
397 .filter_map(|atom| u32::try_from(atom).ok())
398 .collect();
399 Ok(universe.intersect(&selected))
400}
401
402#[cfg(test)]
403#[path = "eval_tests.rs"]
404mod tests;
405
406#[cfg(test)]
407#[path = "eval_universe_tests.rs"]
408mod universe_tests;