cosmolkit_core/search/substruct.rs
1//! Subgraph isomorphism matching (VF2) for molecule pattern matching.
2//!
3//! ## RDKit provenance (protocol: dev/source_reproduction_protocol.md)
4//!
5//! This module reproduces RDKit's substructure matching from:
6//! - `third_party/rdkit/Code/GraphMol/Substruct/vf2.hpp` (~682 lines C++)
7//! - `third_party/rdkit/Code/GraphMol/Substruct/SubstructMatch.cpp` (~735 lines C++)
8//!
9//! The VF2 algorithm implementation is adapted from vflib-2.0 by P. Foggia,
10//! extensively modified by Greg Landrum, ported to Rust with depth-based
11//! term_1/term_2 tracking (BackTrack decrements counters instead of
12//! recomputing from scratch).
13//!
14//! ## Marker convention
15//!
16//! Each copied C++ block below uses the two-axis status marker:
17//! - RDKit✔️✔️: fully reproduced behavior and performance
18//! - RDKit✔️❌: functionally correct, but with a known performance gap
19//! - RDKit❗✔️: unfinished behavior that must not be presented as parity
20//! - RDKit❌❌: not yet ported
21
22use crate::search::query::{
23 QueryMatchContext, and_query_match, atom_predicate_matches_with_context, atom_queries_match,
24 bond_predicate_matches_with_context, bond_queries_match, build_query_match_context,
25 or_query_match, xor_query_match,
26};
27use crate::{
28 Atom, AtomQueryPredicate, Bond, BondOrder, BondQueryPredicate, BondStereo, ChiralTag, Molecule,
29 StereoGroupKind,
30};
31use std::collections::{BTreeMap, BTreeSet, HashSet};
32use std::fmt;
33use std::sync::Arc;
34
35// ---------------------------------------------------------------------------
36// Result types
37// ---------------------------------------------------------------------------
38
39/// Result of a single substructure match.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct SubstructMatchResult {
42 /// Mapping from query atom index to molecule atom index.
43 pub atom_mapping: Vec<usize>,
44 /// Mapping from query bond index to molecule bond index.
45 pub bond_mapping: Vec<usize>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
49pub enum SubstructMatchError {
50 #[error(
51 "RDKit substructure matching branch {branch} is unsupported until {rdkit_function} is source-ported"
52 )]
53 Unsupported {
54 branch: &'static str,
55 rdkit_function: &'static str,
56 },
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum SubstructMatchOverload {
61 Molecule,
62 MolBundle,
63 ResonanceMolSupplier,
64 SubstructLibrary,
65}
66
67pub fn check_substruct_match_overload_support(
68 overload: SubstructMatchOverload,
69) -> Result<(), SubstructMatchError> {
70 match overload {
71 SubstructMatchOverload::Molecule => Ok(()),
72 SubstructMatchOverload::MolBundle => Err(SubstructMatchError::Unsupported {
73 branch: "MolBundle substructure-match overloads",
74 rdkit_function: "SubstructMatch(MolBundle, ROMol/MolBundle, params)",
75 }),
76 SubstructMatchOverload::ResonanceMolSupplier => Err(SubstructMatchError::Unsupported {
77 branch: "resonance substructure-match overload",
78 rdkit_function: "SubstructMatch(ResonanceMolSupplier, ROMol, params)",
79 }),
80 SubstructMatchOverload::SubstructLibrary => Err(SubstructMatchError::Unsupported {
81 branch: "SubstructLibrary search overloads",
82 rdkit_function: "SubstructLibrary::getMatches/hasMatch/countMatches",
83 }),
84 }
85}
86
87#[derive(Debug, thiserror::Error)]
88pub enum SubstructMatchParamsJsonError {
89 #[error("invalid substructure match parameter JSON: {0}")]
90 InvalidJson(#[from] serde_json::Error),
91 #[error("invalid JSON value for substructure match parameter '{field}'")]
92 InvalidField { field: &'static str },
93}
94
95type SubstructMatchResultList = Result<Vec<SubstructMatchResult>, SubstructMatchError>;
96
97/// Parameters controlling substructure matching behaviour.
98pub type ExtraAtomCheck = Arc<dyn Fn(&Molecule, &Atom, &Molecule, &Atom) -> bool + Send + Sync>;
99pub type ExtraBondCheck = Arc<dyn Fn(&Bond, &Bond) -> bool + Send + Sync>;
100pub type ExtraFinalCheck = Arc<dyn Fn(&Molecule, &[usize]) -> bool + Send + Sync>;
101
102#[derive(Debug, Clone, Copy, PartialEq)]
103pub struct AtomCoordsMatchFunctor {
104 pub ref_conf_id: i32,
105 pub query_conf_id: i32,
106 pub tol2: f64,
107}
108
109impl AtomCoordsMatchFunctor {
110 #[must_use]
111 pub fn new(ref_conf_id: i32, query_conf_id: i32, tolerance: f64) -> Self {
112 Self {
113 ref_conf_id,
114 query_conf_id,
115 tol2: tolerance * tolerance,
116 }
117 }
118
119 #[must_use]
120 pub fn matches(
121 &self,
122 query_mol: &Molecule,
123 query_atom: &Atom,
124 target_mol: &Molecule,
125 target_atom: &Atom,
126 ) -> bool {
127 // RDKit✔️✔️: bool AtomCoordsMatchFunctor::operator()(const Atom &queryAtom,
128 // RDKit✔️✔️: const Atom &targetAtom) const {
129 // RDKit✔️✔️: if (!queryAtom.getOwningMol().getNumConformers() ||
130 // RDKit✔️✔️: !targetAtom.getOwningMol().getNumConformers()) {
131 // RDKit✔️✔️: return false;
132 // RDKit✔️✔️: }
133 // RDKit✔️✔️: const auto &queryPos = queryAtom.getOwningMol()
134 // RDKit✔️✔️: .getConformer(d_queryConfId)
135 // RDKit✔️✔️: .getAtomPos(queryAtom.getIdx());
136 // RDKit✔️✔️: const auto &targetPos = targetAtom.getOwningMol()
137 // RDKit✔️✔️: .getConformer(d_refConfId)
138 // RDKit✔️✔️: .getAtomPos(targetAtom.getIdx());
139 // RDKit✔️✔️: return (queryPos - targetPos).lengthSq() <= d_tol2;
140 // RDKit✔️✔️: };
141 // Complexity review: both versions select two conformers, index two
142 // coordinate rows, and compare three squared deltas in O(1) after the
143 // conformer-id lookup. No coordinate data is cloned or allocated.
144 fn conformer(molecule: &Molecule, id: i32) -> Option<&crate::Conformer3D> {
145 if id < 0 {
146 molecule.conformers_3d().first()
147 } else {
148 molecule
149 .conformers_3d()
150 .iter()
151 .find(|conformer| conformer.id() == id as usize)
152 }
153 }
154
155 let Some(query_conformer) = conformer(query_mol, self.query_conf_id) else {
156 return false;
157 };
158 let Some(target_conformer) = conformer(target_mol, self.ref_conf_id) else {
159 return false;
160 };
161 let Some(query_position) = query_conformer.coordinates().get(query_atom.id().index())
162 else {
163 return false;
164 };
165 let Some(target_position) = target_conformer.coordinates().get(target_atom.id().index())
166 else {
167 return false;
168 };
169 query_position
170 .iter()
171 .zip(target_position)
172 .map(|(query, target)| {
173 let delta = query - target;
174 delta * delta
175 })
176 .sum::<f64>()
177 <= self.tol2
178 }
179}
180
181impl Default for AtomCoordsMatchFunctor {
182 fn default() -> Self {
183 Self::new(-1, -1, 1e-4)
184 }
185}
186
187#[derive(Clone)]
188pub struct SubstructMatchParams {
189 /// Maximum number of matches to return (default: 1000).
190 pub max_matches: usize,
191 /// Whether to uniquify results (default: true).
192 pub uniquify: bool,
193 /// Whether atom/bond stereochemistry participates in matching.
194 pub use_chirality: bool,
195 /// Whether enhanced stereo groups participate in final matching.
196 pub use_enhanced_stereo: bool,
197 /// Whether specified query stereo may match unspecified molecule stereo.
198 pub specified_stereo_query_matches_unspecified: bool,
199 /// Whether two query atoms are compared as query trees.
200 pub use_query_query_matches: bool,
201 /// Whether recursive query nodes may be evaluated.
202 pub recursion_possible: bool,
203 /// Maximum matches used while evaluating recursive query nodes.
204 pub max_recursive_matches: usize,
205 /// Requested matcher thread count; matching is currently single-threaded.
206 pub num_threads: i32,
207 /// Whether aromatic bonds may match conjugated single or double bonds.
208 pub aromatic_matches_conjugated: bool,
209 /// Whether aromatic bonds may match any single or double bond.
210 pub aromatic_matches_single_or_double: bool,
211 /// Atom property names that must have equal string values on both atoms.
212 pub atom_properties: Vec<String>,
213 /// Bond property names that must have equal string values on both bonds.
214 pub bond_properties: Vec<String>,
215 /// Optional caller-provided atom compatibility check.
216 pub extra_atom_check: Option<ExtraAtomCheck>,
217 /// Whether `extra_atom_check` replaces the default atom comparison.
218 pub extra_atom_check_overrides_default_check: bool,
219 /// Optional caller-provided bond compatibility check.
220 pub extra_bond_check: Option<ExtraBondCheck>,
221 /// Whether `extra_bond_check` replaces the default bond comparison.
222 pub extra_bond_check_overrides_default_check: bool,
223 /// Whether generic-group labels participate in final matching.
224 pub use_generic_matchers: bool,
225 /// Optional caller-provided final check over target atom indices.
226 pub extra_final_check: Option<ExtraFinalCheck>,
227}
228
229impl fmt::Debug for SubstructMatchParams {
230 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
231 formatter
232 .debug_struct("SubstructMatchParams")
233 .field("max_matches", &self.max_matches)
234 .field("uniquify", &self.uniquify)
235 .field("use_chirality", &self.use_chirality)
236 .field("use_enhanced_stereo", &self.use_enhanced_stereo)
237 .field(
238 "specified_stereo_query_matches_unspecified",
239 &self.specified_stereo_query_matches_unspecified,
240 )
241 .field("use_query_query_matches", &self.use_query_query_matches)
242 .field("recursion_possible", &self.recursion_possible)
243 .field("max_recursive_matches", &self.max_recursive_matches)
244 .field("num_threads", &self.num_threads)
245 .field(
246 "aromatic_matches_conjugated",
247 &self.aromatic_matches_conjugated,
248 )
249 .field(
250 "aromatic_matches_single_or_double",
251 &self.aromatic_matches_single_or_double,
252 )
253 .field("atom_properties", &self.atom_properties)
254 .field("bond_properties", &self.bond_properties)
255 .field("extra_atom_check", &self.extra_atom_check.is_some())
256 .field(
257 "extra_atom_check_overrides_default_check",
258 &self.extra_atom_check_overrides_default_check,
259 )
260 .field("extra_bond_check", &self.extra_bond_check.is_some())
261 .field(
262 "extra_bond_check_overrides_default_check",
263 &self.extra_bond_check_overrides_default_check,
264 )
265 .field("use_generic_matchers", &self.use_generic_matchers)
266 .field("extra_final_check", &self.extra_final_check.is_some())
267 .finish()
268 }
269}
270
271#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
272enum RecursiveQueryCacheKey {
273 Serial(u32),
274 OwnedQuery(usize),
275}
276
277type RecursiveQueryMatchCache = BTreeMap<RecursiveQueryCacheKey, Vec<bool>>;
278
279struct RecursiveLocker {
280 cache: RecursiveQueryMatchCache,
281}
282
283impl RecursiveLocker {
284 fn new(query: &Molecule, recursion_possible: bool) -> Self {
285 // RDKit✔️🔝: RecursiveLocker(const ROMol &query, const bool recursionPossible) {
286 // RDKit✔️🔝: if (recursionPossible) {
287 // RDKit✔️🔝: locked.reserve(query.getNumAtoms());
288 // RDKit✔️🔝: }
289 // RDKit✔️🔝: }
290 // Rust keeps recursive match state in this call-local cache instead of
291 // mutating and locking query nodes. This preserves the source lifetime
292 // semantics while avoiding the O(query atoms) pointer-vector reserve
293 // and every mutex operation. The query and flag remain inputs here so
294 // this constructor is the canonical source boundary.
295 let _ = (query, recursion_possible);
296 Self {
297 cache: RecursiveQueryMatchCache::new(),
298 }
299 }
300}
301
302impl Drop for RecursiveLocker {
303 fn drop(&mut self) {
304 // RDKit✔️✔️: ~RecursiveLocker() {
305 // RDKit✔️✔️: for (auto v : locked) {
306 // RDKit✔️✔️: v->clear();
307 // RDKit✔️✔️: #ifdef RDK_BUILD_THREADSAFE_SSS
308 // RDKit✔️✔️: v->d_mutex.unlock();
309 // RDKit✔️✔️: #endif
310 // RDKit✔️✔️: }
311 // RDKit✔️✔️: }
312 // Complexity review: dropping the call-local cache clears each stored
313 // atom-membership vector once, matching RDKit's linear clear pass.
314 // No unlock is required because immutable query nodes are never shared
315 // mutably; ownership enforces the same cleanup on every return path.
316 self.cache.clear();
317 }
318}
319
320fn recursive_query_cache_key(
321 query: &crate::search::query::RecursiveStructureQuery,
322) -> RecursiveQueryCacheKey {
323 if query.serial_number() != 0 {
324 RecursiveQueryCacheKey::Serial(query.serial_number())
325 } else {
326 RecursiveQueryCacheKey::OwnedQuery(query as *const _ as usize)
327 }
328}
329
330impl Default for SubstructMatchParams {
331 fn default() -> Self {
332 // RDKit✔️✔️: bool useChirality = false; //!< Use chirality in determining whether or not
333 // RDKit✔️✔️: //!< atoms/bonds match
334 // RDKit✔️✔️: bool uniquify = true; //!< uniquify (by atom index) match results
335 // RDKit✔️✔️: unsigned int maxMatches = 1000; //!< maximum number of matches to return
336 // RDKit✔️✔️: bool specifiedStereoQueryMatchesUnspecified =
337 // RDKit✔️✔️: false; //!< If set, query atoms and bonds with specified stereochemistry
338 // RDKit✔️✔️: //!< will match atoms and bonds with unspecified stereochemistry
339 // RDKit✔️✔️: bool useEnhancedStereo = false;
340 // RDKit✔️✔️: bool aromaticMatchesConjugated = false;
341 // RDKit✔️✔️: bool useQueryQueryMatches = false;
342 // RDKit✔️✔️: bool useGenericMatchers = false;
343 // RDKit✔️✔️: bool recursionPossible = true;
344 // RDKit✔️✔️: int numThreads = 1;
345 // RDKit✔️✔️: std::vector<std::string> atomProperties;
346 // RDKit✔️✔️: std::vector<std::string> bondProperties;
347 // RDKit✔️✔️: std::function<bool(const ROMol &, std::span<const unsigned int>)>
348 // RDKit✔️✔️: extraFinalCheck;
349 // RDKit✔️✔️: unsigned int maxRecursiveMatches = 1000;
350 // RDKit✔️✔️: bool aromaticMatchesSingleOrDouble = false;
351 // RDKit✔️✔️: std::function<bool(const Atom &, const Atom &)> extraAtomCheck;
352 // RDKit✔️✔️: bool extraAtomCheckOverridesDefaultCheck = false;
353 // RDKit✔️✔️: std::function<bool(const Bond &, const Bond &)> extraBondCheck;
354 // RDKit✔️✔️: bool extraBondCheckOverridesDefaultCheck = false;
355 // Complexity review: initialization is O(1) and allocates only empty
356 // Vec headers and absent callback slots, matching the C++ defaults.
357 Self {
358 max_matches: 1000,
359 uniquify: true,
360 use_chirality: false,
361 use_enhanced_stereo: false,
362 specified_stereo_query_matches_unspecified: false,
363 use_query_query_matches: false,
364 recursion_possible: true,
365 max_recursive_matches: 1000,
366 num_threads: 1,
367 aromatic_matches_conjugated: false,
368 aromatic_matches_single_or_double: false,
369 atom_properties: Vec::new(),
370 bond_properties: Vec::new(),
371 extra_atom_check: None,
372 extra_atom_check_overrides_default_check: false,
373 extra_bond_check: None,
374 extra_bond_check_overrides_default_check: false,
375 use_generic_matchers: false,
376 extra_final_check: None,
377 }
378 }
379}
380
381fn json_param_bool(
382 object: &serde_json::Map<String, serde_json::Value>,
383 field: &'static str,
384) -> Result<Option<bool>, SubstructMatchParamsJsonError> {
385 let Some(value) = object.get(field) else {
386 return Ok(None);
387 };
388 match value {
389 serde_json::Value::Bool(value) => Ok(Some(*value)),
390 serde_json::Value::String(value) if value == "true" || value == "1" => Ok(Some(true)),
391 serde_json::Value::String(value) if value == "false" || value == "0" => Ok(Some(false)),
392 _ => Err(SubstructMatchParamsJsonError::InvalidField { field }),
393 }
394}
395
396fn json_param_usize(
397 object: &serde_json::Map<String, serde_json::Value>,
398 field: &'static str,
399) -> Result<Option<usize>, SubstructMatchParamsJsonError> {
400 let Some(value) = object.get(field) else {
401 return Ok(None);
402 };
403 let parsed = match value {
404 serde_json::Value::Number(value) => {
405 value.as_u64().and_then(|value| usize::try_from(value).ok())
406 }
407 serde_json::Value::String(value) => value.parse().ok(),
408 _ => None,
409 };
410 parsed
411 .map(Some)
412 .ok_or(SubstructMatchParamsJsonError::InvalidField { field })
413}
414
415fn json_param_i32(
416 object: &serde_json::Map<String, serde_json::Value>,
417 field: &'static str,
418) -> Result<Option<i32>, SubstructMatchParamsJsonError> {
419 let Some(value) = object.get(field) else {
420 return Ok(None);
421 };
422 let parsed = match value {
423 serde_json::Value::Number(value) => {
424 value.as_i64().and_then(|value| i32::try_from(value).ok())
425 }
426 serde_json::Value::String(value) => value.parse().ok(),
427 _ => None,
428 };
429 parsed
430 .map(Some)
431 .ok_or(SubstructMatchParamsJsonError::InvalidField { field })
432}
433
434pub fn update_substruct_match_params_from_json(
435 params: &mut SubstructMatchParams,
436 json: &str,
437) -> Result<(), SubstructMatchParamsJsonError> {
438 // BEGIN RDKIT CPP FUNCTION: third_party/rdkit/Code/GraphMol/Substruct/SubstructUtils.cpp :: updateSubstructMatchParamsFromJSON
439 // RDKit✔️✔️: void updateSubstructMatchParamsFromJSON(SubstructMatchParameters ¶ms,
440 // RDKit✔️✔️: const std::string &json) {
441 // RDKit✔️✔️: if (json.empty()) {
442 // RDKit✔️✔️: return;
443 // RDKit✔️✔️: }
444 // RDKit✔️✔️: std::istringstream ss;
445 // RDKit✔️✔️: ss.str(json);
446 // RDKit✔️✔️: boost::property_tree::ptree pt;
447 // RDKit✔️✔️: boost::property_tree::read_json(ss, pt);
448 // RDKit✔️✔️: PT_OPT_GET(useChirality);
449 // RDKit✔️✔️: PT_OPT_GET(useEnhancedStereo);
450 // RDKit✔️✔️: PT_OPT_GET(aromaticMatchesConjugated);
451 // RDKit✔️✔️: PT_OPT_GET(useQueryQueryMatches);
452 // RDKit✔️✔️: PT_OPT_GET(recursionPossible);
453 // RDKit✔️✔️: PT_OPT_GET(uniquify);
454 // RDKit✔️✔️: PT_OPT_GET(maxMatches);
455 // RDKit✔️✔️: PT_OPT_GET(maxRecursiveMatches);
456 // RDKit✔️✔️: PT_OPT_GET(numThreads);
457 // RDKit✔️✔️: PT_OPT_GET(specifiedStereoQueryMatchesUnspecified);
458 // RDKit✔️✔️: PT_OPT_GET(aromaticMatchesSingleOrDouble);
459 // RDKit✔️✔️: }
460 // END RDKIT CPP FUNCTION
461 //
462 // Local complexity review: both parsers are O(input length), followed by
463 // eleven expected O(1) object lookups. No molecule/query data is touched
464 // or cloned. Staging prevents partial mutation on malformed input.
465 if json.is_empty() {
466 return Ok(());
467 }
468 let value: serde_json::Value = serde_json::from_str(json)?;
469 let object = value
470 .as_object()
471 .ok_or(SubstructMatchParamsJsonError::InvalidField { field: "root" })?;
472 let use_chirality = json_param_bool(object, "useChirality")?;
473 let use_enhanced_stereo = json_param_bool(object, "useEnhancedStereo")?;
474 let aromatic_matches_conjugated = json_param_bool(object, "aromaticMatchesConjugated")?;
475 let use_query_query_matches = json_param_bool(object, "useQueryQueryMatches")?;
476 let recursion_possible = json_param_bool(object, "recursionPossible")?;
477 let uniquify = json_param_bool(object, "uniquify")?;
478 let max_matches = json_param_usize(object, "maxMatches")?;
479 let max_recursive_matches = json_param_usize(object, "maxRecursiveMatches")?;
480 let num_threads = json_param_i32(object, "numThreads")?;
481 let specified_stereo = json_param_bool(object, "specifiedStereoQueryMatchesUnspecified")?;
482 let aromatic_matches_single_or_double =
483 json_param_bool(object, "aromaticMatchesSingleOrDouble")?;
484
485 if let Some(value) = use_chirality {
486 params.use_chirality = value;
487 }
488 if let Some(value) = use_enhanced_stereo {
489 params.use_enhanced_stereo = value;
490 }
491 if let Some(value) = aromatic_matches_conjugated {
492 params.aromatic_matches_conjugated = value;
493 }
494 if let Some(value) = use_query_query_matches {
495 params.use_query_query_matches = value;
496 }
497 if let Some(value) = recursion_possible {
498 params.recursion_possible = value;
499 }
500 if let Some(value) = uniquify {
501 params.uniquify = value;
502 }
503 if let Some(value) = max_matches {
504 params.max_matches = value;
505 }
506 if let Some(value) = max_recursive_matches {
507 params.max_recursive_matches = value;
508 }
509 if let Some(value) = num_threads {
510 params.num_threads = value;
511 }
512 if let Some(value) = specified_stereo {
513 params.specified_stereo_query_matches_unspecified = value;
514 }
515 if let Some(value) = aromatic_matches_single_or_double {
516 params.aromatic_matches_single_or_double = value;
517 }
518 Ok(())
519}
520
521pub fn substruct_match_params_to_json(params: &SubstructMatchParams) -> String {
522 // BEGIN RDKIT CPP FUNCTION: third_party/rdkit/Code/GraphMol/Substruct/SubstructUtils.cpp :: substructMatchParamsToJSON
523 // RDKit✔️✔️: std::string substructMatchParamsToJSON(const SubstructMatchParameters ¶ms) {
524 // RDKit✔️✔️: boost::property_tree::ptree pt;
525 // RDKit✔️✔️:
526 // RDKit✔️✔️: PT_OPT_PUT(useChirality);
527 // RDKit✔️✔️: PT_OPT_PUT(useEnhancedStereo);
528 // RDKit✔️✔️: PT_OPT_PUT(aromaticMatchesConjugated);
529 // RDKit✔️✔️: PT_OPT_PUT(useQueryQueryMatches);
530 // RDKit✔️✔️: PT_OPT_PUT(recursionPossible);
531 // RDKit✔️✔️: PT_OPT_PUT(uniquify);
532 // RDKit✔️✔️: PT_OPT_PUT(maxMatches);
533 // RDKit✔️✔️: PT_OPT_PUT(maxRecursiveMatches);
534 // RDKit✔️✔️: PT_OPT_PUT(numThreads);
535 // RDKit✔️✔️: PT_OPT_PUT(specifiedStereoQueryMatchesUnspecified);
536 // RDKit✔️✔️: PT_OPT_PUT(aromaticMatchesSingleOrDouble);
537 // RDKit✔️✔️:
538 // RDKit✔️✔️: std::stringstream ss;
539 // RDKit✔️✔️: boost::property_tree::json_parser::write_json(ss, pt);
540 // RDKit✔️✔️: return ss.str();
541 // RDKit✔️✔️: }
542 // END RDKIT CPP FUNCTION
543 //
544 // Local complexity review: both implementations serialize the same fixed
545 // eleven scalar fields in O(output length), without molecule/query work.
546 let fields = serde_json::json!({
547 "useChirality": params.use_chirality.to_string(),
548 "useEnhancedStereo": params.use_enhanced_stereo.to_string(),
549 "aromaticMatchesConjugated": params.aromatic_matches_conjugated.to_string(),
550 "useQueryQueryMatches": params.use_query_query_matches.to_string(),
551 "recursionPossible": params.recursion_possible.to_string(),
552 "uniquify": params.uniquify.to_string(),
553 "maxMatches": params.max_matches.to_string(),
554 "maxRecursiveMatches": params.max_recursive_matches.to_string(),
555 "numThreads": params.num_threads.to_string(),
556 "specifiedStereoQueryMatchesUnspecified": params.specified_stereo_query_matches_unspecified.to_string(),
557 "aromaticMatchesSingleOrDouble": params.aromatic_matches_single_or_double.to_string(),
558 });
559 serde_json::to_string_pretty(&fields).expect("fixed scalar JSON serialization cannot fail")
560 + "\n"
561}
562
563// ---------------------------------------------------------------------------
564// Internal minimum-degree graph representation
565// ---------------------------------------------------------------------------
566
567/// Minimal adjacency info needed for VF2.
568///
569/// `nbrs[i]` is a slice into `edges`.
570#[derive(Debug, Clone)]
571struct Vf2Graph {
572 n_atoms: usize,
573 n_bonds: usize,
574 /// Canonical source/target endpoints indexed by bond id.
575 edge_endpoints: Vec<(usize, usize)>,
576 /// For each atom index, the neighbor indices and bond ids.
577 adjacency: Vec<Vec<(usize, usize)>>, // (neighbor_atom_index, bond_index)
578}
579
580/// Build a VF2-compatible adjacency view from a molecule.
581///
582/// The C++ code iterates `out_edges` via Boost graph. We pre-build adjacency
583/// once and use raw index lookups.
584fn build_vf2_graph(mol: &Molecule) -> Vf2Graph {
585 // RDKit source (implicit in vf2.hpp usage of out_edges):
586 // The VF2 state stores Graph *g1, *g2 and calls:
587 // boost::out_edges(node, *g)
588 // boost::out_degree(node, *g)
589 // boost::adjacent_vertices(node, *g)
590 // These are all O(1) in Boost adjacency_list.
591 //
592 // RDKit✔️❌: We build a flat adjacency Vec<(usize, usize)> per atom.
593 // This adds a one-time O(V+E) allocation vs the Boost inline storage,
594 // but lookups are O(degree) which matches the original hot-path cost.
595 let n_atoms = mol.num_atoms();
596 let mut adjacency: Vec<Vec<(usize, usize)>> = vec![Vec::new(); n_atoms];
597 let mut edge_endpoints = Vec::with_capacity(mol.num_bonds());
598 for (bond_idx, bond) in mol.bonds().iter().enumerate() {
599 let b = bond.begin().index();
600 let e = bond.end().index();
601 edge_endpoints.push((b, e));
602 adjacency[b].push((e, bond_idx));
603 adjacency[e].push((b, bond_idx));
604 }
605 Vf2Graph {
606 n_atoms,
607 n_bonds: mol.num_bonds(),
608 edge_endpoints,
609 adjacency,
610 }
611}
612
613fn get_other_idx(g: &Vf2Graph, edge: usize, vertex: NodeId) -> NodeId {
614 // RDKit✔️✔️: template <class Graph, class VertexDescr, class EdgeDescr>
615 // RDKit✔️✔️: VertexDescr getOtherIdx(const Graph &g, const EdgeDescr &edge,
616 // RDKit✔️✔️: const VertexDescr &vertex) {
617 // RDKit✔️✔️: VertexDescr tmp = boost::source(edge, g);
618 // RDKit✔️✔️: if (tmp == vertex) {
619 // RDKit✔️✔️: tmp = boost::target(edge, g);
620 // RDKit✔️✔️: }
621 // RDKit✔️✔️: return tmp;
622 // RDKit✔️✔️: }
623 // Complexity review: the endpoint table provides the same O(1) source and
624 // target lookup as the Boost edge descriptor, with no per-call allocation.
625 let (source, target) = g.edge_endpoints[edge];
626 if source == vertex { target } else { source }
627}
628
629impl Vf2Graph {
630 fn out_degree(&self, node: usize) -> usize {
631 self.adjacency[node].len()
632 }
633
634 fn out_edges(&self, node: usize) -> &[(usize, usize)] {
635 &self.adjacency[node]
636 }
637}
638
639// ---------------------------------------------------------------------------
640// Atom and bond matching functors
641// ---------------------------------------------------------------------------
642
643fn property_compat(
644 properties1: &BTreeMap<String, String>,
645 properties2: &BTreeMap<String, String>,
646 properties: &[String],
647) -> bool {
648 // RDKit✔️🔝: bool propertyCompat(const RDProps *r1, const RDProps *r2,
649 // RDKit✔️🔝: const std::vector<std::string> &properties) {
650 // RDKit✔️🔝: PRECONDITION(r1, "bad RDProps");
651 // RDKit✔️🔝: PRECONDITION(r2, "bad RDProps");
652 // RDKit✔️🔝:
653 // RDKit✔️🔝: for (const auto &prop : properties) {
654 // RDKit✔️🔝: std::string prop1;
655 // RDKit✔️🔝: bool hasprop1 = r1->getPropIfPresent<std::string>(prop, prop1);
656 // RDKit✔️🔝: std::string prop2;
657 // RDKit✔️🔝: bool hasprop2 = r2->getPropIfPresent<std::string>(prop, prop2);
658 // RDKit✔️🔝: if (hasprop1 && hasprop2) {
659 // RDKit✔️🔝: if (prop1 != prop2) {
660 // RDKit✔️🔝: return false;
661 // RDKit✔️🔝: }
662 // RDKit✔️🔝: } else if (hasprop1 || hasprop2) {
663 // RDKit✔️🔝: // only one has the property
664 // RDKit✔️🔝: return false;
665 // RDKit✔️🔝: }
666 // RDKit✔️🔝: }
667 // RDKit✔️🔝: return true;
668 // RDKit✔️🔝: }
669 //
670 // Typed references make both source pointer preconditions
671 // unrepresentable. COSMolKit's canonical atom/bond property maps store
672 // only strings, exactly the type requested by the source function, so a
673 // pair of Option<&String> values preserves the source's present/missing
674 // cases without temporary string copies. Local complexity review: both
675 // implementations scan the requested property list once and short-circuit
676 // at the first mismatch without cloning or allocating. RDKit's Dict scans
677 // its vector of entries for each lookup (O(P*N)); BTreeMap lookup is
678 // O(log N), making this O(P*log N) while preserving lookup semantics.
679 for property in properties {
680 if properties1.get(property) != properties2.get(property) {
681 return false;
682 }
683 }
684 true
685}
686
687// RDKit source (SubstructMatch.cpp):
688// class AtomLabelFunctor {
689// public:
690// AtomLabelFunctor(const ROMol &query, const ROMol &mol,
691// const SubstructMatchParameters &ps)
692// : d_query(query), d_mol(mol), d_params(ps) {};
693// bool operator()(unsigned int i, unsigned int j) const {
694// bool res = false;
695// if (d_params.useChirality) {
696// const Atom *qAt = d_query.getAtomWithIdx(i);
697// if (qAt->getChiralTag() == Atom::CHI_TETRAHEDRAL_CW ||
698// qAt->getChiralTag() == Atom::CHI_TETRAHEDRAL_CCW) {
699// const Atom *mAt = d_mol.getAtomWithIdx(j);
700// if (!d_params.specifiedStereoQueryMatchesUnspecified &&
701// mAt->getChiralTag() != Atom::CHI_TETRAHEDRAL_CW &&
702// mAt->getChiralTag() != Atom::CHI_TETRAHEDRAL_CCW) {
703// return false;
704// }
705// }
706// }
707// res = atomCompat(d_query[i], d_mol[j], d_params);
708// return res;
709// }
710// private:
711// const ROMol &d_query;
712// const ROMol &d_mol;
713// const SubstructMatchParameters &d_params;
714// };
715//
716// RDKit❗✔️: AtomLabelFunctor is ported as plain functions. The
717// useChirality specified/unspecified precheck is wired below; the final
718// tetrahedral parity check remains in MolMatchFinalCheckFunctor.
719
720fn has_chiral_label(atom: &Atom) -> bool {
721 // RDKit✔️✔️: bool hasChiralLabel(const Atom *at) {
722 // RDKit✔️✔️: PRECONDITION(at, "bad atom");
723 // RDKit✔️✔️: return at->getChiralTag() == Atom::CHI_TETRAHEDRAL_CW ||
724 // RDKit✔️✔️: at->getChiralTag() == Atom::CHI_TETRAHEDRAL_CCW;
725 // RDKit✔️✔️: }
726 // Rust's reference type enforces the non-null precondition. Complexity
727 // review: both implementations read one enum and perform at most two O(1)
728 // comparisons without allocation.
729 matches!(
730 atom.chiral_tag(),
731 ChiralTag::TetrahedralCw | ChiralTag::TetrahedralCcw
732 )
733}
734
735type MatchVect = Vec<(i32, i32)>;
736
737fn insert_if_needed(matches: &mut BTreeSet<MatchVect>, candidate: MatchVect) -> bool {
738 // RDKit✔️✔️: bool insertIfNeeded(std::set<MatchVectType> &matches, const MatchVectType &m) {
739 // RDKit✔️✔️: bool shouldInsert = true;
740 // RDKit✔️✔️: std::unordered_set<int> matchAsSet;
741 // RDKit✔️✔️: std::transform(m.begin(), m.end(),
742 // RDKit✔️✔️: std::inserter(matchAsSet, matchAsSet.begin()),
743 // RDKit✔️✔️: [](const std::pair<int, int> &p) { return p.second; });
744 // RDKit✔️✔️: for (auto it = matches.begin(); it != matches.end(); ++it) {
745 // RDKit✔️✔️: std::unordered_set<int> existingMatchAsSet;
746 // RDKit✔️✔️: std::transform(
747 // RDKit✔️✔️: it->begin(), it->end(),
748 // RDKit✔️✔️: std::inserter(existingMatchAsSet, existingMatchAsSet.begin()),
749 // RDKit✔️✔️: [](const std::pair<int, int> &p) { return p.second; });
750 // RDKit✔️✔️: if (matchAsSet == existingMatchAsSet) {
751 // RDKit✔️✔️: if (m < *it) {
752 // RDKit✔️✔️: matches.erase(it);
753 // RDKit✔️✔️: } else {
754 // RDKit✔️✔️: shouldInsert = false;
755 // RDKit✔️✔️: }
756 // RDKit✔️✔️: break;
757 // RDKit✔️✔️: }
758 // RDKit✔️✔️: }
759 // RDKit✔️✔️: if (shouldInsert) {
760 // RDKit✔️✔️: matches.insert(m);
761 // RDKit✔️✔️: }
762 // RDKit✔️✔️: return shouldInsert;
763 // RDKit✔️✔️: }
764 // Complexity review: both scan O(number of matches), build one O(match
765 // length) hash set per comparison, and use a logarithmic ordered-set erase
766 // and insert. Rust retains no temporary sets after the call.
767 let candidate_atoms: HashSet<i32> = candidate.iter().map(|pair| pair.1).collect();
768 let existing = matches.iter().find(|existing| {
769 existing.iter().map(|pair| pair.1).collect::<HashSet<_>>() == candidate_atoms
770 });
771 let mut should_insert = true;
772 if let Some(existing) = existing.cloned() {
773 if candidate < existing {
774 matches.remove(&existing);
775 } else {
776 should_insert = false;
777 }
778 }
779 if should_insert {
780 matches.insert(candidate);
781 }
782 should_insert
783}
784
785fn try_to_insert(
786 matches: &mut BTreeSet<MatchVect>,
787 candidate: MatchVect,
788 params: &SubstructMatchParams,
789) -> bool {
790 // RDKit✔️✔️: bool tryToInsert(std::set<MatchVectType> &matches, const MatchVectType &match,
791 // RDKit✔️✔️: const SubstructMatchParameters ¶ms) {
792 // RDKit✔️✔️: if (matches.size() == params.maxMatches) {
793 // RDKit✔️✔️: return false;
794 // RDKit✔️✔️: }
795 // RDKit✔️✔️: if (!params.uniquify) {
796 // RDKit✔️✔️: matches.insert(match);
797 // RDKit✔️✔️: } else {
798 // RDKit✔️✔️: insertIfNeeded(matches, match);
799 // RDKit✔️✔️: }
800 // RDKit✔️✔️: return true;
801 // RDKit✔️✔️: }
802 // Complexity review: the limit check is O(1), ordinary insertion is
803 // O(log M), and the uniquify branch delegates to the source-equivalent
804 // O(M * match length) canonical helper without additional copying.
805 if matches.len() == params.max_matches {
806 return false;
807 }
808 if !params.uniquify {
809 matches.insert(candidate);
810 } else {
811 insert_if_needed(matches, candidate);
812 }
813 true
814}
815
816fn atom_label_matches(
817 query: &Molecule,
818 mol: &Molecule,
819 query_index: usize,
820 mol_index: usize,
821 params: &SubstructMatchParams,
822 recursive_cache: Option<&RecursiveQueryMatchCache>,
823 query_ctx: &QueryMatchContext,
824) -> bool {
825 // RDKit✔️✔️: bool operator()(unsigned int i, unsigned int j) const {
826 // RDKit✔️✔️: bool res = false;
827 // RDKit✔️✔️: if (d_params.useChirality) {
828 // RDKit✔️✔️: const Atom *qAt = d_query.getAtomWithIdx(i);
829 // RDKit✔️✔️: if (qAt->getChiralTag() == Atom::CHI_TETRAHEDRAL_CW ||
830 // RDKit✔️✔️: qAt->getChiralTag() == Atom::CHI_TETRAHEDRAL_CCW) {
831 // RDKit✔️✔️: const Atom *mAt = d_mol.getAtomWithIdx(j);
832 // RDKit✔️✔️: if (!d_params.specifiedStereoQueryMatchesUnspecified &&
833 // RDKit✔️✔️: mAt->getChiralTag() != Atom::CHI_TETRAHEDRAL_CW &&
834 // RDKit✔️✔️: mAt->getChiralTag() != Atom::CHI_TETRAHEDRAL_CCW) {
835 // RDKit✔️✔️: return false;
836 // RDKit✔️✔️: }
837 // RDKit✔️✔️: }
838 // RDKit✔️✔️: }
839 // RDKit✔️✔️: res = atomCompat(d_query[i], d_mol[j], d_params);
840 // RDKit✔️✔️: return res;
841 // RDKit✔️✔️: }
842 // Complexity review: the precheck is O(1), then this delegates exactly once
843 // to canonical atom_compat; it introduces no allocation or repeated query
844 // evaluation beyond the source functor.
845 let query_atom = &query.atoms()[query_index];
846 let mol_atom = &mol.atoms()[mol_index];
847 if params.use_chirality
848 && has_chiral_label(query_atom)
849 && !params.specified_stereo_query_matches_unspecified
850 && !has_chiral_label(mol_atom)
851 {
852 return false;
853 }
854 atom_compat(
855 query_atom,
856 query,
857 mol_atom,
858 mol,
859 params,
860 recursive_cache,
861 query_ctx,
862 )
863}
864
865fn atom_matches(query_atom: &Atom, query_mol: &Molecule, mol_atom: &Atom, mol: &Molecule) -> bool {
866 if let Some(query_node) = query_atom.query() {
867 let query_ctx = build_query_match_context(mol);
868 return evaluate_atom_query(
869 query_node,
870 mol_atom,
871 mol,
872 &SubstructMatchParams::default(),
873 None,
874 &query_ctx,
875 );
876 }
877
878 // BEGIN RDKIT CPP FUNCTION: third_party/rdkit/Code/GraphMol/Atom.cpp :: Atom::Match
879 // RDKit✔️✔️: bool Atom::Match(Atom const *what) const {
880 // RDKit✔️✔️: PRECONDITION(what, "bad query atom");
881 // RDKit✔️✔️: bool res = getAtomicNum() == what->getAtomicNum();
882 // RDKit✔️✔️:
883 // RDKit✔️✔️: // special dummy--dummy match case:
884 // RDKit✔️✔️: // [*] matches [*],[1*],[2*],etc.
885 // RDKit✔️✔️: // [1*] only matches [*] and [1*]
886 // RDKit✔️✔️: if (res) {
887 // RDKit✔️✔️: if (!this->getAtomicNum()) {
888 // RDKit✔️✔️: // this is the new behavior, based on the isotopes:
889 // RDKit✔️✔️: int tgt = this->getIsotope();
890 // RDKit✔️✔️: int test = what->getIsotope();
891 // RDKit✔️✔️: if (tgt && test && tgt != test) {
892 // RDKit✔️✔️: res = false;
893 // RDKit✔️✔️: }
894 // RDKit✔️✔️: } else {
895 // RDKit✔️✔️: // standard atom-atom match: The general rule here is that if this atom
896 // RDKit✔️✔️: // has a property that
897 // RDKit✔️✔️: // deviates from the default, then the other atom should match that value.
898 // RDKit✔️✔️: if ((this->getFormalCharge() &&
899 // RDKit✔️✔️: this->getFormalCharge() != what->getFormalCharge()) ||
900 // RDKit✔️✔️: (this->getIsotope() && this->getIsotope() != what->getIsotope()) ||
901 // RDKit✔️✔️: (this->getNumRadicalElectrons() &&
902 // RDKit✔️✔️: this->getNumRadicalElectrons() != what->getNumRadicalElectrons())) {
903 // RDKit✔️✔️: res = false;
904 // RDKit✔️✔️: }
905 // RDKit✔️✔️: }
906 // RDKit✔️✔️: }
907 // RDKit✔️✔️: return res;
908 // RDKit✔️✔️: }
909 // END RDKIT CPP FUNCTION
910 //
911 // Local complexity review: the plain-atom path is constant time and uses
912 // only scalar field reads, exactly as the source. No allocation, cloning,
913 // molecule scan, keyed lookup, or temporary collection is introduced.
914 if query_atom.atomic_number() != mol_atom.atomic_number() {
915 return false;
916 }
917 let _ = query_mol;
918 if query_atom.atomic_number() == 0 {
919 return match (query_atom.isotope(), mol_atom.isotope()) {
920 (Some(query_isotope), Some(mol_isotope)) => query_isotope == mol_isotope,
921 _ => true,
922 };
923 }
924 (query_atom.formal_charge() == 0 || query_atom.formal_charge() == mol_atom.formal_charge())
925 && (query_atom.isotope().is_none() || query_atom.isotope() == mol_atom.isotope())
926 && (query_atom.radical_electrons() == 0
927 || query_atom.radical_electrons() == mol_atom.radical_electrons())
928}
929
930fn recursive_smarts_root_matches(
931 atom: &Atom,
932 recursive_query: &crate::search::query::RecursiveStructureQuery,
933 mol: &Molecule,
934 recursive_cache: Option<&RecursiveQueryMatchCache>,
935) -> bool {
936 // BEGIN RDKIT CPP FUNCTION: third_party/rdkit/Code/GraphMol/QueryOps.h :: RecursiveStructureQuery
937 // RDKit✔️✔️: class RDKIT_GRAPHMOL_EXPORT RecursiveStructureQuery
938 // RDKit✔️✔️: : public Queries::SetQuery<int, Atom const *, true> {
939 // RDKit✔️✔️: RecursiveStructureQuery(ROMol const *query, unsigned int serialNumber = 0)
940 // RDKit✔️✔️: : Queries::SetQuery<int, Atom const *, true>(),
941 // RDKit✔️✔️: d_serialNumber(serialNumber) {
942 // RDKit✔️✔️: setQueryMol(query);
943 // RDKit✔️✔️: setDataFunc(getAtIdx);
944 // RDKit✔️✔️: setDescription("RecursiveStructure");
945 // RDKit✔️✔️: }
946 // RDKit✔️✔️: static inline int getAtIdx(Atom const *at) {
947 // RDKit✔️✔️: PRECONDITION(at, "bad atom argument");
948 // RDKit✔️✔️: return at->getIdx();
949 // RDKit✔️✔️: }
950 // END RDKIT CPP FUNCTION
951 //
952 // BEGIN RDKIT CPP FUNCTION: third_party/rdkit/Code/GraphMol/Substruct/SubstructMatch.cpp :: detail::RecursiveMatcher
953 // RDKit✔️✔️: if (!query.hasProp(common_properties::_queryRootAtom)) {
954 // RDKit✔️✔️: matches.push_back(pairs.begin()->second);
955 // RDKit✔️✔️: } else {
956 // RDKit✔️✔️: int rootIdx;
957 // RDKit✔️✔️: query.getProp(common_properties::_queryRootAtom, rootIdx);
958 // RDKit✔️✔️: bool found = false;
959 // RDKit✔️✔️: for (const auto &pairIter : pairs) {
960 // RDKit✔️✔️: if (pairIter.first == static_cast<unsigned int>(rootIdx)) {
961 // RDKit✔️✔️: matches.push_back(pairIter.second);
962 // RDKit✔️✔️: found = true;
963 // RDKit✔️✔️: break;
964 // RDKit✔️✔️: }
965 // RDKit✔️✔️: }
966 // RDKit✔️✔️: }
967 // END RDKIT CPP FUNCTION
968 //
969 // COSMolKit currently parses the recursive SMARTS used by Lipinski NumHBA
970 // without `_queryRootAtom`; matching therefore uses RDKit's first mapped
971 // query atom as the recursive root and tests membership in the cached
972 // RecursiveStructureQuery atom-index set.
973 if let Some(cache) = recursive_cache {
974 return cache
975 .get(&recursive_query_cache_key(recursive_query))
976 .and_then(|match_starts| match_starts.get(atom.id().index()))
977 .copied()
978 .unwrap_or(false);
979 }
980
981 let Some(query) = recursive_query.query_mol() else {
982 return false;
983 };
984 substruct_match_impl(
985 mol,
986 query,
987 &SubstructMatchParams {
988 max_matches: 1000,
989 uniquify: false,
990 use_chirality: false,
991 specified_stereo_query_matches_unspecified: false,
992 ..Default::default()
993 },
994 )
995 .unwrap_or_default()
996 .into_iter()
997 .any(|matched| matched.atom_mapping.first().copied() == Some(atom.id().index()))
998}
999
1000fn atom_query_predicate_matches_for_substruct(
1001 atom: &Atom,
1002 pred: &AtomQueryPredicate,
1003 mol: &Molecule,
1004 params: &SubstructMatchParams,
1005 recursive_cache: Option<&RecursiveQueryMatchCache>,
1006 query_ctx: &QueryMatchContext,
1007) -> bool {
1008 match pred {
1009 // RDKit✔️✔️: Chiral SMARTS labels are not ordinary atom-compatibility
1010 // constraints when `useChirality` is false. AtomLabelFunctor and
1011 // MolMatchFinalCheckFunctor handle stereochemistry explicitly.
1012 AtomQueryPredicate::ChiralTagMatch(_) | AtomQueryPredicate::ChiralPermutationMatch(_)
1013 if !params.use_chirality =>
1014 {
1015 true
1016 }
1017 AtomQueryPredicate::RecursiveSmarts(recursive_query) => {
1018 recursive_smarts_root_matches(atom, recursive_query, mol, recursive_cache)
1019 }
1020 _ => atom_predicate_matches_with_context(atom, pred, mol, query_ctx),
1021 }
1022}
1023
1024/// RDKit❗✔️: Evaluation of an atom query node for the SMARTS subset currently
1025/// modeled by COSMolKit.
1026///
1027/// Recursive SMARTS are evaluated through the recursive match cache used by
1028/// SubstructMatch; unsupported predicate leaves still evaluate false.
1029fn evaluate_atom_query(
1030 query: &crate::QueryNode<AtomQueryPredicate>,
1031 atom: &Atom,
1032 mol: &Molecule,
1033 params: &SubstructMatchParams,
1034 recursive_cache: Option<&RecursiveQueryMatchCache>,
1035 query_ctx: &QueryMatchContext,
1036) -> bool {
1037 match query {
1038 crate::QueryNode::Predicate(pred) => atom_query_predicate_matches_for_substruct(
1039 atom,
1040 pred,
1041 mol,
1042 params,
1043 recursive_cache,
1044 query_ctx,
1045 ),
1046 crate::QueryNode::And(children) => and_query_match(children, false, |child| {
1047 evaluate_atom_query(child, atom, mol, params, recursive_cache, query_ctx)
1048 }),
1049 crate::QueryNode::Or(children) => or_query_match(children, false, |child| {
1050 evaluate_atom_query(child, atom, mol, params, recursive_cache, query_ctx)
1051 }),
1052 crate::QueryNode::Xor(children) => xor_query_match(children, false, |child| {
1053 evaluate_atom_query(child, atom, mol, params, recursive_cache, query_ctx)
1054 }),
1055 crate::QueryNode::Not(child) => {
1056 !evaluate_atom_query(child, atom, mol, params, recursive_cache, query_ctx)
1057 }
1058 }
1059}
1060
1061// RDKit source (SubstructMatch.cpp):
1062// class BondLabelFunctor {
1063// public:
1064// BondLabelFunctor(const ROMol &query, const ROMol &mol,
1065// const SubstructMatchParameters &ps)
1066// : d_query(query), d_mol(mol), d_params(ps) {};
1067// bool operator()(MolGraph::edge_descriptor i,
1068// MolGraph::edge_descriptor j) const {
1069// if (d_params.useChirality) {
1070// const Bond *qBnd = d_query[i];
1071// if (qBnd->getBondType() == Bond::DOUBLE &&
1072// qBnd->getStereo() > Bond::STEREOANY) {
1073// const Bond *mBnd = d_mol[j];
1074// if (mBnd->getBondType() == Bond::DOUBLE &&
1075// !d_params.specifiedStereoQueryMatchesUnspecified &&
1076// mBnd->getStereo() <= Bond::STEREOANY) {
1077// return false;
1078// }
1079// }
1080// }
1081// bool res = bondCompat(d_query[i], d_mol[j], d_params);
1082// return res;
1083// }
1084// private:
1085// const ROMol &d_query;
1086// const ROMol &d_mol;
1087// const SubstructMatchParameters &d_params;
1088// };
1089
1090fn rdkit_bond_stereo_is_above_any(stereo: BondStereo) -> bool {
1091 !matches!(stereo, BondStereo::None | BondStereo::Any)
1092}
1093
1094fn bond_label_matches(
1095 query: &Molecule,
1096 mol: &Molecule,
1097 query_index: usize,
1098 mol_index: usize,
1099 params: &SubstructMatchParams,
1100 query_ctx: &QueryMatchContext,
1101) -> bool {
1102 // RDKit✔️✔️: bool operator()(MolGraph::edge_descriptor i,
1103 // RDKit✔️✔️: MolGraph::edge_descriptor j) const {
1104 // RDKit✔️✔️: if (d_params.useChirality) {
1105 // RDKit✔️✔️: const Bond *qBnd = d_query[i];
1106 // RDKit✔️✔️: if (qBnd->getBondType() == Bond::DOUBLE &&
1107 // RDKit✔️✔️: qBnd->getStereo() > Bond::STEREOANY) {
1108 // RDKit✔️✔️: const Bond *mBnd = d_mol[j];
1109 // RDKit✔️✔️: if (mBnd->getBondType() == Bond::DOUBLE &&
1110 // RDKit✔️✔️: !d_params.specifiedStereoQueryMatchesUnspecified &&
1111 // RDKit✔️✔️: mBnd->getStereo() <= Bond::STEREOANY) {
1112 // RDKit✔️✔️: return false;
1113 // RDKit✔️✔️: }
1114 // RDKit✔️✔️: }
1115 // RDKit✔️✔️: }
1116 // RDKit✔️✔️: bool res = bondCompat(d_query[i], d_mol[j], d_params);
1117 // RDKit✔️✔️: return res;
1118 // RDKit✔️✔️: }
1119 // Complexity review: the stereo precheck is O(1), then this delegates
1120 // exactly once to canonical bond_compat. It adds no allocation, scan,
1121 // cloning, or repeated query evaluation beyond the source functor.
1122 let query_bond = &query.bonds()[query_index];
1123 let mol_bond = &mol.bonds()[mol_index];
1124 if params.use_chirality
1125 && query_bond.order() == BondOrder::Double
1126 && rdkit_bond_stereo_is_above_any(query_bond.stereo())
1127 && mol_bond.order() == BondOrder::Double
1128 && !params.specified_stereo_query_matches_unspecified
1129 && !rdkit_bond_stereo_is_above_any(mol_bond.stereo())
1130 {
1131 return false;
1132 }
1133 bond_compat(query_bond, query, mol_bond, mol, params, query_ctx)
1134}
1135
1136/// RDKit❗✔️: Evaluation of a bond query node for the currently modeled SMARTS
1137/// bond predicate subset.
1138fn evaluate_bond_query(
1139 query: &crate::QueryNode<BondQueryPredicate>,
1140 bond: &Bond,
1141 mol: &Molecule,
1142 query_ctx: &QueryMatchContext,
1143) -> bool {
1144 match query {
1145 crate::QueryNode::Predicate(pred) => {
1146 bond_predicate_matches_with_context(bond, pred, mol, query_ctx)
1147 }
1148 crate::QueryNode::And(children) => and_query_match(children, false, |child| {
1149 evaluate_bond_query(child, bond, mol, query_ctx)
1150 }),
1151 crate::QueryNode::Or(children) => or_query_match(children, false, |child| {
1152 evaluate_bond_query(child, bond, mol, query_ctx)
1153 }),
1154 crate::QueryNode::Xor(children) => xor_query_match(children, false, |child| {
1155 evaluate_bond_query(child, bond, mol, query_ctx)
1156 }),
1157 crate::QueryNode::Not(child) => !evaluate_bond_query(child, bond, mol, query_ctx),
1158 }
1159}
1160
1161// ---------------------------------------------------------------------------
1162// VF2 State Machine
1163// ---------------------------------------------------------------------------
1164//
1165// ## RDKit source reproduction: vf2.hpp
1166//
1167// The following section reproduces the VF2SubState class from vf2.hpp.
1168// The C++ code is shown as verbatim comments with RDKit markers.
1169//
1170// ### Key design differences from RDKit:
1171//
1172// 1. `core_1`/`core_2`: Same role — mapping from query atom idx → mol atom idx
1173// and vice versa. Uses `Option<usize>` instead of NULL_NODE sentinel.
1174//
1175// 2. `term_1`/`term_2`: Stores the core_len *depth* at which each atom was
1176// added to the terminal set, exactly as in vf2.hpp. BackTrack decrements
1177// counters keyed by depth, not recomputes from scratch.
1178//
1179// 3. No shared_ptr copy semantics: VF2SubState in RDKit uses COW with
1180// `share_count`. Rust's Clone+Vf2State avoids raw pointer sharing.
1181// This means each VF2 recursive branch owns its state, which is
1182// semantically correct but allocates O(depth * n) instead of
1183// O(n) shared storage. For typical molecule sizes (<1000 atoms) this
1184// is negligible; for very large searches the COW approach could be
1185// reinstated with Arc<Vec<NodeId>>.
1186//
1187// 4. No boost graph: hand-rolled Vf2Graph adjacency.
1188
1189// RDKit source (vf2.hpp):
1190// typedef std::uint32_t node_id;
1191// const node_id NULL_NODE = 0xFFFFFFFF;
1192
1193type NodeId = usize;
1194const NULL_NODE: NodeId = usize::MAX;
1195
1196// RDKit source (vf2.hpp):
1197// template <class Graph>
1198// struct Pair {
1199// node_id n1, n2;
1200// bool hasiter{false};
1201// RDK_ADJ_ITER nbrbeg, nbrend;
1202// Pair() : n1(NULL_NODE), n2(NULL_NODE) {}
1203// };
1204
1205#[derive(Debug, Clone)]
1206struct Vf2Pair {
1207 n1: NodeId,
1208 n2: NodeId,
1209 hasiter: bool,
1210 /// VF2+ source atom in the mol graph (g2) whose adjacency drives the
1211 /// neighbor iterator.
1212 nbr_node: NodeId,
1213 /// VF2+ neighbor iterator over mol graph (g2) neighbors.
1214 nbr_cursor: usize,
1215 nbr_end: usize,
1216}
1217
1218impl Vf2Pair {
1219 fn new() -> Self {
1220 Self {
1221 n1: NULL_NODE,
1222 n2: NULL_NODE,
1223 hasiter: false,
1224 nbr_node: NULL_NODE,
1225 nbr_cursor: 0,
1226 nbr_end: 0,
1227 }
1228 }
1229}
1230
1231#[derive(Debug, Clone, Copy)]
1232struct NodeInfo {
1233 id: usize,
1234 in_deg: usize,
1235 out_deg: usize,
1236}
1237
1238fn node_info_cmp1(a: &NodeInfo, b: &NodeInfo) -> std::cmp::Ordering {
1239 // RDKit✔️✔️: static bool nodeInfoComp1(const NodeInfo &a, const NodeInfo &b) {
1240 // RDKit✔️✔️: if (a.out < b.out) {
1241 // RDKit✔️✔️: return true;
1242 // RDKit✔️✔️: }
1243 // RDKit✔️✔️: if (a.out > b.out) {
1244 // RDKit✔️✔️: return false;
1245 // RDKit✔️✔️: }
1246 // RDKit✔️✔️: if (a.in < b.in) {
1247 // RDKit✔️✔️: return true;
1248 // RDKit✔️✔️: }
1249 // RDKit✔️✔️: if (a.in > b.in) {
1250 // RDKit✔️✔️: return false;
1251 // RDKit✔️✔️: }
1252 // RDKit✔️✔️: return false;
1253 // RDKit✔️✔️: }
1254 // Complexity review: both implementations perform at most two integer
1255 // comparisons in O(1) time without allocation or temporary collections.
1256 a.out_deg
1257 .cmp(&b.out_deg)
1258 .then_with(|| a.in_deg.cmp(&b.in_deg))
1259}
1260
1261fn node_info_cmp2(a: &NodeInfo, b: &NodeInfo) -> std::cmp::Ordering {
1262 // RDKit✔️✔️: static int nodeInfoComp2(const NodeInfo &a, const NodeInfo &b) {
1263 // RDKit✔️✔️: if (!a.in && b.in) {
1264 // RDKit✔️✔️: return 1;
1265 // RDKit✔️✔️: }
1266 // RDKit✔️✔️: if (a.in && !b.in) {
1267 // RDKit✔️✔️: return -1;
1268 // RDKit✔️✔️: }
1269 // RDKit✔️✔️: if (a.out < b.out) {
1270 // RDKit✔️✔️: return -1;
1271 // RDKit✔️✔️: }
1272 // RDKit✔️✔️: if (a.out > b.out) {
1273 // RDKit✔️✔️: return 1;
1274 // RDKit✔️✔️: }
1275 // RDKit✔️✔️: if (a.in < b.in) {
1276 // RDKit✔️✔️: return -1;
1277 // RDKit✔️✔️: }
1278 // RDKit✔️✔️: if (a.in > b.in) {
1279 // RDKit✔️✔️: return 1;
1280 // RDKit✔️✔️: }
1281 // RDKit✔️✔️: return 0;
1282 // RDKit✔️✔️: }
1283 // Complexity review: both implementations perform a bounded sequence of
1284 // integer comparisons in O(1) time without allocation or cloning.
1285 if a.in_deg == 0 && b.in_deg != 0 {
1286 return std::cmp::Ordering::Greater;
1287 }
1288 if a.in_deg != 0 && b.in_deg == 0 {
1289 return std::cmp::Ordering::Less;
1290 }
1291 a.out_deg
1292 .cmp(&b.out_deg)
1293 .then_with(|| a.in_deg.cmp(&b.in_deg))
1294}
1295
1296// RDKit source (vf2.hpp), SortNodesByFrequency:
1297// Sorts the nodes of a graphs, returning a heap-allocated vector
1298// with the node ids in the proper orders.
1299// The sorting criterion takes into account:
1300// 1 - The number of nodes with the same in/out degree.
1301// 2 - The valence of the nodes.
1302// The nodes at the beginning of the vector are the most singular,
1303// from which the matching should start.
1304
1305fn sort_nodes_by_frequency(g: &Vf2Graph) -> Vec<NodeId> {
1306 // RDKit✔️✔️: template <class Graph>
1307 // RDKit✔️✔️: node_id *SortNodesByFrequency(const Graph *g) {
1308 // RDKit✔️✔️: std::vector<NodeInfo> vect;
1309 // RDKit✔️✔️: vect.reserve(boost::num_vertices(*g));
1310 // RDKit✔️✔️: typename Graph::vertex_iterator bNode, eNode;
1311 // RDKit✔️✔️: boost::tie(bNode, eNode) = boost::vertices(*g);
1312 // RDKit✔️✔️: while (bNode != eNode) {
1313 // RDKit✔️✔️: NodeInfo t;
1314 // RDKit✔️✔️: t.id = vect.size();
1315 // RDKit✔️✔️: t.in = boost::out_degree(*bNode, *g); // <- assuming undirected graph
1316 // RDKit✔️✔️: t.out = boost::out_degree(*bNode, *g);
1317 // RDKit✔️✔️: vect.push_back(t);
1318 // RDKit✔️✔️: ++bNode;
1319 // RDKit✔️✔️: }
1320 // RDKit✔️✔️: std::sort(vect.begin(), vect.end(), nodeInfoComp1);
1321 let mut vect: Vec<NodeInfo> = (0..g.n_atoms)
1322 .map(|i| {
1323 let deg = g.out_degree(i);
1324 NodeInfo {
1325 id: i,
1326 in_deg: deg,
1327 out_deg: deg,
1328 }
1329 })
1330 .collect();
1331 vect.sort_unstable_by(node_info_cmp1);
1332
1333 // RDKit✔️✔️: unsigned int run = 1;
1334 // RDKit✔️✔️: for (unsigned int i = 0; i < vect.size(); i += run) {
1335 // RDKit✔️✔️: for (run = 1; i + run < vect.size() && vect[i + run].in == vect[i].in &&
1336 // RDKit✔️✔️: vect[i + run].out == vect[i].out;
1337 // RDKit✔️✔️: ++run) {
1338 // RDKit✔️✔️: ;
1339 // RDKit✔️✔️: }
1340 // RDKit✔️✔️: for (unsigned int j = 0; j < run; ++j) {
1341 // RDKit✔️✔️: vect[i + j].in += vect[i + j].out;
1342 // RDKit✔️✔️: vect[i + j].out = run;
1343 // RDKit✔️✔️: }
1344 // RDKit✔️✔️: }
1345 let mut i = 0;
1346 while i < vect.len() {
1347 let mut run = 1;
1348 while i + run < vect.len()
1349 && vect[i + run].in_deg == vect[i].in_deg
1350 && vect[i + run].out_deg == vect[i].out_deg
1351 {
1352 run += 1;
1353 }
1354 for j in 0..run {
1355 vect[i + j].in_deg += vect[i + j].out_deg; // valence sum
1356 vect[i + j].out_deg = run; // frequency
1357 }
1358 i += run;
1359 }
1360
1361 // RDKit✔️✔️: std::sort(vect.begin(), vect.end(), nodeInfoComp2);
1362 vect.sort_unstable_by(node_info_cmp2);
1363
1364 // RDKit✔️✔️: node_id *nodes = new node_id[vect.size()];
1365 // RDKit✔️✔️: for (unsigned int i = 0; i < vect.size(); ++i) {
1366 // RDKit✔️✔️: nodes[i] = vect[i].id;
1367 // RDKit✔️✔️: }
1368 // RDKit✔️✔️:
1369 // RDKit✔️✔️: return nodes;
1370 // RDKit✔️✔️: }
1371 // Complexity review: both versions allocate O(V) node metadata and an
1372 // O(V) result, perform two O(V log V) unstable sorts, and scan runs in
1373 // O(V). Degree lookup and all loop bodies remain O(1) per visited node.
1374 vect.iter().map(|ni| ni.id).collect()
1375}
1376
1377// RDKit source (vf2.hpp), VF2SubState class:
1378// template <class Graph, class VertexCompatible, class EdgeCompatible,
1379// class MatchChecking>
1380// class VF2SubState {
1381// private:
1382// Graph *g1, *g2;
1383// VertexCompatible &vc;
1384// EdgeCompatible &ec;
1385// MatchChecking &mc;
1386// unsigned int n1, n2;
1387// unsigned int core_len;
1388// unsigned int t1_len;
1389// unsigned int t2_len; // Core nodes are also counted by these...
1390// node_id *core_1;
1391// node_id *core_2;
1392// node_id *term_1;
1393// node_id *term_2;
1394// node_id *order;
1395// long *share_count;
1396// int *vs_compared;
1397
1398/// RDKit❗✔️: VF2 subgraph isomorphism state.
1399///
1400/// g1 = query graph, g2 = molecule graph.
1401/// core_1[i] = mapping from query atom i -> mol atom j (or None).
1402/// core_2[j] = mapping from mol atom j -> query atom i (or None).
1403/// term_1[i] = depth (core_len) when atom i entered terminal set (0 = not terminal).
1404/// term_2[j] = same for mol atoms.
1405struct Vf2SubState<'a> {
1406 g1: &'a Vf2Graph,
1407 g2: &'a Vf2Graph,
1408 n1: usize,
1409 n2: usize,
1410 core_len: usize,
1411 t1_len: usize,
1412 t2_len: usize,
1413 core_1: Vec<NodeId>,
1414 core_2: Vec<NodeId>,
1415 term_1: Vec<usize>,
1416 term_2: Vec<usize>,
1417 order: Option<Vec<NodeId>>,
1418}
1419
1420impl<'a> Vf2SubState<'a> {
1421 fn new(g1: &'a Vf2Graph, g2: &'a Vf2Graph, sort_nodes: bool) -> Self {
1422 // RDKit✔️✔️: VF2SubState(Graph *ag1, Graph *ag2, VertexCompatible &avc,
1423 // RDKit✔️✔️: EdgeCompatible &aec, MatchChecking &amc, bool sortNodes = false)
1424 // RDKit✔️✔️: : g1(ag1),
1425 // RDKit✔️✔️: g2(ag2),
1426 // RDKit✔️✔️: vc(avc),
1427 // RDKit✔️✔️: ec(aec),
1428 // RDKit✔️✔️: mc(amc),
1429 // RDKit✔️✔️: n1(num_vertices(*ag1)),
1430 // RDKit✔️✔️: n2(num_vertices(*ag2)) {
1431 // RDKit✔️✔️: if (sortNodes) {
1432 // RDKit✔️✔️: order = SortNodesByFrequency(ag1);
1433 // RDKit✔️✔️: } else {
1434 // RDKit✔️✔️: order = nullptr;
1435 // RDKit✔️✔️: }
1436 // RDKit✔️✔️:
1437 // RDKit✔️✔️: core_len = 0;
1438 // RDKit✔️✔️: t1_len = 0;
1439 // RDKit✔️✔️: t2_len = 0;
1440 // RDKit✔️✔️:
1441 // RDKit✔️✔️: core_1 = new node_id[n1];
1442 // RDKit✔️✔️: core_2 = new node_id[n2];
1443 // RDKit✔️✔️: term_1 = new node_id[n1];
1444 // RDKit✔️✔️: term_2 = new node_id[n2];
1445 // RDKit✔️✔️: share_count = new long;
1446 // RDKit✔️✔️:
1447 // RDKit✔️✔️: for (unsigned int i = 0; i < n1; i++) {
1448 // RDKit✔️✔️: core_1[i] = NULL_NODE;
1449 // RDKit✔️✔️: term_1[i] = 0;
1450 // RDKit✔️✔️: }
1451 // RDKit✔️✔️: for (unsigned int i = 0; i < n2; i++) {
1452 // RDKit✔️✔️: core_2[i] = NULL_NODE;
1453 // RDKit✔️✔️: term_2[i] = 0;
1454 // RDKit✔️✔️: }
1455 // RDKit✔️✔️: vs_compared = nullptr;
1456 // RDKit✔️✔️: // vs_compared = new int[n1*n2];
1457 // RDKit✔️✔️: // memset((void *)vs_compared,0,n1*n2*sizeof(int));
1458 // RDKit✔️✔️:
1459 // RDKit✔️✔️: // es_compared = new std::map<unsigned int,bool>();
1460 // RDKit✔️✔️: *share_count = 1;
1461 // RDKit✔️✔️: }
1462 // The compatibility functors remain explicit arguments to Rust match
1463 // methods, so the state stores only the source fields those methods use.
1464 // Complexity review: both implementations initialize four O(V) arrays
1465 // and optionally run the same O(V log V) ordering routine. Vec uses the
1466 // same contiguous storage and does not add asymptotic or hot-path work.
1467 let n1 = g1.n_atoms;
1468 let n2 = g2.n_atoms;
1469 let order = if sort_nodes {
1470 Some(sort_nodes_by_frequency(g1))
1471 } else {
1472 None
1473 };
1474
1475 // RDKit✔️✔️: core_len = 0; t1_len = 0; t2_len = 0;
1476 // RDKit✔️✔️: core_1[i] = NULL_NODE; term_1[i] = 0;
1477 // RDKit✔️✔️: core_2[j] = NULL_NODE; term_2[j] = 0;
1478 Self {
1479 g1,
1480 g2,
1481 n1,
1482 n2,
1483 core_len: 0,
1484 t1_len: 0,
1485 t2_len: 0,
1486 core_1: vec![NULL_NODE; n1],
1487 core_2: vec![NULL_NODE; n2],
1488 term_1: vec![0usize; n1],
1489 term_2: vec![0usize; n2],
1490 order,
1491 }
1492 }
1493
1494 fn clone_state(&self) -> Self {
1495 // RDKit✔️❌: VF2SubState(const VF2SubState &state)
1496 // RDKit✔️❌: : g1(state.g1),
1497 // RDKit✔️❌: g2(state.g2),
1498 // RDKit✔️❌: vc(state.vc),
1499 // RDKit✔️❌: ec(state.ec),
1500 // RDKit✔️❌: mc(state.mc),
1501 // RDKit✔️❌: n1(state.n1),
1502 // RDKit✔️❌: n2(state.n2),
1503 // RDKit✔️❌: order(state.order),
1504 // RDKit✔️❌: vs_compared(state.vs_compared)
1505 // RDKit✔️❌: // es_compared(state.es_compared)
1506 // RDKit✔️❌: {
1507 // RDKit✔️❌: core_len = state.core_len;
1508 // RDKit✔️❌: t1_len = state.t1_len;
1509 // RDKit✔️❌: t2_len = state.t2_len;
1510 // RDKit✔️❌:
1511 // RDKit✔️❌: core_1 = state.core_1;
1512 // RDKit✔️❌: core_2 = state.core_2;
1513 // RDKit✔️❌: term_1 = state.term_1;
1514 // RDKit✔️❌: term_2 = state.term_2;
1515 // RDKit✔️❌: share_count = state.share_count;
1516 // RDKit✔️❌:
1517 // RDKit✔️❌: ++(*share_count);
1518 // RDKit✔️❌: }
1519 // Compatibility callbacks are passed to Rust match calls rather than
1520 // stored in the state. Deep-copying Vec state preserves the copied
1521 // values and makes subsequent mutation independent. Complexity review:
1522 // this is O(V) with five allocations, while RDKit shares the arrays and
1523 // increments one reference count in O(1).
1524 Self {
1525 g1: self.g1,
1526 g2: self.g2,
1527 n1: self.n1,
1528 n2: self.n2,
1529 core_len: self.core_len,
1530 t1_len: self.t1_len,
1531 t2_len: self.t2_len,
1532 core_1: self.core_1.clone(),
1533 core_2: self.core_2.clone(),
1534 term_1: self.term_1.clone(),
1535 term_2: self.term_2.clone(),
1536 order: self.order.clone(),
1537 }
1538 }
1539
1540 fn clone(&self) -> Self {
1541 // RDKit✔️❌: VF2SubState *Clone() { return new VF2SubState(*this); }
1542 // Complexity review: this forwards to the single O(V) Rust state-copy
1543 // implementation, while RDKit's shared-array copy is O(1). No second
1544 // clone path is introduced.
1545 self.clone_state()
1546 }
1547
1548 fn debug_order(&self) -> Option<&[NodeId]> {
1549 self.order.as_deref()
1550 }
1551
1552 fn is_goal(&self) -> bool {
1553 // RDKit✔️✔️: bool IsGoal() { return core_len == n1; }
1554 // Complexity review: one integer equality in O(1), without allocation.
1555 self.core_len == self.n1
1556 }
1557
1558 fn match_checks(
1559 &self,
1560 c1: &[NodeId],
1561 c2: &[NodeId],
1562 check: &mut impl FnMut(&[NodeId], &[NodeId]) -> bool,
1563 ) -> bool {
1564 // RDKit✔️✔️: bool MatchChecks(const node_id c1[], const node_id c2[]) {
1565 // RDKit✔️✔️: return mc(c1, c2);
1566 // RDKit✔️✔️: }
1567 // Complexity review: both forms make one callback invocation and pass
1568 // existing mapping storage by reference without allocation or cloning.
1569 check(c1, c2)
1570 }
1571
1572 fn is_dead(&self) -> bool {
1573 // RDKit✔️✔️: bool IsDead() { return n1 > n2 || t1_len > t2_len; }
1574 // Complexity review: at most two integer comparisons in O(1), without
1575 // allocation or temporary collections.
1576 self.n1 > self.n2 || self.t1_len > self.t2_len
1577 }
1578
1579 fn core_len(&self) -> usize {
1580 // RDKit✔️✔️: unsigned int CoreLen() { return core_len; }
1581 // Complexity review: one field read in O(1), without allocation.
1582 self.core_len
1583 }
1584
1585 // RDKit source (vf2.hpp):
1586 // bool NextPair(Pair<Graph> &pair) {
1587 // if (pair.n1 == NULL_NODE) { pair.n1 = 0; }
1588 // if (pair.n2 == NULL_NODE) { pair.n2 = 0; }
1589 // else { pair.n2++; }
1590 // ...
1591 // if (t1_len > core_len && t2_len > core_len) {
1592 // while (pair.n1 < n1 &&
1593 // (core_1[pair.n1] != NULL_NODE || term_1[pair.n1] == 0)) {
1594 // pair.n1++; pair.n2 = 0;
1595 // }
1596 // ...
1597 // } else if (pair.n1 == 0 && order != nullptr) {
1598 // // Optimisation: ...
1599 // unsigned int i = 0;
1600 // while (i < n1 && core_1[pair.n1 = order[i]] != NULL_NODE) { i++; }
1601 // ...
1602 // } else {
1603 // while (pair.n1 < n1 && core_1[pair.n1] != NULL_NODE) {
1604 // pair.n1++; pair.n2 = 0;
1605 // }
1606 // }
1607 // // VF2 Plus iterator ...
1608 // if (pair.hasiter) { ... }
1609 // else if (t1_len > core_len && t2_len > core_len) {
1610 // while (pair.n2 < n2 &&
1611 // (core_2[pair.n2] != NULL_NODE || term_2[pair.n2] == 0)) {
1612 // pair.n2++;
1613 // }
1614 // } else {
1615 // while (pair.n2 < n2 && core_2[pair.n2] != NULL_NODE) { pair.n2++; }
1616 // }
1617 // return pair.n1 < n1 && pair.n2 < n2;
1618 // }
1619
1620 /// RDKit✔️❌: NextPair — find the next candidate pair (n1 from query,
1621 /// n2 from mol) to try matching.
1622 ///
1623 /// Uses terminal-set-based iteration from vf2.hpp, including the VF2+
1624 /// neighbor iterator that restricts mol-side candidates to neighbors of
1625 /// the already-mapped terminal predecessor.
1626 fn next_pair(&self, pair: &mut Vf2Pair) -> bool {
1627 // RDKit✔️✔️: bool NextPair(Pair<Graph> &pair) {
1628 // RDKit✔️✔️: if (pair.n1 == NULL_NODE) {
1629 // RDKit✔️✔️: pair.n1 = 0;
1630 // RDKit✔️✔️: }
1631 // RDKit✔️✔️: if (pair.n2 == NULL_NODE) {
1632 // RDKit✔️✔️: pair.n2 = 0;
1633 // RDKit✔️✔️: } else {
1634 // RDKit✔️✔️: pair.n2++;
1635 // RDKit✔️✔️: }
1636 // RDKit✔️✔️:
1637 // RDKit✔️✔️: #if 0
1638 // RDKit✔️✔️: std::cerr<<" **** np: "<< prev_n1<<","<<prev_n2<<std::endl;
1639 // RDKit✔️✔️: std::cerr<<"in_1 ";
1640 // RDKit✔️✔️: for(unsigned int i=0;i<n1;++i){
1641 // RDKit✔️✔️: std::cerr<<"("<<in_1[i]<<","<<out_1[i]<<"), ";
1642 // RDKit✔️✔️: }
1643 // RDKit✔️✔️: std::cerr<<std::endl;
1644 // RDKit✔️✔️: std::cerr<<"in_2 ";
1645 // RDKit✔️✔️: for(unsigned int i=0;i<n2;++i){
1646 // RDKit✔️✔️: std::cerr<<"("<<in_2[i]<<","<<out_2[i]<<"), ";
1647 // RDKit✔️✔️: }
1648 // RDKit✔️✔️: std::cerr<<std::endl;
1649 // RDKit✔️✔️: #endif
1650 // RDKit✔️✔️: if (t1_len > core_len && t2_len > core_len) {
1651 // RDKit✔️✔️: while (pair.n1 < n1 &&
1652 // RDKit✔️✔️: (core_1[pair.n1] != NULL_NODE || term_1[pair.n1] == 0)) {
1653 // RDKit✔️✔️: pair.n1++;
1654 // RDKit✔️✔️: pair.n2 = 0;
1655 // RDKit✔️✔️: }
1656 // RDKit✔️✔️:
1657 // RDKit✔️✔️: /* Initialize VF2 Plus neighbor iterator.
1658 // RDKit✔️✔️: * The next query node (pair.n1) has been selected from the terminal
1659 // RDKit✔️✔️: * set and is therefore adjacent to an already mapped atom (in
1660 // RDKit✔️✔️: * core_1). Rather than select pair.n2 from all atoms (0...n2) we can
1661 // RDKit✔️✔️: * select it from the neighbors of this mapped atom (0...deg(nbor))
1662 // RDKit✔️✔️: * since it must also be adajcent to this mapped atom!
1663 // RDKit✔️✔️: */
1664 // RDKit✔️✔️: if (!pair.hasiter) {
1665 // RDKit✔️✔️: RDK_ADJ_ITER n1iter_beg, n1iter_end;
1666 // RDKit✔️✔️: boost::tie(n1iter_beg, n1iter_end) =
1667 // RDKit✔️✔️: boost::adjacent_vertices(pair.n1, *g1);
1668 // RDKit✔️✔️:
1669 // RDKit✔️✔️: while (n1iter_beg != n1iter_end && core_1[*n1iter_beg] == NULL_NODE) {
1670 // RDKit✔️✔️: ++n1iter_beg;
1671 // RDKit✔️✔️: }
1672 // RDKit✔️✔️:
1673 // RDKit✔️✔️: assert(n1iter_beg != n1iter_end);
1674 // RDKit✔️✔️:
1675 // RDKit✔️✔️: boost::tie(pair.nbrbeg, pair.nbrend) =
1676 // RDKit✔️✔️: boost::adjacent_vertices(core_1[*n1iter_beg], *g2);
1677 // RDKit✔️✔️: pair.hasiter = true;
1678 // RDKit✔️✔️: }
1679 // RDKit✔️✔️: } else if (pair.n1 == 0 && order != nullptr) {
1680 // RDKit✔️✔️: // Optimisation: if the order vector is laid out in a DFS/BFS then this
1681 // RDKit✔️✔️: // loop can be replaced with:
1682 // RDKit✔️✔️: // pair.n1=order[core_len];
1683 // RDKit✔️✔️: // :)
1684 // RDKit✔️✔️: unsigned int i = 0;
1685 // RDKit✔️✔️: while (i < n1 && core_1[pair.n1 = order[i]] != NULL_NODE) {
1686 // RDKit✔️✔️: i++;
1687 // RDKit✔️✔️: }
1688 // RDKit✔️✔️: if (i == n1) {
1689 // RDKit✔️✔️: pair.n1 = n1;
1690 // RDKit✔️✔️: }
1691 // RDKit✔️✔️: } else {
1692 // RDKit✔️✔️: while (pair.n1 < n1 && core_1[pair.n1] != NULL_NODE) {
1693 // RDKit✔️✔️: pair.n1++;
1694 // RDKit✔️✔️: pair.n2 = 0;
1695 // RDKit✔️✔️: }
1696 // RDKit✔️✔️: }
1697 // RDKit✔️✔️:
1698 // RDKit✔️✔️: /* VF2 Plus iterator available? */
1699 // RDKit✔️✔️: if (pair.hasiter) {
1700 // RDKit✔️✔️: while (pair.nbrbeg < pair.nbrend && core_2[*pair.nbrbeg] != NULL_NODE) {
1701 // RDKit✔️✔️: ++pair.nbrbeg;
1702 // RDKit✔️✔️: }
1703 // RDKit✔️✔️:
1704 // RDKit✔️✔️: if (pair.nbrbeg < pair.nbrend) {
1705 // RDKit✔️✔️: pair.n2 = *pair.nbrbeg;
1706 // RDKit✔️✔️: ++pair.nbrbeg;
1707 // RDKit✔️✔️: } else {
1708 // RDKit✔️✔️: pair.n2 = n2;
1709 // RDKit✔️✔️: }
1710 // RDKit✔️✔️: } else if (t1_len > core_len && t2_len > core_len) {
1711 // RDKit✔️✔️: while (pair.n2 < n2 &&
1712 // RDKit✔️✔️: (core_2[pair.n2] != NULL_NODE || term_2[pair.n2] == 0)) {
1713 // RDKit✔️✔️: pair.n2++;
1714 // RDKit✔️✔️: }
1715 // RDKit✔️✔️: } else {
1716 // RDKit✔️✔️: while (pair.n2 < n2 && core_2[pair.n2] != NULL_NODE) {
1717 // RDKit✔️✔️: pair.n2++;
1718 // RDKit✔️✔️: }
1719 // RDKit✔️✔️: }
1720 // RDKit✔️✔️: return pair.n1 < n1 && pair.n2 < n2;
1721 // RDKit✔️✔️: }
1722 // Complexity review: both versions scan at most O(V) unmapped nodes
1723 // outside the terminal branch and O(degree) adjacency entries in the
1724 // VF2+ branch, with no allocation per candidate pair.
1725 // RDKit✔️✔️: if (pair.n1 == NULL_NODE) pair.n1 = 0;
1726 // RDKit✔️✔️: if (pair.n2 == NULL_NODE) pair.n2 = 0;
1727 // RDKit✔️✔️: else pair.n2++;
1728 if pair.n1 == NULL_NODE {
1729 pair.n1 = 0;
1730 }
1731 if pair.n2 == NULL_NODE {
1732 pair.n2 = 0;
1733 } else {
1734 pair.n2 += 1;
1735 }
1736
1737 // --- Select query node (n1) ---
1738 // RDKit✔️✔️: if (t1_len > core_len && t2_len > core_len) {
1739 if self.t1_len > self.core_len && self.t2_len > self.core_len {
1740 // RDKit✔️✔️: while (pair.n1 < n1 &&
1741 // RDKit✔️✔️: (core_1[pair.n1] != NULL_NODE || term_1[pair.n1] == 0)) {
1742 // RDKit✔️✔️: pair.n1++; pair.n2 = 0;
1743 // RDKit✔️✔️: }
1744 while pair.n1 < self.n1
1745 && (self.core_1[pair.n1] != NULL_NODE || self.term_1[pair.n1] == 0)
1746 {
1747 pair.n1 += 1;
1748 pair.n2 = 0;
1749 }
1750 // RDKit✔️✔️: /* Initialize VF2 Plus neighbor iterator.
1751 // RDKit✔️✔️: * The next query node (pair.n1) has been selected from the terminal
1752 // RDKit✔️✔️: * set and is therefore adjacent to an already mapped atom (in
1753 // RDKit✔️✔️: * core_1). Rather than select pair.n2 from all atoms (0...n2) we can
1754 // RDKit✔️✔️: * select it from the neighbors of this mapped atom (0...deg(nbor))
1755 // RDKit✔️✔️: * since it must also be adajcent to this mapped atom!
1756 // RDKit✔️✔️: */
1757 // RDKit✔️✔️: if (!pair.hasiter) {
1758 // RDKit✔️✔️: boost::tie(n1iter_beg, n1iter_end) =
1759 // RDKit✔️✔️: boost::adjacent_vertices(pair.n1, *g1);
1760 // RDKit✔️✔️: while (n1iter_beg != n1iter_end && core_1[*n1iter_beg] == NULL_NODE) {
1761 // RDKit✔️✔️: ++n1iter_beg;
1762 // RDKit✔️✔️: }
1763 // RDKit✔️✔️: assert(n1iter_beg != n1iter_end);
1764 // RDKit✔️✔️: boost::tie(pair.nbrbeg, pair.nbrend) =
1765 // RDKit✔️✔️: boost::adjacent_vertices(core_1[*n1iter_beg], *g2);
1766 // RDKit✔️✔️: pair.hasiter = true;
1767 // RDKit✔️✔️: }
1768 if !pair.hasiter {
1769 let mut mapped_terminal_neighbor = NULL_NODE;
1770 for &(query_neighbor, _) in self.g1.out_edges(pair.n1) {
1771 if self.core_1[query_neighbor] != NULL_NODE {
1772 mapped_terminal_neighbor = self.core_1[query_neighbor];
1773 break;
1774 }
1775 }
1776 debug_assert_ne!(mapped_terminal_neighbor, NULL_NODE);
1777 if mapped_terminal_neighbor != NULL_NODE {
1778 pair.nbr_node = mapped_terminal_neighbor;
1779 pair.nbr_cursor = 0;
1780 pair.nbr_end = self.g2.out_edges(mapped_terminal_neighbor).len();
1781 pair.hasiter = true;
1782 }
1783 }
1784 } else if pair.n1 == 0 {
1785 // RDKit✔️✔️: } else if (pair.n1 == 0 && order != nullptr) {
1786 if let Some(order) = &self.order {
1787 // RDKit✔️✔️: unsigned int i = 0;
1788 // RDKit✔️✔️: while (i < n1 && core_1[pair.n1 = order[i]] != NULL_NODE) { i++; }
1789 // RDKit✔️✔️: if (i == n1) pair.n1 = n1;
1790 let mut i = 0;
1791 while i < self.n1 {
1792 let candidate = order[i];
1793 if self.core_1[candidate] == NULL_NODE {
1794 pair.n1 = candidate;
1795 break;
1796 }
1797 i += 1;
1798 }
1799 if i == self.n1 {
1800 pair.n1 = self.n1;
1801 }
1802 } else {
1803 // RDKit✔️✔️: } else {
1804 // RDKit✔️✔️: while (pair.n1 < n1 && core_1[pair.n1] != NULL_NODE) {
1805 // RDKit✔️✔️: pair.n1++; pair.n2 = 0;
1806 // RDKit✔️✔️: }
1807 while pair.n1 < self.n1 && self.core_1[pair.n1] != NULL_NODE {
1808 pair.n1 += 1;
1809 pair.n2 = 0;
1810 }
1811 }
1812 } else {
1813 // RDKit✔️✔️: } else {
1814 // RDKit✔️✔️: while (pair.n1 < n1 && core_1[pair.n1] != NULL_NODE) {
1815 // RDKit✔️✔️: pair.n1++; pair.n2 = 0;
1816 // RDKit✔️✔️: }
1817 while pair.n1 < self.n1 && self.core_1[pair.n1] != NULL_NODE {
1818 pair.n1 += 1;
1819 pair.n2 = 0;
1820 }
1821 }
1822
1823 // --- Select mol node (n2) ---
1824 // RDKit✔️✔️: if (pair.hasiter) { ... }
1825 if pair.hasiter {
1826 // RDKit✔️✔️: while (pair.nbrbeg < pair.nbrend && core_2[*pair.nbrbeg] != NULL_NODE) {
1827 // RDKit✔️✔️: ++pair.nbrbeg;
1828 // RDKit✔️✔️: }
1829 let neighbors = self.g2.out_edges(pair.nbr_node);
1830 while pair.nbr_cursor < pair.nbr_end
1831 && self.core_2[neighbors[pair.nbr_cursor].0] != NULL_NODE
1832 {
1833 pair.nbr_cursor += 1;
1834 }
1835 // RDKit✔️✔️: if (pair.nbrbeg < pair.nbrend) {
1836 // RDKit✔️✔️: pair.n2 = *pair.nbrbeg;
1837 // RDKit✔️✔️: ++pair.nbrbeg;
1838 // RDKit✔️✔️: } else {
1839 // RDKit✔️✔️: pair.n2 = n2;
1840 // RDKit✔️✔️: }
1841 if pair.nbr_cursor < pair.nbr_end {
1842 pair.n2 = neighbors[pair.nbr_cursor].0;
1843 pair.nbr_cursor += 1;
1844 } else {
1845 pair.n2 = self.n2;
1846 }
1847 } else if self.t1_len > self.core_len && self.t2_len > self.core_len {
1848 // RDKit✔️✔️: } else if (t1_len > core_len && t2_len > core_len) {
1849 // RDKit✔️✔️: while (pair.n2 < n2 &&
1850 // RDKit✔️✔️: (core_2[pair.n2] != NULL_NODE || term_2[pair.n2] == 0)) {
1851 // RDKit✔️✔️: pair.n2++;
1852 // RDKit✔️✔️: }
1853 while pair.n2 < self.n2
1854 && (self.core_2[pair.n2] != NULL_NODE || self.term_2[pair.n2] == 0)
1855 {
1856 pair.n2 += 1;
1857 }
1858 } else {
1859 // RDKit✔️✔️: } else {
1860 // RDKit✔️✔️: while (pair.n2 < n2 && core_2[pair.n2] != NULL_NODE) { pair.n2++; }
1861 // RDKit✔️✔️: }
1862 while pair.n2 < self.n2 && self.core_2[pair.n2] != NULL_NODE {
1863 pair.n2 += 1;
1864 }
1865 }
1866
1867 // RDKit✔️✔️: return pair.n1 < n1 && pair.n2 < n2;
1868 pair.n1 < self.n1 && pair.n2 < self.n2
1869 }
1870
1871 // RDKit source (vf2.hpp), IsFeasiblePair:
1872 // bool IsFeasiblePair(node_id node1, node_id node2) {
1873 // assert(node1 < n1); assert(node2 < n2);
1874 // assert(core_1[node1] == NULL_NODE); assert(core_2[node2] == NULL_NODE);
1875 //
1876 // // O(1) check for adjacency list
1877 // if (boost::out_degree(node1, *g1) > boost::out_degree(node2, *g2)) {
1878 // return false;
1879 // }
1880 // if (!vc(node1, node2)) { return false; }
1881 //
1882 // unsigned int other1, other2;
1883 // // Check the out edges of node1
1884 // typename Graph::out_edge_iterator bNbrs, eNbrs;
1885 // boost::tie(bNbrs, eNbrs) = boost::out_edges(node1, *g1);
1886 // while (bNbrs != eNbrs) {
1887 // other1 = getOtherIdx(*g1, *bNbrs, node1);
1888 // if (core_1[other1] != NULL_NODE) {
1889 // other2 = core_1[other1];
1890 // typename Graph::edge_descriptor oEdge;
1891 // bool found;
1892 // boost::tie(oEdge, found) = boost::edge(node2, other2, *g2);
1893 // if (!found || !ec(*bNbrs, oEdge)) { return false; }
1894 // }
1895 // ++bNbrs;
1896 // }
1897 // return true;
1898 // }
1899
1900 /// RDKit✔️❌: IsFeasiblePair — check if (node1, node2) can be added.
1901 ///
1902 /// Performs degree check, vertex compatibility, and edge compatibility
1903 /// for already-matched neighbors. RDK_VF2_PRUNING (terminal count
1904 /// pre-check) is not enabled — the C++ code also has it behind an
1905 /// ifdef that is not defined at the top of vf2.hpp.
1906 fn is_feasible_pair(
1907 &self,
1908 node1: NodeId,
1909 node2: NodeId,
1910 atom_fn: &impl Fn(usize, usize) -> bool,
1911 bond_fn: &impl Fn(usize, usize) -> bool,
1912 ) -> bool {
1913 // RDKit✔️✔️: bool IsFeasiblePair(node_id node1, node_id node2) {
1914 // RDKit✔️✔️: assert(node1 < n1);
1915 // RDKit✔️✔️: assert(node2 < n2);
1916 // RDKit✔️✔️: assert(core_1[node1] == NULL_NODE);
1917 // RDKit✔️✔️: assert(core_2[node2] == NULL_NODE);
1918 // RDKit✔️✔️:
1919 // RDKit✔️✔️: // std::cerr<<" ifp:"<<node1<<"-"<<node2<<"
1920 // RDKit✔️✔️: // "<<vs_compared->size()<<std::endl;
1921 // RDKit✔️✔️: // int &isCompat=vs_compared[node1*n2+node2];
1922 // RDKit✔️✔️: // if(isCompat==0){
1923 // RDKit✔️✔️: // isCompat=vc(node1,node2)?1:-1;
1924 // RDKit✔️✔️: // }
1925 // RDKit✔️✔️: // if( isCompat<0 ){
1926 // RDKit✔️✔️: // //std::cerr<<" short1"<<std::endl;
1927 // RDKit✔️✔️: // return false;
1928 // RDKit✔️✔️: // }
1929 // RDKit✔️✔️:
1930 // RDKit✔️✔️: // O(1) check for adjacency list
1931 // RDKit✔️✔️: if (boost::out_degree(node1, *g1) > boost::out_degree(node2, *g2)) {
1932 // RDKit✔️✔️: return false;
1933 // RDKit✔️✔️: }
1934 // RDKit✔️✔️: if (!vc(node1, node2)) {
1935 // RDKit✔️✔️: return false;
1936 // RDKit✔️✔️: }
1937 // RDKit✔️✔️:
1938 // RDKit✔️✔️: unsigned int other1, other2;
1939 // RDKit✔️✔️: #ifdef RDK_VF2_PRUNING
1940 // RDKit✔️✔️: unsigned int term1 = 0, term2 = 0;
1941 // RDKit✔️✔️: unsigned int new1 = 0, new2 = 0;
1942 // RDKit✔️✔️: #endif
1943 // RDKit✔️✔️:
1944 // RDKit✔️✔️: // Check the out edges of node1
1945 // RDKit✔️✔️: typename Graph::out_edge_iterator bNbrs, eNbrs;
1946 // RDKit✔️✔️: boost::tie(bNbrs, eNbrs) = boost::out_edges(node1, *g1);
1947 // RDKit✔️✔️: while (bNbrs != eNbrs) {
1948 // RDKit✔️✔️: other1 = getOtherIdx(*g1, *bNbrs, node1);
1949 // RDKit✔️✔️: if (core_1[other1] != NULL_NODE) {
1950 // RDKit✔️✔️: other2 = core_1[other1];
1951 // RDKit✔️✔️: typename Graph::edge_descriptor oEdge;
1952 // RDKit✔️✔️: bool found;
1953 // RDKit✔️✔️: boost::tie(oEdge, found) = boost::edge(node2, other2, *g2);
1954 // RDKit✔️✔️: if (!found || !ec(*bNbrs, oEdge)) {
1955 // RDKit✔️✔️: // std::cerr<<" short2"<<std::endl;
1956 // RDKit✔️✔️: return false;
1957 // RDKit✔️✔️: }
1958 // RDKit✔️✔️: }
1959 // RDKit✔️✔️: #ifdef RDK_VF2_PRUNING
1960 // RDKit✔️✔️: else {
1961 // RDKit✔️✔️: if (term_1[other1]) ++term1;
1962 // RDKit✔️✔️: if (!term_1[other1]) ++new1;
1963 // RDKit✔️✔️: }
1964 // RDKit✔️✔️: #endif
1965 // RDKit✔️✔️: ++bNbrs;
1966 // RDKit✔️✔️: }
1967 // RDKit✔️✔️:
1968 // RDKit✔️✔️: #ifdef RDK_VF2_PRUNING
1969 // RDKit✔️✔️: // Check the out edges of node2
1970 // RDKit✔️✔️: boost::tie(bNbrs, eNbrs) = boost::out_edges(node2, *g2);
1971 // RDKit✔️✔️: while (bNbrs != eNbrs) {
1972 // RDKit✔️✔️: other2 = getOtherIdx(*g2, *bNbrs, node2);
1973 // RDKit✔️✔️: if (core_2[other2] != NULL_NODE) {
1974 // RDKit✔️✔️: // do nothing
1975 // RDKit✔️✔️: } else {
1976 // RDKit✔️✔️: if (term_2[other2]) ++term2;
1977 // RDKit✔️✔️: if (!term_2[other2]) ++new2;
1978 // RDKit✔️✔️: }
1979 // RDKit✔️✔️: ++bNbrs;
1980 // RDKit✔️✔️: }
1981 // RDKit✔️✔️: // std::cerr<<(termin1 <= termin2 && termout1 <= termout2 &&
1982 // RDKit✔️✔️: // (termin1+termout1+new1)<=(termin2+termout2+new2))<<std::endl;
1983 // RDKit✔️✔️:
1984 // RDKit✔️✔️: // n.b. term1+new1 == boost::out_degree(node1) and
1985 // RDKit✔️✔️: // term2+new2 == boost::out_degree(node2)
1986 // RDKit✔️✔️: return term1 <= term2 && (term1 + new1) <= (term2 + new2);
1987 // RDKit✔️✔️: #else
1988 // RDKit✔️✔️: return true;
1989 // RDKit✔️✔️: #endif
1990 // RDKit✔️✔️: }
1991 // Complexity review: both active builds do O(1) degree and vertex
1992 // checks, scan O(degree(node1)) query edges, and perform target edge
1993 // lookup in O(degree(node2)); neither allocates per candidate.
1994 // RDKit✔️✔️: assert(node1 < n1); assert(node2 < n2);
1995 // RDKit✔️✔️: assert(core_1[node1] == NULL_NODE);
1996 // RDKit✔️✔️: assert(core_2[node2] == NULL_NODE);
1997 debug_assert!(node1 < self.n1);
1998 debug_assert!(node2 < self.n2);
1999 debug_assert_eq!(self.core_1[node1], NULL_NODE);
2000 debug_assert_eq!(self.core_2[node2], NULL_NODE);
2001 if self.core_1[node1] != NULL_NODE || self.core_2[node2] != NULL_NODE {
2002 return false;
2003 }
2004
2005 // RDKit✔️✔️: if (boost::out_degree(node1, *g1) > boost::out_degree(node2, *g2)) {
2006 // RDKit✔️✔️: return false;
2007 // RDKit✔️✔️: }
2008 if self.g1.out_degree(node1) > self.g2.out_degree(node2) {
2009 return false;
2010 }
2011
2012 // RDKit✔️✔️: if (!vc(node1, node2)) { return false; }
2013 if !atom_fn(node1, node2) {
2014 return false;
2015 }
2016
2017 // RDKit✔️✔️: // Check the out edges of node1
2018 // RDKit✔️✔️: boost::tie(bNbrs, eNbrs) = boost::out_edges(node1, *g1);
2019 // RDKit✔️✔️: while (bNbrs != eNbrs) {
2020 // RDKit✔️✔️: other1 = getOtherIdx(*g1, *bNbrs, node1);
2021 // RDKit✔️✔️: if (core_1[other1] != NULL_NODE) {
2022 // RDKit✔️✔️: other2 = core_1[other1];
2023 // RDKit✔️✔️: if (!found || !ec(*bNbrs, oEdge)) { return false; }
2024 // RDKit✔️✔️: }
2025 // RDKit✔️✔️: ++bNbrs;
2026 // RDKit✔️✔️: }
2027 for &(_, edge_idx1) in self.g1.out_edges(node1) {
2028 let other1 = get_other_idx(self.g1, edge_idx1, node1);
2029 if other1 == node1 {
2030 continue;
2031 }
2032 if self.core_1[other1] != NULL_NODE {
2033 let other2 = self.core_1[other1];
2034 // Check that (node2, other2) has a matching bond.
2035 let bond_found = self.find_bond(node2, other2);
2036 match bond_found {
2037 Some(edge_idx2) => {
2038 if !bond_fn(edge_idx1, edge_idx2) {
2039 return false;
2040 }
2041 }
2042 None => return false,
2043 }
2044 }
2045 }
2046
2047 true
2048 }
2049
2050 /// Find a bond between atom `a` and `b` in the molecule graph (g2).
2051 fn find_bond(&self, a: NodeId, b: NodeId) -> Option<usize> {
2052 for &(nbr, bond_idx) in self.g2.out_edges(a) {
2053 if nbr == b {
2054 return Some(bond_idx);
2055 }
2056 }
2057 None
2058 }
2059
2060 fn add_pair(&mut self, node1: NodeId, node2: NodeId) {
2061 // RDKit✔️✔️: void AddPair(node_id node1, node_id node2) {
2062 // RDKit✔️✔️: assert(node1 < n1);
2063 // RDKit✔️✔️: assert(node2 < n2);
2064 // RDKit✔️✔️: assert(core_len < n1);
2065 // RDKit✔️✔️: assert(core_len < n2);
2066 // RDKit✔️✔️:
2067 // RDKit✔️✔️: ++core_len;
2068 // RDKit✔️✔️: if (!term_1[node1]) {
2069 // RDKit✔️✔️: term_1[node1] = core_len;
2070 // RDKit✔️✔️: ++t1_len;
2071 // RDKit✔️✔️: }
2072 // RDKit✔️✔️:
2073 // RDKit✔️✔️: if (!term_2[node2]) {
2074 // RDKit✔️✔️: term_2[node2] = core_len;
2075 // RDKit✔️✔️: ++t2_len;
2076 // RDKit✔️✔️: }
2077 // RDKit✔️✔️:
2078 // RDKit✔️✔️: core_1[node1] = node2;
2079 // RDKit✔️✔️: core_2[node2] = node1;
2080 // RDKit✔️✔️:
2081 // RDKit✔️✔️: typename Graph::out_edge_iterator bNbrs, eNbrs;
2082 // RDKit✔️✔️: // FIX: this is explicitly ignoring directionality
2083 // RDKit✔️✔️: boost::tie(bNbrs, eNbrs) = boost::out_edges(node1, *g1);
2084 // RDKit✔️✔️: while (bNbrs != eNbrs) {
2085 // RDKit✔️✔️: unsigned int other = getOtherIdx(*g1, *bNbrs, node1);
2086 // RDKit✔️✔️: if (!term_1[other]) {
2087 // RDKit✔️✔️: term_1[other] = core_len;
2088 // RDKit✔️✔️: ++t1_len;
2089 // RDKit✔️✔️: }
2090 // RDKit✔️✔️: ++bNbrs;
2091 // RDKit✔️✔️: }
2092 // RDKit✔️✔️:
2093 // RDKit✔️✔️: // FIX: this is explicitly ignoring directionality
2094 // RDKit✔️✔️: boost::tie(bNbrs, eNbrs) = boost::out_edges(node2, *g2);
2095 // RDKit✔️✔️: while (bNbrs != eNbrs) {
2096 // RDKit✔️✔️: unsigned int other = getOtherIdx(*g2, *bNbrs, node2);
2097 // RDKit✔️✔️: if (!term_2[other]) {
2098 // RDKit✔️✔️: term_2[other] = core_len;
2099 // RDKit✔️✔️: ++t2_len;
2100 // RDKit✔️✔️: }
2101 // RDKit✔️✔️: ++bNbrs;
2102 // RDKit✔️✔️: }
2103 // RDKit✔️✔️: }
2104 // Complexity review: both versions update O(1) mapping fields and scan
2105 // each selected node's adjacency once in O(degree1 + degree2), without
2106 // allocation or whole-graph rescanning.
2107 debug_assert!(node1 < self.n1);
2108 debug_assert!(node2 < self.n2);
2109 debug_assert!(self.core_len < self.n1);
2110 debug_assert!(self.core_len < self.n2);
2111 // RDKit✔️✔️: ++core_len;
2112 self.core_len += 1;
2113 let depth = self.core_len;
2114
2115 // RDKit✔️✔️: if (!term_1[node1]) { term_1[node1] = core_len; ++t1_len; }
2116 if self.term_1[node1] == 0 {
2117 self.term_1[node1] = depth;
2118 self.t1_len += 1;
2119 }
2120
2121 // RDKit✔️✔️: if (!term_2[node2]) { term_2[node2] = core_len; ++t2_len; }
2122 if self.term_2[node2] == 0 {
2123 self.term_2[node2] = depth;
2124 self.t2_len += 1;
2125 }
2126
2127 // RDKit✔️✔️: core_1[node1] = node2; core_2[node2] = node1;
2128 self.core_1[node1] = node2;
2129 self.core_2[node2] = node1;
2130
2131 // RDKit✔️✔️: // FIX: explicitly ignoring directionality
2132 // RDKit✔️✔️: boost::tie(bNbrs, eNbrs) = boost::out_edges(node1, *g1);
2133 // RDKit✔️✔️: while (bNbrs != eNbrs) {
2134 // RDKit✔️✔️: unsigned int other = getOtherIdx(*g1, *bNbrs, node1);
2135 // RDKit✔️✔️: if (!term_1[other]) { term_1[other] = core_len; ++t1_len; }
2136 // RDKit✔️✔️: ++bNbrs;
2137 // RDKit✔️✔️: }
2138 for &(_, edge) in self.g1.out_edges(node1) {
2139 let other = get_other_idx(self.g1, edge, node1);
2140 if other == node1 {
2141 continue;
2142 }
2143 if self.term_1[other] == 0 {
2144 self.term_1[other] = depth;
2145 self.t1_len += 1;
2146 }
2147 }
2148
2149 // RDKit✔️✔️: boost::tie(bNbrs, eNbrs) = boost::out_edges(node2, *g2);
2150 // RDKit✔️✔️: while (bNbrs != eNbrs) {
2151 // RDKit✔️✔️: unsigned int other = getOtherIdx(*g2, *bNbrs, node2);
2152 // RDKit✔️✔️: if (!term_2[other]) { term_2[other] = core_len; ++t2_len; }
2153 // RDKit✔️✔️: ++bNbrs;
2154 // RDKit✔️✔️: }
2155 for &(_, edge) in self.g2.out_edges(node2) {
2156 let other = get_other_idx(self.g2, edge, node2);
2157 if other == node2 {
2158 continue;
2159 }
2160 if self.term_2[other] == 0 {
2161 self.term_2[other] = depth;
2162 self.t2_len += 1;
2163 }
2164 }
2165 }
2166
2167 fn back_track(&mut self, node1: NodeId, node2: NodeId) {
2168 // RDKit✔️✔️: void BackTrack(node_id node1, node_id node2) {
2169 // RDKit✔️✔️: if (term_1[node1] == core_len) {
2170 // RDKit✔️✔️: term_1[node1] = 0;
2171 // RDKit✔️✔️: --t1_len;
2172 // RDKit✔️✔️: }
2173 // RDKit✔️✔️:
2174 // RDKit✔️✔️: typename Graph::out_edge_iterator bNbrs, eNbrs;
2175 // RDKit✔️✔️: boost::tie(bNbrs, eNbrs) = boost::out_edges(node1, *g1);
2176 // RDKit✔️✔️: while (bNbrs != eNbrs) {
2177 // RDKit✔️✔️: unsigned int other = getOtherIdx(*g1, *bNbrs, node1);
2178 // RDKit✔️✔️: if (term_1[other] == core_len) {
2179 // RDKit✔️✔️: term_1[other] = 0;
2180 // RDKit✔️✔️: --t1_len;
2181 // RDKit✔️✔️: }
2182 // RDKit✔️✔️: ++bNbrs;
2183 // RDKit✔️✔️: }
2184 // RDKit✔️✔️:
2185 // RDKit✔️✔️: if (term_2[node2] == core_len) {
2186 // RDKit✔️✔️: term_2[node2] = 0;
2187 // RDKit✔️✔️: --t2_len;
2188 // RDKit✔️✔️: }
2189 // RDKit✔️✔️:
2190 // RDKit✔️✔️: boost::tie(bNbrs, eNbrs) = boost::out_edges(node2, *g2);
2191 // RDKit✔️✔️: while (bNbrs != eNbrs) {
2192 // RDKit✔️✔️: unsigned int other = getOtherIdx(*g2, *bNbrs, node2);
2193 // RDKit✔️✔️: if (term_2[other] == core_len) {
2194 // RDKit✔️✔️: term_2[other] = 0;
2195 // RDKit✔️✔️: --t2_len;
2196 // RDKit✔️✔️: }
2197 // RDKit✔️✔️: ++bNbrs;
2198 // RDKit✔️✔️: }
2199 // RDKit✔️✔️:
2200 // RDKit✔️✔️: core_1[node1] = NULL_NODE;
2201 // RDKit✔️✔️: core_2[node2] = NULL_NODE;
2202 // RDKit✔️✔️: --core_len;
2203 // RDKit✔️✔️: }
2204 // Complexity review: both versions scan each removed node's adjacency
2205 // once in O(degree1 + degree2), mutate depth-tagged entries in place,
2206 // and allocate no temporary collections.
2207 let depth = self.core_len;
2208
2209 // RDKit✔️✔️: if (term_1[node1] == core_len) { term_1[node1] = 0; --t1_len; }
2210 if self.term_1[node1] == depth {
2211 self.term_1[node1] = 0;
2212 self.t1_len -= 1;
2213 }
2214
2215 // RDKit✔️✔️: boost::tie(bNbrs, eNbrs) = boost::out_edges(node1, *g1);
2216 // RDKit✔️✔️: while (bNbrs != eNbrs) {
2217 // RDKit✔️✔️: unsigned int other = getOtherIdx(*g1, *bNbrs, node1);
2218 // RDKit✔️✔️: if (term_1[other] == core_len) { term_1[other] = 0; --t1_len; }
2219 // RDKit✔️✔️: ++bNbrs;
2220 // RDKit✔️✔️: }
2221 for &(_, edge) in self.g1.out_edges(node1) {
2222 let other = get_other_idx(self.g1, edge, node1);
2223 if other == node1 {
2224 continue;
2225 }
2226 if self.term_1[other] == depth {
2227 self.term_1[other] = 0;
2228 self.t1_len -= 1;
2229 }
2230 }
2231
2232 // RDKit✔️✔️: if (term_2[node2] == core_len) { term_2[node2] = 0; --t2_len; }
2233 if self.term_2[node2] == depth {
2234 self.term_2[node2] = 0;
2235 self.t2_len -= 1;
2236 }
2237
2238 // RDKit✔️✔️: boost::tie(bNbrs, eNbrs) = boost::out_edges(node2, *g2);
2239 // RDKit✔️✔️: while (bNbrs != eNbrs) {
2240 // RDKit✔️✔️: unsigned int other = getOtherIdx(*g2, *bNbrs, node2);
2241 // RDKit✔️✔️: if (term_2[other] == core_len) { term_2[other] = 0; --t2_len; }
2242 // RDKit✔️✔️: ++bNbrs;
2243 // RDKit✔️✔️: }
2244 for &(_, edge) in self.g2.out_edges(node2) {
2245 let other = get_other_idx(self.g2, edge, node2);
2246 if other == node2 {
2247 continue;
2248 }
2249 if self.term_2[other] == depth {
2250 self.term_2[other] = 0;
2251 self.t2_len -= 1;
2252 }
2253 }
2254
2255 // RDKit✔️✔️: core_1[node1] = NULL_NODE;
2256 // RDKit✔️✔️: core_2[node2] = NULL_NODE;
2257 // RDKit✔️✔️: --core_len;
2258 self.core_1[node1] = NULL_NODE;
2259 self.core_2[node2] = NULL_NODE;
2260 self.core_len -= 1;
2261 }
2262
2263 fn get_core_set(&self) -> (Vec<NodeId>, Vec<NodeId>) {
2264 // RDKit✔️❌: void GetCoreSet(node_id c1[], node_id c2[]) {
2265 // RDKit✔️❌: unsigned int i, j;
2266 // RDKit✔️❌: for (i = 0, j = 0; i < n1; ++i) {
2267 // RDKit✔️❌: if (core_1[i] != NULL_NODE) {
2268 // RDKit✔️❌: c1[j] = i;
2269 // RDKit✔️❌: c2[j] = core_1[i];
2270 // RDKit✔️❌: ++j;
2271 // RDKit✔️❌: }
2272 // RDKit✔️❌: }
2273 // RDKit✔️❌: }
2274 // Complexity review: both scan n1 entries in O(V) and write core_len
2275 // outputs. Rust allocates two result Vecs here, whereas RDKit writes
2276 // into caller-provided arrays, so repeated goal checks pay two extra
2277 // allocations despite identical mapping order and asymptotic cost.
2278 let mut c1 = Vec::with_capacity(self.core_len);
2279 let mut c2 = Vec::with_capacity(self.core_len);
2280 for i in 0..self.n1 {
2281 if self.core_1[i] != NULL_NODE {
2282 c1.push(i);
2283 c2.push(self.core_1[i]);
2284 }
2285 }
2286 (c1, c2)
2287 }
2288
2289 fn match_one(
2290 &mut self,
2291 atom_fn: &impl Fn(usize, usize) -> bool,
2292 bond_fn: &impl Fn(usize, usize) -> bool,
2293 mut match_check: Option<&mut impl FnMut(&[NodeId], &[NodeId]) -> bool>,
2294 ) -> Option<(Vec<NodeId>, Vec<NodeId>)> {
2295 // RDKit✔️❌: bool Match(node_id c1[], node_id c2[]) {
2296 // RDKit✔️❌: if (IsGoal()) {
2297 // RDKit✔️❌: GetCoreSet(c1, c2);
2298 // RDKit✔️❌: if (MatchChecks(c1, c2)) {
2299 // RDKit✔️❌: return true;
2300 // RDKit✔️❌: }
2301 // RDKit✔️❌: }
2302 // RDKit✔️❌:
2303 // RDKit✔️❌: if (IsDead()) {
2304 // RDKit✔️❌: return false;
2305 // RDKit✔️❌: }
2306 // RDKit✔️❌:
2307 // RDKit✔️❌: Pair<Graph> pair;
2308 // RDKit✔️❌: while (NextPair(pair)) {
2309 // RDKit✔️❌: if (IsFeasiblePair(pair.n1, pair.n2)) {
2310 // RDKit✔️❌: AddPair(pair.n1, pair.n2);
2311 // RDKit✔️❌: if (Match(c1, c2)) { // recurse
2312 // RDKit✔️❌: return true;
2313 // RDKit✔️❌: }
2314 // RDKit✔️❌: BackTrack(pair.n1, pair.n2);
2315 // RDKit✔️❌: }
2316 // RDKit✔️❌: }
2317 // RDKit✔️❌: return false;
2318 // RDKit✔️❌: }
2319 // Complexity review: candidate generation, feasibility checks, and
2320 // depth-first recursion match RDKit's search tree. The known gap is
2321 // inherited from get_core_set(), which allocates two Vecs at each goal.
2322 if self.is_goal() {
2323 let (c1, c2) = self.get_core_set();
2324 let accepted = match match_check.as_mut() {
2325 Some(check) => self.match_checks(&c1, &c2, check),
2326 None => true,
2327 };
2328 if accepted {
2329 return Some((c1, c2));
2330 }
2331 }
2332 if self.is_dead() {
2333 return None;
2334 }
2335 let mut pair = Vf2Pair::new();
2336 while self.next_pair(&mut pair) {
2337 if self.is_feasible_pair(pair.n1, pair.n2, atom_fn, bond_fn) {
2338 self.add_pair(pair.n1, pair.n2);
2339 if let Some(result) = self.match_one(atom_fn, bond_fn, match_check.as_deref_mut()) {
2340 return Some(result);
2341 }
2342 self.back_track(pair.n1, pair.n2);
2343 }
2344 }
2345 None
2346 }
2347
2348 fn match_all(
2349 &mut self,
2350 atom_fn: &impl Fn(usize, usize) -> bool,
2351 bond_fn: &impl Fn(usize, usize) -> bool,
2352 mut match_check: Option<&mut impl FnMut(&[NodeId], &[NodeId]) -> bool>,
2353 results: &mut Vec<(Vec<NodeId>, Vec<NodeId>)>,
2354 max_matches: usize,
2355 ) -> bool {
2356 // RDKit✔️❌: template <class DoubleBackInsertionSequence>
2357 // RDKit✔️❌: bool MatchAll(node_id c1[], node_id c2[], DoubleBackInsertionSequence &res,
2358 // RDKit✔️❌: unsigned int lim = 0) {
2359 // RDKit✔️❌: if (IsGoal()) {
2360 // RDKit✔️❌: GetCoreSet(c1, c2);
2361 // RDKit✔️❌: if (MatchChecks(c1, c2)) {
2362 // RDKit✔️❌: typename DoubleBackInsertionSequence::value_type newSeq;
2363 // RDKit✔️❌: newSeq.reserve(core_len);
2364 // RDKit✔️❌: for (unsigned int i = 0; i < core_len; ++i) {
2365 // RDKit✔️❌: newSeq.emplace_back(c1[i], c2[i]);
2366 // RDKit✔️❌: }
2367 // RDKit✔️❌: res.push_back(newSeq);
2368 // RDKit✔️❌: return lim && res.size() >= lim;
2369 // RDKit✔️❌: }
2370 // RDKit✔️❌: }
2371 // RDKit✔️❌:
2372 // RDKit✔️❌: if (IsDead()) {
2373 // RDKit✔️❌: return false;
2374 // RDKit✔️❌: }
2375 // RDKit✔️❌:
2376 // RDKit✔️❌: Pair<Graph> pair;
2377 // RDKit✔️❌: while (NextPair(pair)) {
2378 // RDKit✔️❌: if (IsFeasiblePair(pair.n1, pair.n2)) {
2379 // RDKit✔️❌: AddPair(pair.n1, pair.n2);
2380 // RDKit✔️❌: if (MatchAll(c1, c2, res, lim)) { // recurse
2381 // RDKit✔️❌: return true;
2382 // RDKit✔️❌: }
2383 // RDKit✔️❌: BackTrack(pair.n1, pair.n2);
2384 // RDKit✔️❌: }
2385 // RDKit✔️❌: }
2386 // RDKit✔️❌: return false;
2387 // RDKit✔️❌: }
2388 // Complexity review: the DFS search tree, candidate order, early limit,
2389 // and per-result O(core_len) storage match RDKit. The known extra cost
2390 // is get_core_set() allocating two Vecs before each final check.
2391 if self.is_goal() {
2392 let (c1, c2) = self.get_core_set();
2393 let accepted = match match_check.as_mut() {
2394 Some(check) => self.match_checks(&c1, &c2, check),
2395 None => true,
2396 };
2397 if accepted {
2398 results.push((c1, c2));
2399 return max_matches > 0 && results.len() >= max_matches;
2400 }
2401 }
2402 if self.is_dead() {
2403 return false;
2404 }
2405 let mut pair = Vf2Pair::new();
2406 while self.next_pair(&mut pair) {
2407 if self.is_feasible_pair(pair.n1, pair.n2, atom_fn, bond_fn) {
2408 self.add_pair(pair.n1, pair.n2);
2409 if self.match_all(
2410 atom_fn,
2411 bond_fn,
2412 match_check.as_deref_mut(),
2413 results,
2414 max_matches,
2415 ) {
2416 return true;
2417 }
2418 self.back_track(pair.n1, pair.n2);
2419 }
2420 }
2421 false
2422 }
2423}
2424
2425// ---------------------------------------------------------------------------
2426// VF2 recursive matching
2427// ---------------------------------------------------------------------------
2428//
2429// RDKit source (vf2.hpp):
2430// bool Match(node_id c1[], node_id c2[]) {
2431// if (IsGoal()) { GetCoreSet(c1, c2); if (MatchChecks(c1, c2)) return true; }
2432// if (IsDead()) return false;
2433// Pair<Graph> pair;
2434// while (NextPair(pair)) {
2435// if (IsFeasiblePair(pair.n1, pair.n2)) {
2436// AddPair(pair.n1, pair.n2);
2437// if (Match(c1, c2)) return true; // recurse
2438// BackTrack(pair.n1, pair.n2);
2439// }
2440// }
2441// return false;
2442// }
2443
2444/// RDKit✔️❌: Match — find first match via VF2 recursion.
2445///
2446/// Matches RDKit's `Match(c1, c2)` entry point. `match_check` allows
2447/// final verification (like MolMatchFinalCheckFunctor). If None, all
2448/// completed matches are accepted.
2449fn vf2_match(
2450 state: &mut Vf2SubState,
2451 atom_fn: &impl Fn(usize, usize) -> bool,
2452 bond_fn: &impl Fn(usize, usize) -> bool,
2453 match_check: Option<&mut impl FnMut(&[NodeId], &[NodeId]) -> bool>,
2454) -> Option<(Vec<NodeId>, Vec<NodeId>)> {
2455 // RDKit✔️❌: template <class SubState>
2456 // RDKit✔️❌: bool match(int *pn, node_id c1[], node_id c2[], SubState &s) {
2457 // RDKit✔️❌: if (s.Match(c1, c2)) {
2458 // RDKit✔️❌: // not needed, pn = num query atoms (n1)...
2459 // RDKit✔️❌: *pn = s.CoreLen();
2460 // RDKit✔️❌: return true;
2461 // RDKit✔️❌: }
2462 // RDKit✔️❌: return false;
2463 // RDKit✔️❌: }
2464 // Rust returns the mapping and its length is available directly. Complexity
2465 // and allocation behavior are exactly those of the single member core.
2466 state.match_one(atom_fn, bond_fn, match_check)
2467}
2468
2469// RDKit source (vf2.hpp), MatchAll:
2470// template <class DoubleBackInsertionSequence>
2471// bool MatchAll(node_id c1[], node_id c2[], DoubleBackInsertionSequence &res,
2472// unsigned int lim = 0) {
2473// if (IsGoal()) {
2474// GetCoreSet(c1, c2);
2475// if (MatchChecks(c1, c2)) {
2476// typename DoubleBackInsertionSequence::value_type newSeq;
2477// newSeq.reserve(core_len);
2478// for (unsigned int i = 0; i < core_len; ++i) {
2479// newSeq.emplace_back(c1[i], c2[i]);
2480// }
2481// res.push_back(newSeq);
2482// return lim && res.size() >= lim;
2483// }
2484// }
2485// if (IsDead()) return false;
2486// Pair<Graph> pair;
2487// while (NextPair(pair)) {
2488// if (IsFeasiblePair(pair.n1, pair.n2)) {
2489// AddPair(pair.n1, pair.n2);
2490// if (MatchAll(c1, c2, res, lim)) return true; // recurse
2491// BackTrack(pair.n1, pair.n2);
2492// }
2493// }
2494// return false;
2495// }
2496
2497/// RDKit✔️❌: MatchAll — find all matches up to `max_matches`.
2498///
2499/// Collects matches into `results` as (c1, c2) pairs.
2500/// Returns true when the limit has been reached, signaling the caller
2501/// to stop.
2502fn vf2_match_all(
2503 state: &mut Vf2SubState,
2504 atom_fn: &impl Fn(usize, usize) -> bool,
2505 bond_fn: &impl Fn(usize, usize) -> bool,
2506 match_check: Option<&mut impl FnMut(&[NodeId], &[NodeId]) -> bool>,
2507 results: &mut Vec<(Vec<NodeId>, Vec<NodeId>)>,
2508 max_matches: usize,
2509) -> bool {
2510 // RDKit✔️❌: template <class SubState, class DoubleBackInsertionSequence>
2511 // RDKit✔️❌: bool match(node_id c1[], node_id c2[], SubState &s,
2512 // RDKit✔️❌: DoubleBackInsertionSequence &res, unsigned int max_results) {
2513 // RDKit✔️❌: s.MatchAll(c1, c2, res, max_results);
2514 // RDKit✔️❌: return !res.empty();
2515 // RDKit✔️❌: }
2516 // Complexity review: this wrapper adds one emptiness check after invoking
2517 // the single member recursion core; it does not copy or re-enumerate results.
2518 state.match_all(atom_fn, bond_fn, match_check, results, max_matches);
2519 !results.is_empty()
2520}
2521
2522fn vf2_entry_one(
2523 g1: &Vf2Graph,
2524 g2: &Vf2Graph,
2525 atom_fn: &impl Fn(usize, usize) -> bool,
2526 bond_fn: &impl Fn(usize, usize) -> bool,
2527 match_check: Option<&mut impl FnMut(&[NodeId], &[NodeId]) -> bool>,
2528 result: &mut Vec<(NodeId, NodeId)>,
2529) -> bool {
2530 // RDKit✔️✔️: template <
2531 // RDKit✔️✔️: class Graph, class VertexLabeling // binary predicate
2532 // RDKit✔️✔️: ,
2533 // RDKit✔️✔️: class EdgeLabeling // binary predicate
2534 // RDKit✔️✔️: ,
2535 // RDKit✔️✔️: class MatchChecking // binary predicate
2536 // RDKit✔️✔️: ,
2537 // RDKit✔️✔️: class
2538 // RDKit✔️✔️: BackInsertionSequence // contains
2539 // RDKit✔️✔️: // std::pair<vertex_descriptor,vertex_descriptor>
2540 // RDKit✔️✔️: >
2541 // RDKit✔️✔️: bool vf2(const Graph &g1, const Graph &g2, VertexLabeling &vertex_labeling,
2542 // RDKit✔️✔️: EdgeLabeling &edge_labeling, MatchChecking &match_checking,
2543 // RDKit✔️✔️: BackInsertionSequence &F) {
2544 // RDKit✔️✔️: detail::VF2SubState<const Graph, VertexLabeling, EdgeLabeling, MatchChecking>
2545 // RDKit✔️✔️: s0(&g1, &g2, vertex_labeling, edge_labeling, match_checking, false);
2546 // RDKit✔️✔️: detail::node_id *ni1 = new detail::node_id[num_vertices(g1)];
2547 // RDKit✔️✔️: detail::node_id *ni2 = new detail::node_id[num_vertices(g2)];
2548 // RDKit✔️✔️: int n = 0;
2549 // RDKit✔️✔️:
2550 // RDKit✔️✔️: F.clear();
2551 // RDKit✔️✔️: if (match(&n, ni1, ni2, s0)) {
2552 // RDKit✔️✔️: auto sz = num_vertices(g1);
2553 // RDKit✔️✔️: F.reserve(sz);
2554 // RDKit✔️✔️: for (unsigned int i = 0; i < sz; ++i) {
2555 // RDKit✔️✔️: F.emplace_back(ni1[i], ni2[i]);
2556 // RDKit✔️✔️: }
2557 // RDKit✔️✔️: }
2558 // RDKit✔️✔️: delete[] ni1;
2559 // RDKit✔️✔️: delete[] ni2;
2560 // RDKit✔️✔️:
2561 // RDKit✔️✔️: return !F.empty();
2562 // RDKit✔️✔️: };
2563 // Complexity review: both allocate two O(V) mapping buffers, construct one
2564 // unsorted state, run the same first-match DFS, and fill one O(V) result.
2565 let mut state = Vf2SubState::new(g1, g2, false);
2566 result.clear();
2567 if let Some((c1, c2)) = vf2_match(&mut state, atom_fn, bond_fn, match_check) {
2568 result.reserve(c1.len());
2569 result.extend(c1.into_iter().zip(c2));
2570 }
2571 !result.is_empty()
2572}
2573
2574fn vf2_entry_all(
2575 g1: &Vf2Graph,
2576 g2: &Vf2Graph,
2577 atom_fn: &impl Fn(usize, usize) -> bool,
2578 bond_fn: &impl Fn(usize, usize) -> bool,
2579 match_check: Option<&mut impl FnMut(&[NodeId], &[NodeId]) -> bool>,
2580 results: &mut Vec<(Vec<NodeId>, Vec<NodeId>)>,
2581 max_results: usize,
2582) -> bool {
2583 // RDKit✔️❌: template <class Graph, class VertexLabeling // binary predicate
2584 // RDKit✔️❌: ,
2585 // RDKit✔️❌: class EdgeLabeling // binary predicate
2586 // RDKit✔️❌: ,
2587 // RDKit✔️❌: class MatchChecking // binary predicate
2588 // RDKit✔️❌: ,
2589 // RDKit✔️❌: class DoubleBackInsertionSequence // contains a back insertion
2590 // RDKit✔️❌: // sequence
2591 // RDKit✔️❌: >
2592 // RDKit✔️❌: bool vf2_all(const Graph &g1, const Graph &g2, VertexLabeling &vertex_labeling,
2593 // RDKit✔️❌: EdgeLabeling &edge_labeling, MatchChecking &match_checking,
2594 // RDKit✔️❌: DoubleBackInsertionSequence &F, unsigned int max_results = 1000) {
2595 // RDKit✔️❌: detail::VF2SubState<const Graph, VertexLabeling, EdgeLabeling, MatchChecking>
2596 // RDKit✔️❌: s0(&g1, &g2, vertex_labeling, edge_labeling, match_checking, false);
2597 // RDKit✔️❌: std::unique_ptr<detail::node_id[]> ni1(new detail::node_id[num_vertices(g1)]);
2598 // RDKit✔️❌: std::unique_ptr<detail::node_id[]> ni2(new detail::node_id[num_vertices(g2)]);
2599 // RDKit✔️❌:
2600 // RDKit✔️❌: F.clear();
2601 // RDKit✔️❌: F.resize(0);
2602 // RDKit✔️❌:
2603 // RDKit✔️❌: match(ni1.get(), ni2.get(), s0, F, max_results);
2604 // RDKit✔️❌:
2605 // RDKit✔️❌: return !F.empty();
2606 // RDKit✔️❌: };
2607 // Complexity review: search order and result storage match RDKit. The known
2608 // gap is the member core allocating mapping Vecs before each final check,
2609 // while RDKit reuses ni1/ni2 across goal states.
2610 let mut state = Vf2SubState::new(g1, g2, false);
2611 results.clear();
2612 vf2_match_all(
2613 &mut state,
2614 atom_fn,
2615 bond_fn,
2616 match_check,
2617 results,
2618 max_results,
2619 )
2620}
2621
2622// ---------------------------------------------------------------------------
2623// Final match check (simplified MolMatchFinalCheckFunctor)
2624// ---------------------------------------------------------------------------
2625//
2626// RDKit source (SubstructMatch.cpp):
2627// bool MolMatchFinalCheckFunctor::operator()(const std::uint32_t q_c[],
2628// const std::uint32_t m_c[]) {
2629// if (d_params.extraFinalCheck || d_params.useGenericMatchers) { ... }
2630// HashedStorageType match;
2631// if (d_params.uniquify) {
2632// match.resize(d_mol.getNumAtoms());
2633// std::fill(match.begin(), match.end(), 0);
2634// for (unsigned int i = 0; i < d_query.getNumAtoms(); ++i) {
2635// match[m_c[i]] = 1;
2636// }
2637// if (matchesSeen.find(match) != matchesSeen.end()) { return false; }
2638// }
2639// if (!d_params.useChirality) {
2640// if (d_params.uniquify) { matchesSeen.insert(match); }
2641// return true;
2642// }
2643// // ... chirality checks ...
2644// }
2645
2646/// RDKit✔️✔️: Final match atom-set mask used for uniquification.
2647fn match_mask(atom_mapping: &[usize], mol_num_atoms: usize) -> Vec<bool> {
2648 let mut mask = vec![false; mol_num_atoms];
2649 for &ma in atom_mapping {
2650 if ma < mol_num_atoms {
2651 mask[ma] = true;
2652 }
2653 }
2654 mask
2655}
2656
2657fn count_swaps_to_interconvert_i32(reference: &[i32], probe: &[i32]) -> Option<u32> {
2658 crate::source_port_helpers::count_swaps_to_interconvert(reference, probe)
2659 .ok()
2660 .and_then(|swaps| u32::try_from(swaps).ok())
2661}
2662
2663fn rdkit_atom_perturbation_order_from_bond_indices(
2664 mol: &Molecule,
2665 atom_idx: usize,
2666 probe: &[i32],
2667) -> Result<u32, SubstructMatchError> {
2668 // BEGIN RDKIT CPP FUNCTION Atom::getPerturbationOrder
2669 // RDKit✔️✔️: int Atom::getPerturbationOrder(const INT_LIST &probe) const {
2670 // RDKit✔️✔️: INT_LIST ref;
2671 // RDKit✔️✔️: for (const auto bond : getOwningMol().atomBonds(this)) {
2672 // RDKit✔️✔️: ref.push_back(bond->getIdx());
2673 // RDKit✔️✔️: }
2674 // RDKit✔️✔️: return static_cast<int>(countSwapsToInterconvert(probe, ref));
2675 // RDKit✔️✔️: }
2676 // END RDKIT CPP FUNCTION
2677 let reference: Vec<i32> = mol
2678 .topology_block()
2679 .adjacency
2680 .neighbors_of(atom_idx)
2681 .iter()
2682 .map(|neighbor| i32::try_from(neighbor.bond.index()))
2683 .collect::<Result<_, _>>()
2684 .map_err(|_| SubstructMatchError::Unsupported {
2685 branch: "MolMatchFinalCheckFunctor/Atom::getPerturbationOrder/bond-index-overflow",
2686 rdkit_function: "Atom::getPerturbationOrder",
2687 })?;
2688 count_swaps_to_interconvert_i32(probe, &reference).ok_or(SubstructMatchError::Unsupported {
2689 branch: "MolMatchFinalCheckFunctor/Atom::getPerturbationOrder/unmodeled-bond-ordering",
2690 rdkit_function: "Atom::getPerturbationOrder/countSwapsToInterconvert",
2691 })
2692}
2693
2694fn rdkit_translate_ez_label_to_cis_trans(stereo: BondStereo) -> BondStereo {
2695 match stereo {
2696 BondStereo::E => BondStereo::Trans,
2697 BondStereo::Z => BondStereo::Cis,
2698 other => other,
2699 }
2700}
2701
2702fn enhanced_stereo_is_ok(
2703 mol: &Molecule,
2704 query: &Molecule,
2705 q_to_mol: &[NodeId],
2706 mol_stereo_groups: &[Option<usize>],
2707 matches: &[Option<bool>],
2708) -> bool {
2709 // RDKit✔️✔️: bool enhancedStereoIsOK(
2710 // RDKit✔️✔️: const ROMol &mol, const ROMol &query,
2711 // RDKit✔️✔️: std::unordered_map<unsigned int, unsigned int> &q_to_mol,
2712 // RDKit✔️✔️: const std::unordered_map<unsigned int, StereoGroup const *>
2713 // RDKit✔️✔️: &molStereoGroups,
2714 // RDKit✔️✔️: const std::unordered_map<unsigned int, bool> &matches) {
2715 // RDKit✔️✔️: std::unordered_map<unsigned int, StereoGroup const *> molAtomsToQueryGroups;
2716 // RDKit✔️✔️:
2717 // RDKit✔️✔️: // If the query has stereo groups:
2718 // RDKit✔️✔️: // * OR only matches AND or OR (not absolute)
2719 // RDKit✔️✔️: // * AND only matches OR
2720 // RDKit✔️✔️: for (const auto &sg : query.getStereoGroups()) {
2721 // RDKit✔️✔️: if (sg.getGroupType() == StereoGroupType::STEREO_ABSOLUTE) {
2722 // RDKit✔️✔️: continue;
2723 // RDKit✔️✔️: }
2724 // RDKit✔️✔️: // StereoGroup const* matched_mol_group = nullptr;
2725 // RDKit✔️✔️: const bool is_and = sg.getGroupType() == StereoGroupType::STEREO_AND;
2726 // RDKit✔️✔️: for (const auto a : sg.getAtoms()) {
2727 // RDKit✔️✔️: const auto mol_group = molStereoGroups.find(q_to_mol[a->getIdx()]);
2728 // RDKit✔️✔️: if (mol_group == molStereoGroups.end()) {
2729 // RDKit✔️✔️: // group matching absolute. not ok.
2730 // RDKit✔️✔️: return false;
2731 // RDKit✔️✔️: } else if (is_and && mol_group->second->getGroupType() !=
2732 // RDKit✔️✔️: StereoGroupType::STEREO_AND) {
2733 // RDKit✔️✔️: // AND matching OR. not ok.
2734 // RDKit✔️✔️: return false;
2735 // RDKit✔️✔️: }
2736 // RDKit✔️✔️:
2737 // RDKit✔️✔️: molAtomsToQueryGroups[q_to_mol[a->getIdx()]] = &sg;
2738 // RDKit✔️✔️: }
2739 // RDKit✔️✔️: }
2740 // RDKit✔️✔️:
2741 // RDKit✔️✔️: // If the mol has stereo groups:
2742 // RDKit✔️✔️: // * All atoms must either be the same or opposite, you can't mix
2743 // RDKit✔️✔️: // * Only one stereogroup must cover all matched atoms in the mol stereo group
2744 // RDKit✔️✔️: for (const auto &sg : mol.getStereoGroups()) {
2745 // RDKit✔️✔️: if (sg.getGroupType() == StereoGroupType::STEREO_ABSOLUTE) {
2746 // RDKit✔️✔️: continue;
2747 // RDKit✔️✔️: }
2748 // RDKit✔️✔️: bool doesMatch = false;
2749 // RDKit✔️✔️: bool seen = false;
2750 // RDKit✔️✔️: StereoGroup const *QGroup = nullptr;
2751 // RDKit✔️✔️:
2752 // RDKit✔️✔️: for (const auto &a : sg.getAtoms()) {
2753 // RDKit✔️✔️: auto thisDoesMatch = matches.find(a->getIdx());
2754 // RDKit✔️✔️: if (thisDoesMatch == matches.end()) {
2755 // RDKit✔️✔️: // not matched
2756 // RDKit✔️✔️: continue;
2757 // RDKit✔️✔️: }
2758 // RDKit✔️✔️:
2759 // RDKit✔️✔️: auto pos = molAtomsToQueryGroups.find(a->getIdx());
2760 // RDKit✔️✔️: auto thisQGroup =
2761 // RDKit✔️✔️: pos == molAtomsToQueryGroups.end() ? nullptr : pos->second;
2762 // RDKit✔️✔️: if (!seen) {
2763 // RDKit✔️✔️: doesMatch = thisDoesMatch->second;
2764 // RDKit✔️✔️: QGroup = thisQGroup;
2765 // RDKit✔️✔️: seen = true;
2766 // RDKit✔️✔️: } else if (doesMatch != thisDoesMatch->second) {
2767 // RDKit✔️✔️: // diastereomer. not ok.
2768 // RDKit✔️✔️: return false;
2769 // RDKit✔️✔️: } else if (thisQGroup != QGroup) {
2770 // RDKit✔️✔️: // mix of groups in query. not ok.
2771 // RDKit✔️✔️: return false;
2772 // RDKit✔️✔️: }
2773 // RDKit✔️✔️: }
2774 // RDKit✔️✔️: }
2775 // RDKit✔️✔️:
2776 // RDKit✔️✔️: return true;
2777 // RDKit✔️✔️: }
2778 // Complexity review: both implementations allocate O(mol atoms) lookup
2779 // state and scan each query/target group member once. Vec indexing replaces
2780 // unordered-map lookup with O(1) direct indexing and no worse allocation.
2781 let mut mol_atoms_to_query_groups = vec![None; mol.num_atoms()];
2782 for (query_group_idx, group) in query.stereo_groups().iter().enumerate() {
2783 if group.kind() == StereoGroupKind::Absolute {
2784 continue;
2785 }
2786 let is_and = group.kind() == StereoGroupKind::And;
2787 for atom in group.atoms() {
2788 let mol_atom = q_to_mol[atom.index()];
2789 let Some(mol_group_idx) = mol_stereo_groups[mol_atom] else {
2790 return false;
2791 };
2792 if is_and && mol.stereo_groups()[mol_group_idx].kind() != StereoGroupKind::And {
2793 return false;
2794 }
2795 mol_atoms_to_query_groups[mol_atom] = Some(query_group_idx);
2796 }
2797 }
2798
2799 for group in mol.stereo_groups() {
2800 if group.kind() == StereoGroupKind::Absolute {
2801 continue;
2802 }
2803 let mut first: Option<(bool, Option<usize>)> = None;
2804 for atom in group.atoms() {
2805 let mol_atom = atom.index();
2806 let Some(does_match) = matches[mol_atom] else {
2807 continue;
2808 };
2809 let query_group = mol_atoms_to_query_groups[mol_atom];
2810 match first {
2811 None => first = Some((does_match, query_group)),
2812 Some((first_match, _)) if first_match != does_match => return false,
2813 Some((_, first_group)) if first_group != query_group => return false,
2814 Some(_) => {}
2815 }
2816 }
2817 }
2818 true
2819}
2820
2821struct MolMatchFinalCheckSetup {
2822 mol_stereo_groups: Vec<Option<usize>>,
2823}
2824
2825impl MolMatchFinalCheckSetup {
2826 fn new(_query: &Molecule, mol: &Molecule, params: &SubstructMatchParams) -> Self {
2827 // RDKit✔️✔️: MolMatchFinalCheckFunctor::MolMatchFinalCheckFunctor(
2828 // RDKit✔️✔️: const ROMol &query, const ROMol &mol, const SubstructMatchParameters &ps)
2829 // RDKit✔️✔️: : d_query(query), d_mol(mol), d_params(ps) {
2830 // RDKit✔️✔️: if (d_params.useEnhancedStereo) {
2831 // RDKit✔️✔️: for (const auto &sg : d_mol.getStereoGroups()) {
2832 // RDKit✔️✔️: if (sg.getGroupType() == StereoGroupType::STEREO_ABSOLUTE) {
2833 // RDKit✔️✔️: continue;
2834 // RDKit✔️✔️: }
2835 // RDKit✔️✔️: for (const auto a : sg.getAtoms()) {
2836 // RDKit✔️✔️: d_molStereoGroups[a->getIdx()] = &sg;
2837 // RDKit✔️✔️: }
2838 // RDKit✔️✔️: }
2839 // RDKit✔️✔️: }
2840 // RDKit✔️✔️: }
2841 // Complexity review: both build the group lookup once in O(mol atoms
2842 // plus non-absolute group members), then reuse it across goal checks.
2843 let mut mol_stereo_groups = vec![None; mol.num_atoms()];
2844 if params.use_enhanced_stereo {
2845 for (group_idx, group) in mol.stereo_groups().iter().enumerate() {
2846 if group.kind() == StereoGroupKind::Absolute {
2847 continue;
2848 }
2849 for atom in group.atoms() {
2850 mol_stereo_groups[atom.index()] = Some(group_idx);
2851 }
2852 }
2853 }
2854 Self { mol_stereo_groups }
2855 }
2856}
2857
2858fn find_bond_between(mol: &Molecule, begin: usize, end: usize) -> Option<&Bond> {
2859 mol.bonds().iter().find(|bond| {
2860 let b = bond.begin().index();
2861 let e = bond.end().index();
2862 (b == begin && e == end) || (b == end && e == begin)
2863 })
2864}
2865
2866fn rdkit_match_final_check(
2867 mol: &Molecule,
2868 query: &Molecule,
2869 params: &SubstructMatchParams,
2870 c1: &[NodeId],
2871 c2: &[NodeId],
2872 setup: &MolMatchFinalCheckSetup,
2873 matches_seen: &mut Vec<Vec<bool>>,
2874) -> Result<bool, SubstructMatchError> {
2875 // BEGIN RDKIT CPP FUNCTION MolMatchFinalCheckFunctor::operator()
2876 // RDKit✔️✔️: bool MolMatchFinalCheckFunctor::operator()(const std::uint32_t q_c[],
2877 // RDKit✔️✔️: const std::uint32_t m_c[]) {
2878 // RDKit✔️✔️: if (d_params.extraFinalCheck || d_params.useGenericMatchers) {
2879 // RDKit✔️✔️: const std::span<const std::uint32_t> aids(m_c, d_query.getNumAtoms());
2880 // RDKit✔️✔️: if (d_params.useGenericMatchers &&
2881 // RDKit✔️✔️: !GenericGroups::genericAtomMatcher(d_mol, d_query, aids)) {
2882 // RDKit✔️✔️: return false;
2883 // RDKit✔️✔️: }
2884 // RDKit✔️✔️: if (d_params.extraFinalCheck && !d_params.extraFinalCheck(d_mol, aids)) {
2885 // RDKit✔️✔️: return false;
2886 // RDKit✔️✔️: }
2887 // RDKit✔️✔️: }
2888 // Complexity review: this adds the source-equivalent O(Q) dispatcher and
2889 // its selected generic-group matcher only when the option is enabled.
2890 if params.use_generic_matchers && !super::generic_groups::generic_atom_matcher(mol, query, c2) {
2891 return Ok(false);
2892 }
2893 if let Some(extra_final_check) = ¶ms.extra_final_check
2894 && !extra_final_check(mol, c2)
2895 {
2896 return Ok(false);
2897 }
2898 // RDKit✔️✔️: HashedStorageType match;
2899 // RDKit✔️✔️: if (d_params.uniquify) {
2900 // RDKit✔️✔️: match.resize(d_mol.getNumAtoms());
2901 // RDKit✔️✔️: std::fill(match.begin(), match.end(), 0);
2902 // RDKit✔️✔️: for (unsigned int i = 0; i < d_query.getNumAtoms(); ++i) {
2903 // RDKit✔️✔️: match[m_c[i]] = 1;
2904 // RDKit✔️✔️: }
2905 // RDKit✔️✔️: if (matchesSeen.find(match) != matchesSeen.end()) {
2906 // RDKit✔️✔️: return false;
2907 // RDKit✔️✔️: }
2908 // RDKit✔️✔️: }
2909 let mut q_to_mol = vec![NULL_NODE; query.num_atoms()];
2910 for (&qa, &ma) in c1.iter().zip(c2.iter()) {
2911 if qa < q_to_mol.len() {
2912 q_to_mol[qa] = ma;
2913 }
2914 }
2915 let match_key = if params.uniquify {
2916 let mask = match_mask(&q_to_mol, mol.num_atoms());
2917 if matches_seen.iter().any(|existing| *existing == mask) {
2918 return Ok(false);
2919 }
2920 Some(mask)
2921 } else {
2922 None
2923 };
2924
2925 // RDKit✔️✔️: if (!d_params.useChirality) {
2926 // RDKit✔️✔️: if (d_params.uniquify) {
2927 // RDKit✔️✔️: matchesSeen.insert(match);
2928 // RDKit✔️✔️: }
2929 // RDKit✔️✔️: return true;
2930 // RDKit✔️✔️: }
2931 if !params.use_chirality {
2932 if let Some(mask) = match_key {
2933 matches_seen.push(mask);
2934 }
2935 return Ok(true);
2936 }
2937
2938 // RDKit✔️✔️: std::unordered_map<unsigned int, bool> matches;
2939 let mol_stereo_groups = &setup.mol_stereo_groups;
2940 let mut stereo_matches = vec![None; mol.num_atoms()];
2941
2942 // RDKit✔️✔️: // check chiral atoms:
2943 // RDKit✔️✔️: for (unsigned int i = 0; i < d_query.getNumAtoms(); ++i) {
2944 // RDKit✔️✔️: const Atom *qAt = d_query.getAtomWithIdx(q_c[i]);
2945 // RDKit✔️✔️: if (qAt->getDegree() < 3 || !detail::hasChiralLabel(qAt)) {
2946 // RDKit✔️✔️: continue;
2947 // RDKit✔️✔️: }
2948 for qi in 0..query.num_atoms() {
2949 let q_at = &query.atoms()[qi];
2950 if query.topology_block().adjacency.neighbors_of(qi).len() < 3 || !has_chiral_label(q_at) {
2951 continue;
2952 }
2953 let mi = q_to_mol[qi];
2954 let m_at = &mol.atoms()[mi];
2955 // RDKit✔️✔️: if (!detail::hasChiralLabel(mAt)) {
2956 // RDKit✔️✔️: if (d_params.specifiedStereoQueryMatchesUnspecified) {
2957 // RDKit✔️✔️: continue;
2958 // RDKit✔️✔️: }
2959 // RDKit✔️✔️: return false;
2960 // RDKit✔️✔️: }
2961 if !has_chiral_label(m_at) {
2962 if params.specified_stereo_query_matches_unspecified {
2963 continue;
2964 }
2965 return Ok(false);
2966 }
2967 // RDKit✔️✔️: if (qAt->getDegree() > mAt->getDegree()) {
2968 // RDKit✔️✔️: return false;
2969 // RDKit✔️✔️: }
2970 if query.topology_block().adjacency.neighbors_of(qi).len()
2971 > mol.topology_block().adjacency.neighbors_of(mi).len()
2972 {
2973 return Ok(false);
2974 }
2975
2976 // RDKit✔️✔️: INT_LIST qOrder;
2977 // RDKit✔️✔️: INT_LIST mOrder;
2978 // RDKit✔️✔️: for (unsigned int j = 0; j < d_query.getNumAtoms(); ++j) {
2979 // RDKit✔️✔️: const Bond *qB = d_query.getBondBetweenAtoms(q_c[i], q_c[j]);
2980 // RDKit✔️✔️: const Bond *mB = d_mol.getBondBetweenAtoms(m_c[i], m_c[j]);
2981 // RDKit✔️✔️: if (qB && mB) {
2982 // RDKit✔️✔️: mOrder.push_back(mB->getIdx());
2983 // RDKit✔️✔️: qOrder.push_back(qB->getIdx());
2984 // RDKit✔️✔️: if (mOrder.size() == qAt->getDegree()) {
2985 // RDKit✔️✔️: break;
2986 // RDKit✔️✔️: }
2987 // RDKit✔️✔️: }
2988 // RDKit✔️✔️: }
2989 let mut q_order: Vec<i32> = Vec::new();
2990 let mut m_order: Vec<i32> = Vec::new();
2991 for qj in 0..query.num_atoms() {
2992 let Some(q_bond) = find_bond_between(query, qi, qj) else {
2993 continue;
2994 };
2995 let mj = q_to_mol[qj];
2996 let Some(m_bond) = find_bond_between(mol, mi, mj) else {
2997 continue;
2998 };
2999 q_order.push(i32::try_from(q_bond.id().index()).map_err(|_| {
3000 SubstructMatchError::Unsupported {
3001 branch: "MolMatchFinalCheckFunctor/qOrder/bond-index-overflow",
3002 rdkit_function: "Atom::getPerturbationOrder",
3003 }
3004 })?);
3005 m_order.push(i32::try_from(m_bond.id().index()).map_err(|_| {
3006 SubstructMatchError::Unsupported {
3007 branch: "MolMatchFinalCheckFunctor/mOrder/bond-index-overflow",
3008 rdkit_function: "countSwapsToInterconvert",
3009 }
3010 })?);
3011 if m_order.len() == query.topology_block().adjacency.neighbors_of(qi).len() {
3012 break;
3013 }
3014 }
3015 if q_order.len() != query.topology_block().adjacency.neighbors_of(qi).len()
3016 || q_order.len() != m_order.len()
3017 {
3018 return Err(SubstructMatchError::Unsupported {
3019 branch: "MolMatchFinalCheckFunctor/chiral-atom-missing-matched-neighbors",
3020 rdkit_function: "MolMatchFinalCheckFunctor::operator()",
3021 });
3022 }
3023 // RDKit✔️✔️: int qPermCount = qAt->getPerturbationOrder(qOrder);
3024 let q_perm_count = rdkit_atom_perturbation_order_from_bond_indices(query, qi, &q_order)?;
3025
3026 // RDKit✔️✔️: unsigned unmatchedNeighbors = mAt->getDegree() - mOrder.size();
3027 // RDKit✔️✔️: mOrder.insert(mOrder.end(), unmatchedNeighbors, -1);
3028 let unmatched_neighbors = mol
3029 .topology_block()
3030 .adjacency
3031 .neighbors_of(mi)
3032 .len()
3033 .saturating_sub(m_order.len());
3034 m_order.extend(std::iter::repeat_n(-1, unmatched_neighbors));
3035
3036 // RDKit✔️✔️: INT_LIST moOrder;
3037 // RDKit✔️✔️: for (const auto &bond : d_mol.atomBonds(mAt)) {
3038 // RDKit✔️✔️: const int dbidx = bond->getIdx();
3039 // RDKit✔️✔️: if (std::find(mOrder.begin(), mOrder.end(), dbidx) != mOrder.end()) {
3040 // RDKit✔️✔️: moOrder.push_back(dbidx);
3041 // RDKit✔️✔️: } else {
3042 // RDKit✔️✔️: moOrder.push_back(-1);
3043 // RDKit✔️✔️: }
3044 // RDKit✔️✔️: }
3045 let mo_order: Vec<i32> = mol
3046 .topology_block()
3047 .adjacency
3048 .neighbors_of(mi)
3049 .iter()
3050 .map(|neighbor| {
3051 i32::try_from(neighbor.bond.index()).map(|bond_idx| {
3052 if m_order.contains(&bond_idx) {
3053 bond_idx
3054 } else {
3055 -1
3056 }
3057 })
3058 })
3059 .collect::<Result<_, _>>()
3060 .map_err(|_| SubstructMatchError::Unsupported {
3061 branch: "MolMatchFinalCheckFunctor/moOrder/bond-index-overflow",
3062 rdkit_function: "countSwapsToInterconvert",
3063 })?;
3064 // RDKit✔️✔️: const int mPermCount =
3065 // RDKit✔️✔️: static_cast<int>(countSwapsToInterconvert(moOrder, mOrder));
3066 let m_perm_count = count_swaps_to_interconvert_i32(&mo_order, &m_order).ok_or(
3067 SubstructMatchError::Unsupported {
3068 branch: "MolMatchFinalCheckFunctor/mPermCount/unmodeled-bond-ordering",
3069 rdkit_function: "countSwapsToInterconvert",
3070 },
3071 )?;
3072
3073 // RDKit✔️✔️: const bool requireMatch = qPermCount % 2 == mPermCount % 2;
3074 // RDKit✔️✔️: const bool labelsMatch = qAt->getChiralTag() == mAt->getChiralTag();
3075 // RDKit✔️✔️: const bool matchOK = requireMatch == labelsMatch;
3076 // RDKit✔️✔️: // if this is not part of a stereogroup and doesn't match, return false
3077 // RDKit✔️✔️: const auto msg = d_molStereoGroups.find(m_c[i]);
3078 // RDKit✔️✔️: if (msg == d_molStereoGroups.end()) {
3079 // RDKit✔️✔️: if (!matchOK) {
3080 // RDKit✔️✔️: return false;
3081 // RDKit✔️✔️: }
3082 // RDKit✔️✔️: } else {
3083 // RDKit✔️✔️: matches[m_c[i]] = matchOK;
3084 // RDKit✔️✔️: }
3085 let require_match = q_perm_count % 2 == m_perm_count % 2;
3086 let labels_match = q_at.chiral_tag() == m_at.chiral_tag();
3087 let match_ok = require_match == labels_match;
3088 if mol_stereo_groups[mi].is_some() {
3089 stereo_matches[mi] = Some(match_ok);
3090 } else if !match_ok {
3091 return Ok(false);
3092 }
3093 }
3094
3095 // RDKit✔️✔️: std::unordered_map<unsigned int, unsigned int> q_to_mol;
3096 // RDKit✔️✔️: for (unsigned int j = 0; j < d_query.getNumAtoms(); ++j) {
3097 // RDKit✔️✔️: q_to_mol[q_c[j]] = m_c[j];
3098 // RDKit✔️✔️: }
3099 // RDKit✔️✔️:
3100 // RDKit✔️✔️: if (d_params.useEnhancedStereo) {
3101 // RDKit✔️✔️: if (!detail::enhancedStereoIsOK(d_mol, d_query, q_to_mol, d_molStereoGroups,
3102 // RDKit✔️✔️: matches)) {
3103 // RDKit✔️✔️: return false;
3104 // RDKit✔️✔️: }
3105 // RDKit✔️✔️: }
3106 if params.use_enhanced_stereo
3107 && !enhanced_stereo_is_ok(mol, query, &q_to_mol, mol_stereo_groups, &stereo_matches)
3108 {
3109 return Ok(false);
3110 }
3111
3112 // RDKit✔️✔️: // now check double bonds
3113 // RDKit✔️✔️: for (const auto &qBnd : d_query.bonds()) {
3114 // RDKit✔️✔️: if (qBnd->getBondType() != Bond::DOUBLE ||
3115 // RDKit✔️✔️: qBnd->getStereo() <= Bond::STEREOANY) {
3116 // RDKit✔️✔️: continue;
3117 // RDKit✔️✔️: }
3118 for q_bnd in query.bonds() {
3119 if q_bnd.order() != BondOrder::Double || !rdkit_bond_stereo_is_above_any(q_bnd.stereo()) {
3120 continue;
3121 }
3122 // RDKit✔️✔️: if (qBnd->getStereoAtoms().size() != 2) {
3123 // RDKit✔️✔️: continue;
3124 // RDKit✔️✔️: }
3125 let Some(q_stereo_atoms) = q_bnd.stereo_atoms() else {
3126 continue;
3127 };
3128 // RDKit✔️✔️: const Bond *mBnd = d_mol.getBondBetweenAtoms(
3129 // RDKit✔️✔️: q_to_mol[qBnd->getBeginAtomIdx()], q_to_mol[qBnd->getEndAtomIdx()]);
3130 let q_begin_mol = q_to_mol[q_bnd.begin().index()];
3131 let q_end_mol = q_to_mol[q_bnd.end().index()];
3132 let Some(m_bnd) = find_bond_between(mol, q_begin_mol, q_end_mol) else {
3133 return Err(SubstructMatchError::Unsupported {
3134 branch: "MolMatchFinalCheckFunctor/double-bond-matching-bond-missing",
3135 rdkit_function: "MolMatchFinalCheckFunctor::operator()",
3136 });
3137 };
3138 // RDKit✔️✔️: if (mBnd->getBondType() != Bond::DOUBLE) {
3139 // RDKit✔️✔️: continue;
3140 // RDKit✔️✔️: }
3141 if m_bnd.order() != BondOrder::Double {
3142 continue;
3143 }
3144 // RDKit✔️✔️: if (!d_params.specifiedStereoQueryMatchesUnspecified &&
3145 // RDKit✔️✔️: mBnd->getStereo() <= Bond::STEREOANY) {
3146 // RDKit✔️✔️: return false;
3147 // RDKit✔️✔️: }
3148 if !params.specified_stereo_query_matches_unspecified
3149 && !rdkit_bond_stereo_is_above_any(m_bnd.stereo())
3150 {
3151 return Ok(false);
3152 }
3153 // RDKit✔️✔️: if (mBnd->getStereoAtoms().size() != 2) {
3154 // RDKit✔️✔️: continue;
3155 // RDKit✔️✔️: }
3156 let Some(m_stereo_atoms) = m_bnd.stereo_atoms() else {
3157 continue;
3158 };
3159
3160 // RDKit✔️✔️: unsigned int end1Matches = 0;
3161 // RDKit✔️✔️: unsigned int end2Matches = 0;
3162 // RDKit✔️✔️: if (q_to_mol[qBnd->getBeginAtomIdx()] == mBnd->getBeginAtomIdx()) {
3163 // RDKit✔️✔️: if (q_to_mol[qBnd->getStereoAtoms()[0]] ==
3164 // RDKit✔️✔️: static_cast<unsigned>(mBnd->getStereoAtoms()[0])) {
3165 // RDKit✔️✔️: end1Matches = 1;
3166 // RDKit✔️✔️: }
3167 // RDKit✔️✔️: if (q_to_mol[qBnd->getStereoAtoms()[1]] ==
3168 // RDKit✔️✔️: static_cast<unsigned>(mBnd->getStereoAtoms()[1])) {
3169 // RDKit✔️✔️: end2Matches = 1;
3170 // RDKit✔️✔️: }
3171 // RDKit✔️✔️: } else {
3172 // RDKit✔️✔️: if (q_to_mol[qBnd->getStereoAtoms()[0]] ==
3173 // RDKit✔️✔️: static_cast<unsigned>(mBnd->getStereoAtoms()[1])) {
3174 // RDKit✔️✔️: end1Matches = 1;
3175 // RDKit✔️✔️: }
3176 // RDKit✔️✔️: if (q_to_mol[qBnd->getStereoAtoms()[1]] ==
3177 // RDKit✔️✔️: static_cast<unsigned>(mBnd->getStereoAtoms()[0])) {
3178 // RDKit✔️✔️: end2Matches = 1;
3179 // RDKit✔️✔️: }
3180 // RDKit✔️✔️: }
3181 let mut end1_matches = 0_u32;
3182 let mut end2_matches = 0_u32;
3183 if q_begin_mol == m_bnd.begin().index() {
3184 if q_to_mol[q_stereo_atoms[0].index()] == m_stereo_atoms[0].index() {
3185 end1_matches = 1;
3186 }
3187 if q_to_mol[q_stereo_atoms[1].index()] == m_stereo_atoms[1].index() {
3188 end2_matches = 1;
3189 }
3190 } else {
3191 if q_to_mol[q_stereo_atoms[0].index()] == m_stereo_atoms[1].index() {
3192 end1_matches = 1;
3193 }
3194 if q_to_mol[q_stereo_atoms[1].index()] == m_stereo_atoms[0].index() {
3195 end2_matches = 1;
3196 }
3197 }
3198
3199 // RDKit✔️✔️: const unsigned totalMatches = end1Matches + end2Matches;
3200 // RDKit✔️✔️: const auto mStereo =
3201 // RDKit✔️✔️: Chirality::translateEZLabelToCisTrans(mBnd->getStereo());
3202 // RDKit✔️✔️: const auto qStereo =
3203 // RDKit✔️✔️: Chirality::translateEZLabelToCisTrans(qBnd->getStereo());
3204 // RDKit✔️✔️: if (mStereo == qStereo && totalMatches == 1) {
3205 // RDKit✔️✔️: return false;
3206 // RDKit✔️✔️: }
3207 // RDKit✔️✔️: if (mStereo != qStereo && totalMatches != 1) {
3208 // RDKit✔️✔️: return false;
3209 // RDKit✔️✔️: }
3210 let total_matches = end1_matches + end2_matches;
3211 let m_stereo = rdkit_translate_ez_label_to_cis_trans(m_bnd.stereo());
3212 let q_stereo = rdkit_translate_ez_label_to_cis_trans(q_bnd.stereo());
3213 if m_stereo == q_stereo && total_matches == 1 {
3214 return Ok(false);
3215 }
3216 if m_stereo != q_stereo && total_matches != 1 {
3217 return Ok(false);
3218 }
3219 }
3220
3221 // RDKit✔️✔️: if (d_params.uniquify) {
3222 // RDKit✔️✔️: matchesSeen.insert(match);
3223 // RDKit✔️✔️: }
3224 // RDKit✔️✔️: return true;
3225 if let Some(mask) = match_key {
3226 matches_seen.push(mask);
3227 }
3228 Ok(true)
3229}
3230
3231// ---------------------------------------------------------------------------
3232// Bond mapping builder
3233// ---------------------------------------------------------------------------
3234
3235/// Build the bond mapping for a match result.
3236///
3237/// For each query bond (by index), find the corresponding molecular bond
3238/// that connects the matched query endpoints.
3239#[allow(dead_code)]
3240fn build_bond_mapping(
3241 query_atom_to_mol: &[Option<usize>],
3242 query: &Vf2Graph,
3243 mol: &Vf2Graph,
3244) -> Vec<usize> {
3245 let mut bond_mapping = Vec::with_capacity(query.n_bonds);
3246 for bond_idx in 0..query.n_bonds {
3247 // Find the query atoms connected by this bond.
3248 let mut q_begin = NULL_NODE;
3249 let mut q_end = NULL_NODE;
3250 for qa in 0..query.n_atoms {
3251 for &(nbr, eidx) in &query.adjacency[qa] {
3252 if eidx == bond_idx {
3253 q_begin = qa;
3254 q_end = nbr;
3255 break;
3256 }
3257 }
3258 if q_begin != NULL_NODE {
3259 break;
3260 }
3261 }
3262
3263 if q_begin != NULL_NODE {
3264 let m_begin = query_atom_to_mol[q_begin];
3265 let m_end = query_atom_to_mol[q_end];
3266 if let (Some(mb), Some(me)) = (m_begin, m_end) {
3267 // Find bond between mb and me in mol.
3268 let mut mol_bond_idx = NULL_NODE;
3269 for &(nbr, eidx) in &mol.adjacency[mb] {
3270 if nbr == me {
3271 mol_bond_idx = eidx;
3272 break;
3273 }
3274 }
3275 bond_mapping.push(mol_bond_idx);
3276 } else {
3277 bond_mapping.push(NULL_NODE);
3278 }
3279 } else {
3280 bond_mapping.push(NULL_NODE);
3281 }
3282 }
3283 bond_mapping
3284}
3285
3286// ---------------------------------------------------------------------------
3287// Public API
3288// ---------------------------------------------------------------------------
3289
3290fn preflight_atom_query(
3291 query: &crate::QueryNode<AtomQueryPredicate>,
3292) -> Result<(), SubstructMatchError> {
3293 match query {
3294 crate::QueryNode::Predicate(AtomQueryPredicate::UnsupportedFeature(branch)) => {
3295 Err(SubstructMatchError::Unsupported {
3296 branch,
3297 rdkit_function: "QueryAtom::Match",
3298 })
3299 }
3300 crate::QueryNode::Predicate(AtomQueryPredicate::RecursiveSmarts(query)) => {
3301 let inner_query = query.query_mol().ok_or(SubstructMatchError::Unsupported {
3302 branch: "recursive SMARTS without a compiled query molecule",
3303 rdkit_function: "RecursiveStructureQuery::getQueryMol",
3304 })?;
3305 preflight_query_molecule(inner_query)
3306 }
3307 crate::QueryNode::Predicate(_) => Ok(()),
3308 crate::QueryNode::And(children)
3309 | crate::QueryNode::Or(children)
3310 | crate::QueryNode::Xor(children) => {
3311 for child in children {
3312 preflight_atom_query(child)?;
3313 }
3314 Ok(())
3315 }
3316 crate::QueryNode::Not(child) => preflight_atom_query(child),
3317 }
3318}
3319
3320fn preflight_bond_query(
3321 query: &crate::QueryNode<BondQueryPredicate>,
3322) -> Result<(), SubstructMatchError> {
3323 match query {
3324 crate::QueryNode::Predicate(BondQueryPredicate::UnsupportedFeature(branch)) => {
3325 Err(SubstructMatchError::Unsupported {
3326 branch,
3327 rdkit_function: "QueryBond::Match",
3328 })
3329 }
3330 crate::QueryNode::Predicate(_) => Ok(()),
3331 crate::QueryNode::And(children)
3332 | crate::QueryNode::Or(children)
3333 | crate::QueryNode::Xor(children) => {
3334 for child in children {
3335 preflight_bond_query(child)?;
3336 }
3337 Ok(())
3338 }
3339 crate::QueryNode::Not(child) => preflight_bond_query(child),
3340 }
3341}
3342
3343fn preflight_query_molecule(query: &Molecule) -> Result<(), SubstructMatchError> {
3344 // This fail-closed preflight has no RDKit counterpart: RDKit query leaves
3345 // are executable, while COSMolKit can preserve explicitly unsupported
3346 // leaves imported from other formats. Inspecting every leaf before VF2
3347 // prevents AND/OR short-circuiting from turning unsupported chemistry into
3348 // a plausible match or mismatch.
3349 //
3350 // Local complexity review: this is O(A + B + Q), where Q includes all
3351 // owned recursive query trees. It allocates no collections and performs no
3352 // molecule or query clones. Each supported leaf is visited once before the
3353 // existing matcher traversal; failure returns at the first unsupported
3354 // leaf.
3355 for atom in query.atoms() {
3356 if let Some(query) = atom.query() {
3357 preflight_atom_query(query)?;
3358 }
3359 }
3360 for bond in query.bonds() {
3361 if let Some(query) = bond.query() {
3362 preflight_bond_query(query)?;
3363 }
3364 }
3365 Ok(())
3366}
3367
3368fn recursive_matcher(
3369 mol: &Molecule,
3370 query: &Molecule,
3371 params: &SubstructMatchParams,
3372 recursive_cache: &mut RecursiveQueryMatchCache,
3373) -> Result<Vec<bool>, SubstructMatchError> {
3374 // RDKit✔️❌: unsigned int RecursiveMatcher(const ROMol &mol, const ROMol &query,
3375 // RDKit✔️❌: std::vector<int> &matches,
3376 // RDKit✔️❌: SUBQUERY_MAP &subqueryMap,
3377 // RDKit✔️❌: const SubstructMatchParameters ¶ms,
3378 // RDKit✔️❌: std::vector<RecursiveStructureQuery *> &locked) {
3379 // RDKit✔️❌: SubstructMatchParameters lparams = params;
3380 // RDKit✔️❌: lparams.maxMatches = std::max(params.maxRecursiveMatches, params.maxMatches);
3381 // RDKit✔️❌: lparams.uniquify = false;
3382 // RDKit✔️❌: for (auto qAtom : query.atoms()) {
3383 // RDKit✔️❌: if (qAtom->hasQuery()) {
3384 // RDKit✔️❌: MatchSubqueries(mol, qAtom->getQuery(), lparams, subqueryMap, locked);
3385 // RDKit✔️❌: }
3386 // RDKit✔️❌: }
3387 // RDKit✔️❌:
3388 // RDKit✔️❌: detail::AtomLabelFunctor atomLabeler(query, mol, lparams);
3389 // RDKit✔️❌: detail::BondLabelFunctor bondLabeler(query, mol, lparams);
3390 // RDKit✔️❌: MolMatchFinalCheckFunctor matchChecker(query, mol, lparams);
3391 // RDKit✔️❌:
3392 // RDKit✔️❌: matches.clear();
3393 // RDKit✔️❌: matches.resize(0);
3394 // RDKit✔️❌: std::vector<detail::ssPairType> pms;
3395 // RDKit✔️❌: bool found =
3396 // RDKit✔️❌: boost::vf2_all(query.getTopology(), mol.getTopology(), atomLabeler,
3397 // RDKit✔️❌: bondLabeler, matchChecker, pms, lparams.maxMatches);
3398 // RDKit✔️❌: unsigned int res = 0;
3399 // RDKit✔️❌: if (found) {
3400 // RDKit✔️❌: matches.reserve(pms.size());
3401 // RDKit✔️❌: for (const auto &pairs : pms) {
3402 // RDKit✔️❌: if (!query.hasProp(common_properties::_queryRootAtom)) {
3403 // RDKit✔️❌: matches.push_back(pairs.begin()->second);
3404 // RDKit✔️❌: } else {
3405 // RDKit✔️❌: int rootIdx;
3406 // RDKit✔️❌: query.getProp(common_properties::_queryRootAtom, rootIdx);
3407 // RDKit✔️❌: bool found = false;
3408 // RDKit✔️❌: for (const auto &pairIter : pairs) {
3409 // RDKit✔️❌: if (pairIter.first == static_cast<unsigned int>(rootIdx)) {
3410 // RDKit✔️❌: matches.push_back(pairIter.second);
3411 // RDKit✔️❌: found = true;
3412 // RDKit✔️❌: break;
3413 // RDKit✔️❌: }
3414 // RDKit✔️❌: }
3415 // RDKit✔️❌: if (!found) {
3416 // RDKit✔️❌: BOOST_LOG(rdErrorLog)
3417 // RDKit✔️❌: << "no match found for queryRootAtom" << std::endl;
3418 // RDKit✔️❌: }
3419 // RDKit✔️❌: }
3420 // RDKit✔️❌: if (matches.size() == lparams.maxMatches) {
3421 // RDKit✔️❌: break;
3422 // RDKit✔️❌: }
3423 // RDKit✔️❌: }
3424 // RDKit✔️❌: res = matches.size();
3425 // RDKit✔️❌: }
3426 // RDKit✔️❌: return res;
3427 // RDKit✔️❌: }
3428 // Complexity review: nested preparation and VF2 follow the source. The
3429 // membership result is one O(target atoms) bool Vec in place of RDKit's
3430 // ordered set, while the canonical VF2 mapping-allocation gap remains.
3431 let mut local_params = params.clone();
3432 local_params.max_matches = params.max_recursive_matches.max(params.max_matches);
3433 local_params.uniquify = false;
3434 for atom in query.atoms() {
3435 if let Some(query_node) = atom.query() {
3436 match_subqueries(mol, query_node, &local_params, recursive_cache)?;
3437 }
3438 }
3439
3440 let matches = substruct_match_impl_with_recursive_cache(
3441 mol,
3442 query,
3443 &local_params,
3444 Some(recursive_cache),
3445 )?;
3446 let root_index = query
3447 .prop("_queryRootAtom")
3448 .and_then(|value| value.parse::<usize>().ok())
3449 .unwrap_or(0);
3450 let mut match_starts = vec![false; mol.num_atoms()];
3451 for matched in matches.into_iter().take(local_params.max_matches) {
3452 if let Some(&root_atom_idx) = matched.atom_mapping.get(root_index)
3453 && root_atom_idx != NULL_NODE
3454 && root_atom_idx < match_starts.len()
3455 {
3456 match_starts[root_atom_idx] = true;
3457 }
3458 }
3459 Ok(match_starts)
3460}
3461
3462fn match_subqueries(
3463 mol: &Molecule,
3464 query: &crate::QueryNode<AtomQueryPredicate>,
3465 params: &SubstructMatchParams,
3466 recursive_cache: &mut RecursiveQueryMatchCache,
3467) -> Result<(), SubstructMatchError> {
3468 // RDKit✔️❌: void MatchSubqueries(const ROMol &mol, QueryAtom::QUERYATOM_QUERY *query,
3469 // RDKit✔️❌: const SubstructMatchParameters ¶ms,
3470 // RDKit✔️❌: SUBQUERY_MAP &subqueryMap,
3471 // RDKit✔️❌: std::vector<RecursiveStructureQuery *> &locked) {
3472 // RDKit✔️❌: PRECONDITION(query, "bad query");
3473 // RDKit✔️❌: if (query->getDescription() == "RecursiveStructure") {
3474 // RDKit✔️❌: auto *rsq = (RecursiveStructureQuery *)query;
3475 // RDKit✔️❌: #ifdef RDK_BUILD_THREADSAFE_SSS
3476 // RDKit✔️❌: rsq->d_mutex.lock();
3477 // RDKit✔️❌: #endif
3478 // RDKit✔️❌: locked.push_back(rsq);
3479 // RDKit✔️❌: rsq->clear();
3480 // RDKit✔️❌: bool matchDone = false;
3481 // RDKit✔️❌: if (rsq->getSerialNumber() &&
3482 // RDKit✔️❌: subqueryMap.find(rsq->getSerialNumber()) != subqueryMap.end()) {
3483 // RDKit✔️❌: matchDone = true;
3484 // RDKit✔️❌: auto orsq =
3485 // RDKit✔️❌: (const RecursiveStructureQuery *)subqueryMap[rsq->getSerialNumber()];
3486 // RDKit✔️❌: for (auto setIter = orsq->beginSet(); setIter != orsq->endSet();
3487 // RDKit✔️❌: ++setIter) {
3488 // RDKit✔️❌: rsq->insert(*setIter);
3489 // RDKit✔️❌: }
3490 // RDKit✔️❌: }
3491 // RDKit✔️❌:
3492 // RDKit✔️❌: if (!matchDone) {
3493 // RDKit✔️❌: ROMol const *queryMol = rsq->getQueryMol();
3494 // RDKit✔️❌: if (queryMol) {
3495 // RDKit✔️❌: std::vector<int> matchStarts;
3496 // RDKit✔️❌: unsigned int res = RecursiveMatcher(mol, *queryMol, matchStarts,
3497 // RDKit✔️❌: subqueryMap, params, locked);
3498 // RDKit✔️❌: if (res) {
3499 // RDKit✔️❌: for (int &matchStart : matchStarts) {
3500 // RDKit✔️❌: rsq->insert(matchStart);
3501 // RDKit✔️❌: }
3502 // RDKit✔️❌: }
3503 // RDKit✔️❌: }
3504 // RDKit✔️❌: if (rsq->getSerialNumber()) {
3505 // RDKit✔️❌: subqueryMap[rsq->getSerialNumber()] = query;
3506 // RDKit✔️❌: }
3507 // RDKit✔️❌: }
3508 // RDKit✔️❌: }
3509 // RDKit✔️❌:
3510 // RDKit✔️❌: for (auto childIt = query->beginChildren(); childIt != query->endChildren();
3511 // RDKit✔️❌: ++childIt) {
3512 // RDKit✔️❌: MatchSubqueries(mol, childIt->get(), params, subqueryMap, locked);
3513 // RDKit✔️❌: }
3514 // RDKit✔️❌: }
3515 // Complexity review: every query node is visited once unless a serial-key
3516 // cache hit skips recursive VF2, matching RDKit. BTreeMap lookup is O(log
3517 // R) instead of unordered-map average O(1), but recursive VF2 dominates;
3518 // query trees and match vectors are never cloned here.
3519 match query {
3520 crate::QueryNode::Predicate(AtomQueryPredicate::RecursiveSmarts(recursive_query)) => {
3521 let cache_key = recursive_query_cache_key(recursive_query);
3522 if !recursive_cache.contains_key(&cache_key) {
3523 let match_starts = match recursive_query.query_mol() {
3524 Some(inner_query) => {
3525 recursive_matcher(mol, inner_query, params, recursive_cache)?
3526 }
3527 None => vec![false; mol.num_atoms()],
3528 };
3529 recursive_cache.insert(cache_key, match_starts);
3530 }
3531 }
3532 crate::QueryNode::Predicate(_) => {}
3533 crate::QueryNode::And(children)
3534 | crate::QueryNode::Or(children)
3535 | crate::QueryNode::Xor(children) => {
3536 for child in children {
3537 match_subqueries(mol, child, params, recursive_cache)?;
3538 }
3539 }
3540 crate::QueryNode::Not(child) => {
3541 match_subqueries(mol, child, params, recursive_cache)?;
3542 }
3543 }
3544 Ok(())
3545}
3546
3547fn populate_recursive_query_match_cache(
3548 mol: &Molecule,
3549 query: &Molecule,
3550 params: &SubstructMatchParams,
3551 recursive_cache: &mut RecursiveQueryMatchCache,
3552) -> Result<(), SubstructMatchError> {
3553 for atom in query.atoms() {
3554 if let Some(query_node) = atom.query() {
3555 match_subqueries(mol, query_node, params, recursive_cache)?;
3556 }
3557 }
3558 Ok(())
3559}
3560
3561fn substruct_match_impl_with_recursive_cache(
3562 mol: &Molecule,
3563 query: &Molecule,
3564 params: &SubstructMatchParams,
3565 recursive_cache: Option<&RecursiveQueryMatchCache>,
3566) -> SubstructMatchResultList {
3567 let query_ctx = build_query_match_context(mol);
3568 substruct_match_impl_with_recursive_cache_and_context(
3569 mol,
3570 query,
3571 params,
3572 recursive_cache,
3573 &query_ctx,
3574 )
3575}
3576
3577fn substruct_match_impl_with_recursive_cache_and_context(
3578 mol: &Molecule,
3579 query: &Molecule,
3580 params: &SubstructMatchParams,
3581 recursive_cache: Option<&RecursiveQueryMatchCache>,
3582 query_ctx: &QueryMatchContext,
3583) -> SubstructMatchResultList {
3584 let m_num_atoms = mol.num_atoms();
3585 let q_num_atoms = query.num_atoms();
3586
3587 // RDKit source (SubstructMatch.cpp):
3588 // if (!mNumAtoms || !qNumAtoms || qNumAtoms > mNumAtoms) {
3589 // return matches;
3590 // }
3591 if m_num_atoms == 0 || q_num_atoms == 0 || q_num_atoms > m_num_atoms {
3592 return Ok(Vec::new());
3593 }
3594
3595 // Build VF2 graphs.
3596 let q_graph = build_vf2_graph(query);
3597 let m_graph = build_vf2_graph(mol);
3598
3599 // Build atom matching closure.
3600 // RDKit source:
3601 // detail::AtomLabelFunctor atomLabeler(query, mol, params);
3602 // detail::BondLabelFunctor bondLabeler(query, mol, params);
3603 // MolMatchFinalCheckFunctor matchChecker(query, mol, params);
3604 let atom_fn = |qi: usize, mj: usize| -> bool {
3605 atom_label_matches(query, mol, qi, mj, params, recursive_cache, query_ctx)
3606 };
3607
3608 let bond_fn = |qei: usize, mei: usize| -> bool {
3609 bond_label_matches(query, mol, qei, mei, params, query_ctx)
3610 };
3611
3612 // RDKit source:
3613 // bool found = boost::vf2_all(query.getTopology(), mol.getTopology(),
3614 // atomLabeler, bondLabeler, matchChecker,
3615 // pms, params.maxMatches);
3616 let mut raw_matches: Vec<(Vec<NodeId>, Vec<NodeId>)> = Vec::new();
3617 let mut matches_seen: Vec<Vec<bool>> = Vec::new();
3618 let final_check_setup = MolMatchFinalCheckSetup::new(query, mol, params);
3619 let mut final_check_error: Option<SubstructMatchError> = None;
3620 let mut check_fn = |c1: &[NodeId], c2: &[NodeId]| -> bool {
3621 match rdkit_match_final_check(
3622 mol,
3623 query,
3624 params,
3625 c1,
3626 c2,
3627 &final_check_setup,
3628 &mut matches_seen,
3629 ) {
3630 Ok(accepted) => accepted,
3631 Err(err) => {
3632 final_check_error = Some(err);
3633 false
3634 }
3635 }
3636 };
3637
3638 vf2_entry_all(
3639 &q_graph,
3640 &m_graph,
3641 &atom_fn,
3642 &bond_fn,
3643 Some(&mut check_fn),
3644 &mut raw_matches,
3645 params.max_matches,
3646 );
3647 if let Some(err) = final_check_error {
3648 return Err(err);
3649 }
3650
3651 // RDKit source (SubstructMatch.cpp):
3652 // if (found) {
3653 // const unsigned int nQueryAtoms = query.getNumAtoms();
3654 // matches.reserve(pms.size());
3655 // MatchVectType matchVect(nQueryAtoms);
3656 // for (const auto &pairs : pms) {
3657 // for (const auto &pair : pairs) {
3658 // matchVect[pair.first] = pair;
3659 // }
3660 // matches.push_back(matchVect);
3661 // }
3662 // }
3663 let mut results: Vec<SubstructMatchResult> = Vec::new();
3664
3665 for (c1, c2) in &raw_matches {
3666 // Build atom_mapping: query_atom_index -> mol_atom_index.
3667 // RDKit uses MatchVectType (vector<pair<int,int>>) where
3668 // pair.second is the mol atom index and pair.first is query atom index.
3669 let mut atom_to_mol: Vec<Option<usize>> = vec![None; q_num_atoms];
3670 for (&qa, &ma) in c1.iter().zip(c2.iter()) {
3671 if qa < q_num_atoms {
3672 atom_to_mol[qa] = Some(ma);
3673 }
3674 }
3675
3676 // Build bond mapping by looking up bonds between matched atoms.
3677 let mut bond_mapping = Vec::with_capacity(query.num_bonds());
3678 for qbond in query.bonds() {
3679 let q_begin = qbond.begin().index();
3680 let q_end = qbond.end().index();
3681 let m_begin = atom_to_mol[q_begin];
3682 let m_end = atom_to_mol[q_end];
3683 match (m_begin, m_end) {
3684 (Some(mb), Some(me)) => {
3685 // Find bond between mb and me in mol.
3686 let found = m_graph.adjacency[mb]
3687 .iter()
3688 .find(|&&(nbr, _)| nbr == me)
3689 .map(|&(_, eidx)| eidx);
3690 bond_mapping.push(found.unwrap_or(NULL_NODE));
3691 }
3692 _ => {
3693 bond_mapping.push(NULL_NODE);
3694 }
3695 }
3696 }
3697
3698 results.push(SubstructMatchResult {
3699 atom_mapping: atom_to_mol
3700 .into_iter()
3701 .map(|x| x.unwrap_or(NULL_NODE))
3702 .collect(),
3703 bond_mapping,
3704 });
3705 }
3706
3707 Ok(results)
3708}
3709
3710fn atom_compat(
3711 query_atom: &Atom,
3712 query_mol: &Molecule,
3713 mol_atom: &Atom,
3714 mol: &Molecule,
3715 params: &SubstructMatchParams,
3716 recursive_cache: Option<&RecursiveQueryMatchCache>,
3717 query_ctx: &QueryMatchContext,
3718) -> bool {
3719 // BEGIN RDKIT CPP FUNCTION: third_party/rdkit/Code/GraphMol/Substruct/SubstructUtils.cpp :: atomCompat
3720 // RDKit✔️✔️: bool atomCompat(const Atom *a1, const Atom *a2,
3721 // RDKit✔️✔️: const SubstructMatchParameters &ps) {
3722 // RDKit✔️✔️: PRECONDITION(a1, "bad atom");
3723 // RDKit✔️✔️: PRECONDITION(a2, "bad atom");
3724 // RDKit✔️✔️: // std::cerr << "\t\tatomCompat: "<< a1 << " " << a1->getIdx() << "-" << a2 <<
3725 // RDKit✔️✔️: // " " << a2->getIdx() << std::endl;
3726 // RDKit✔️✔️:
3727 // RDKit✔️✔️: if (ps.extraAtomCheckOverridesDefaultCheck && ps.extraAtomCheck) {
3728 // RDKit✔️✔️: return ps.extraAtomCheck(*a1, *a2);
3729 // RDKit✔️✔️: }
3730 // RDKit✔️✔️: bool res;
3731 // RDKit✔️✔️: if (ps.useQueryQueryMatches && a1->hasQuery() && a2->hasQuery()) {
3732 // RDKit✔️✔️: res = static_cast<const QueryAtom *>(a1)->QueryMatch(
3733 // RDKit✔️✔️: static_cast<const QueryAtom *>(a2));
3734 // RDKit✔️✔️: } else {
3735 // RDKit✔️✔️: res = a1->Match(a2);
3736 // RDKit✔️✔️: }
3737 // RDKit✔️✔️: if (!res) {
3738 // RDKit✔️✔️: return false;
3739 // RDKit✔️✔️: }
3740 // RDKit✔️✔️: if (!ps.atomProperties.empty()) {
3741 // RDKit✔️✔️: if (!propertyCompat(a1, a2, ps.atomProperties)) {
3742 // RDKit✔️✔️: return false;
3743 // RDKit✔️✔️: }
3744 // RDKit✔️✔️: }
3745 // RDKit✔️✔️: if (ps.extraAtomCheck && !ps.extraAtomCheck(*a1, *a2)) {
3746 // RDKit✔️✔️: return false;
3747 // RDKit✔️✔️: }
3748 // RDKit✔️✔️:
3749 // RDKit✔️✔️: return res;
3750 // RDKit✔️✔️: }
3751 // END RDKIT CPP FUNCTION
3752 //
3753 // Typed references make the source pointer preconditions
3754 // unrepresentable. Local complexity review: both implementations perform
3755 // the same constant-time option/flag dispatch, one default query or atom
3756 // match, the requested property scan, and at most one callback invocation
3757 // after the default match. Arc callback dispatch is the Rust equivalent of
3758 // std::function dispatch and allocates nothing per match. Query-tree
3759 // traversal and recursive-cache lookup retain their existing complexity;
3760 // property_compat has the separately documented BTreeMap improvement. No
3761 // atom, molecule, query, property map, or callback is cloned in this hot
3762 // path.
3763 if params.extra_atom_check_overrides_default_check
3764 && let Some(extra_atom_check) = ¶ms.extra_atom_check
3765 {
3766 return extra_atom_check(query_mol, query_atom, mol, mol_atom);
3767 }
3768
3769 let matches = if params.use_query_query_matches
3770 && let (Some(query), Some(mol_query)) = (query_atom.query(), mol_atom.query())
3771 {
3772 atom_queries_match(query, mol_query)
3773 } else if let Some(query_node) = query_atom.query() {
3774 evaluate_atom_query(
3775 query_node,
3776 mol_atom,
3777 mol,
3778 params,
3779 recursive_cache,
3780 query_ctx,
3781 )
3782 } else {
3783 atom_matches(query_atom, query_mol, mol_atom, mol)
3784 };
3785 if !matches {
3786 return false;
3787 }
3788 if !params.atom_properties.is_empty()
3789 && !property_compat(
3790 query_atom.props(),
3791 mol_atom.props(),
3792 ¶ms.atom_properties,
3793 )
3794 {
3795 return false;
3796 }
3797 if let Some(extra_atom_check) = ¶ms.extra_atom_check
3798 && !extra_atom_check(query_mol, query_atom, mol, mol_atom)
3799 {
3800 return false;
3801 }
3802 matches
3803}
3804
3805#[allow(deprecated)]
3806fn chiral_atom_compat(
3807 query_atom: &Atom,
3808 query_mol: &Molecule,
3809 mol_atom: &Atom,
3810 mol: &Molecule,
3811) -> bool {
3812 // BEGIN RDKIT CPP FUNCTION: third_party/rdkit/Code/GraphMol/Substruct/SubstructUtils.cpp :: chiralAtomCompat
3813 // RDKit✔️✔️: bool chiralAtomCompat(const Atom *&a1, const Atom *&a2) {
3814 // RDKit✔️✔️: /// DEPRECATED
3815 // RDKit✔️✔️: PRECONDITION(a1, "bad atom");
3816 // RDKit✔️✔️: PRECONDITION(a2, "bad atom");
3817 // RDKit✔️✔️: bool res = a1->Match(a2);
3818 // RDKit✔️✔️: if (res) {
3819 // RDKit✔️✔️: std::string s1, s2;
3820 // RDKit✔️✔️: bool hascode1 = a1->getPropIfPresent(common_properties::_CIPCode, s1);
3821 // RDKit✔️✔️: bool hascode2 = a2->getPropIfPresent(common_properties::_CIPCode, s2);
3822 // RDKit✔️✔️: if (hascode1 || hascode2) {
3823 // RDKit✔️✔️: res = hascode1 && hascode2 && s1 == s2;
3824 // RDKit✔️✔️: }
3825 // RDKit✔️✔️: }
3826 // RDKit✔️✔️: std::cerr << "\t\tchiralAtomCompat: " << a1 << " " << a1->getIdx() << "-"
3827 // RDKit✔️✔️: << a2 << " " << a2->getIdx() << std::endl;
3828 // RDKit✔️✔️: std::cerr << "\t\t " << res << std::endl;
3829 // RDKit✔️✔️: return res;
3830 // RDKit✔️✔️: }
3831 // END RDKIT CPP FUNCTION
3832 //
3833 // Rust references make both pointer preconditions unrepresentable. Local
3834 // complexity review: the shared atom matcher has the same source-defined
3835 // atom-query cost, followed by two property lookups and one string
3836 // comparison only after a successful atom match. No molecule, atom,
3837 // property map, or string is cloned. BTreeMap lookup retains the canonical
3838 // atom property representation and has the same logarithmic lookup class
3839 // as RDKit's property dictionary for the modeled state.
3840 let mut matches = atom_matches(query_atom, query_mol, mol_atom, mol);
3841 if matches {
3842 let query_cip = query_atom.prop("_CIPCode");
3843 let mol_cip = mol_atom.prop("_CIPCode");
3844 if query_cip.is_some() || mol_cip.is_some() {
3845 matches = query_cip.is_some() && mol_cip.is_some() && query_cip == mol_cip;
3846 }
3847 }
3848 eprintln!(
3849 "\t\tchiralAtomCompat: {:p} {}-{:p} {}",
3850 query_atom,
3851 query_atom.id().index(),
3852 mol_atom,
3853 mol_atom.id().index()
3854 );
3855 eprintln!("\t\t {}", u8::from(matches));
3856 matches
3857}
3858
3859fn bond_compat(
3860 query_bond: &Bond,
3861 query_mol: &Molecule,
3862 mol_bond: &Bond,
3863 mol: &Molecule,
3864 params: &SubstructMatchParams,
3865 query_ctx: &QueryMatchContext,
3866) -> bool {
3867 // BEGIN RDKIT CPP FUNCTION: third_party/rdkit/Code/GraphMol/Substruct/SubstructUtils.cpp :: bondCompat
3868 // RDKit✔️✔️: bool bondCompat(const Bond *b1, const Bond *b2,
3869 // RDKit✔️✔️: const SubstructMatchParameters &ps) {
3870 // RDKit✔️✔️: PRECONDITION(b1, "bad bond");
3871 // RDKit✔️✔️: PRECONDITION(b2, "bad bond");
3872 // RDKit✔️✔️:
3873 // RDKit✔️✔️: if (ps.extraBondCheckOverridesDefaultCheck && ps.extraBondCheck) {
3874 // RDKit✔️✔️: return ps.extraBondCheck(*b1, *b2);
3875 // RDKit✔️✔️: }
3876 // RDKit✔️✔️:
3877 // RDKit✔️✔️: bool res;
3878 // RDKit✔️✔️:
3879 // RDKit✔️✔️: auto isConjugatedSingleOrDoubleBond([](const Bond *bond) {
3880 // RDKit✔️✔️: return bond->getIsConjugated() && (bond->getBondType() == Bond::SINGLE ||
3881 // RDKit✔️✔️: bond->getBondType() == Bond::DOUBLE);
3882 // RDKit✔️✔️: });
3883 // RDKit✔️✔️: auto isSingleOrDoubleBond([](const Bond *bond) {
3884 // RDKit✔️✔️: return (bond->getBondType() == Bond::SINGLE ||
3885 // RDKit✔️✔️: bond->getBondType() == Bond::DOUBLE);
3886 // RDKit✔️✔️: });
3887 // RDKit✔️✔️:
3888 // RDKit✔️✔️: if (ps.useQueryQueryMatches && b1->hasQuery() && b2->hasQuery()) {
3889 // RDKit✔️✔️: res = static_cast<const QueryBond *>(b1)->QueryMatch(
3890 // RDKit✔️✔️: static_cast<const QueryBond *>(b2));
3891 // RDKit✔️✔️: } else if (ps.aromaticMatchesConjugated && !b1->hasQuery() &&
3892 // RDKit✔️✔️: !b2->hasQuery() &&
3893 // RDKit✔️✔️: ((b1->getBondType() == Bond::AROMATIC &&
3894 // RDKit✔️✔️: b2->getBondType() == Bond::AROMATIC) ||
3895 // RDKit✔️✔️: (b1->getBondType() == Bond::AROMATIC &&
3896 // RDKit✔️✔️: isConjugatedSingleOrDoubleBond(b2)) ||
3897 // RDKit✔️✔️: (b2->getBondType() == Bond::AROMATIC &&
3898 // RDKit✔️✔️: isConjugatedSingleOrDoubleBond(b1)))) {
3899 // RDKit✔️✔️: res = true;
3900 // RDKit✔️✔️: } else if (ps.aromaticMatchesSingleOrDouble && !b1->hasQuery() &&
3901 // RDKit✔️✔️: !b2->hasQuery() &&
3902 // RDKit✔️✔️: ((b1->getBondType() == Bond::AROMATIC &&
3903 // RDKit✔️✔️: b2->getBondType() == Bond::AROMATIC) ||
3904 // RDKit✔️✔️: (b1->getBondType() == Bond::AROMATIC &&
3905 // RDKit✔️✔️: isSingleOrDoubleBond(b2)) ||
3906 // RDKit✔️✔️: (b2->getBondType() == Bond::AROMATIC &&
3907 // RDKit✔️✔️: isSingleOrDoubleBond(b1)))) {
3908 // RDKit✔️✔️: res = true;
3909 // RDKit✔️✔️: } else {
3910 // RDKit✔️✔️: res = b1->Match(b2);
3911 // RDKit✔️✔️: }
3912 // RDKit✔️✔️: if (!res) {
3913 // RDKit✔️✔️: return false;
3914 // RDKit✔️✔️: }
3915 // RDKit✔️✔️: if (b1->getBondType() == Bond::DATIVE && b2->getBondType() == Bond::DATIVE) {
3916 // RDKit✔️✔️: // for dative bonds we need to make sure that the direction also matches:
3917 // RDKit✔️✔️: if (!b1->getBeginAtom()->Match(b2->getBeginAtom()) ||
3918 // RDKit✔️✔️: !b1->getEndAtom()->Match(b2->getEndAtom())) {
3919 // RDKit✔️✔️: return false;
3920 // RDKit✔️✔️: }
3921 // RDKit✔️✔️: }
3922 // RDKit✔️✔️: if (!ps.bondProperties.empty()) {
3923 // RDKit✔️✔️: if (!propertyCompat(b1, b2, ps.bondProperties)) {
3924 // RDKit✔️✔️: return false;
3925 // RDKit✔️✔️: }
3926 // RDKit✔️✔️: }
3927 // RDKit✔️✔️: if (ps.extraBondCheck && !ps.extraBondCheck(*b1, *b2)) {
3928 // RDKit✔️✔️: return false;
3929 // RDKit✔️✔️: }
3930 // RDKit✔️✔️:
3931 // RDKit✔️✔️: return res;
3932 // RDKit✔️✔️: }
3933 // END RDKIT CPP FUNCTION
3934 //
3935 // Rust references make both pointer preconditions unrepresentable. Local
3936 // complexity review: flag/order checks and dative endpoint lookups are
3937 // constant time; query-tree matching reuses the canonical evaluator with
3938 // its source-equivalent tree complexity. The property scan is linear in
3939 // the requested names with logarithmic canonical BTreeMap lookup, and at
3940 // most one Arc callback dispatch occurs. Nothing is cloned or allocated.
3941 if params.extra_bond_check_overrides_default_check
3942 && let Some(extra_bond_check) = ¶ms.extra_bond_check
3943 {
3944 return extra_bond_check(query_bond, mol_bond);
3945 }
3946
3947 let is_conjugated_single_or_double = |bond: &Bond| {
3948 bond.is_conjugated() && matches!(bond.order(), BondOrder::Single | BondOrder::Double)
3949 };
3950 let is_single_or_double =
3951 |bond: &Bond| matches!(bond.order(), BondOrder::Single | BondOrder::Double);
3952 let aromatic_pair_matches = |other_matches: &dyn Fn(&Bond) -> bool| {
3953 (query_bond.order() == BondOrder::Aromatic && mol_bond.order() == BondOrder::Aromatic)
3954 || (query_bond.order() == BondOrder::Aromatic && other_matches(mol_bond))
3955 || (mol_bond.order() == BondOrder::Aromatic && other_matches(query_bond))
3956 };
3957
3958 let matches = if params.use_query_query_matches
3959 && let (Some(query), Some(mol_query)) = (query_bond.query(), mol_bond.query())
3960 {
3961 bond_queries_match(query, mol_query)
3962 } else if params.aromatic_matches_conjugated
3963 && query_bond.query().is_none()
3964 && mol_bond.query().is_none()
3965 && aromatic_pair_matches(&is_conjugated_single_or_double)
3966 {
3967 true
3968 } else if params.aromatic_matches_single_or_double
3969 && query_bond.query().is_none()
3970 && mol_bond.query().is_none()
3971 && aromatic_pair_matches(&is_single_or_double)
3972 {
3973 true
3974 } else if let Some(query) = query_bond.query() {
3975 evaluate_bond_query(query, mol_bond, mol, query_ctx)
3976 } else {
3977 query_bond.order() == BondOrder::Unspecified
3978 || mol_bond.order() == BondOrder::Unspecified
3979 || query_bond.order() == mol_bond.order()
3980 };
3981 if !matches {
3982 return false;
3983 }
3984
3985 if query_bond.order() == BondOrder::Dative && mol_bond.order() == BondOrder::Dative {
3986 let query_begin = &query_mol.atoms()[query_bond.begin().index()];
3987 let query_end = &query_mol.atoms()[query_bond.end().index()];
3988 let mol_begin = &mol.atoms()[mol_bond.begin().index()];
3989 let mol_end = &mol.atoms()[mol_bond.end().index()];
3990 if !atom_matches(query_begin, query_mol, mol_begin, mol)
3991 || !atom_matches(query_end, query_mol, mol_end, mol)
3992 {
3993 return false;
3994 }
3995 }
3996 if !params.bond_properties.is_empty()
3997 && !property_compat(
3998 query_bond.props(),
3999 mol_bond.props(),
4000 ¶ms.bond_properties,
4001 )
4002 {
4003 return false;
4004 }
4005 if let Some(extra_bond_check) = ¶ms.extra_bond_check
4006 && !extra_bond_check(query_bond, mol_bond)
4007 {
4008 return false;
4009 }
4010 matches
4011}
4012
4013fn remove_duplicates(matches: &mut Vec<SubstructMatchResult>, atom_count: usize) {
4014 // BEGIN RDKIT CPP FUNCTION: third_party/rdkit/Code/GraphMol/Substruct/SubstructUtils.cpp :: removeDuplicates
4015 // RDKit✔️✔️: void removeDuplicates(std::vector<MatchVectType> &matches,
4016 // RDKit✔️✔️: unsigned int nAtoms) {
4017 // RDKit✔️✔️: //
4018 // RDKit✔️✔️: // This works by tracking the indices of the atoms in each match vector.
4019 // RDKit✔️✔️: // This can lead to unexpected behavior when looking at rings and queries
4020 // RDKit✔️✔️: // that don't specify bond orders. For example querying this molecule:
4021 // RDKit✔️✔️: // C1CCC=1
4022 // RDKit✔️✔️: // with the pattern constructed from SMARTS C~C~C~C will return a
4023 // RDKit✔️✔️: // single match, despite the fact that there are 4 different paths
4024 // RDKit✔️✔️: // when valence is considered. The defense of this behavior is
4025 // RDKit✔️✔️: // that the 4 paths are equivalent in the semantics of the query.
4026 // RDKit✔️✔️: // Also, OELib returns the same results
4027 // RDKit✔️✔️: //
4028 // RDKit✔️✔️: std::unordered_set<std::string> seen;
4029 // RDKit✔️✔️: std::vector<MatchVectType> res;
4030 // RDKit✔️✔️: res.reserve(matches.size());
4031 // RDKit✔️✔️: seen.reserve(matches.size());
4032 // RDKit✔️✔️: for (const auto &match : matches) {
4033 // RDKit✔️✔️: std::string val(nAtoms, '0');
4034 // RDKit✔️✔️: for (const auto &ci : match) {
4035 // RDKit✔️✔️: val[ci.second] = '1';
4036 // RDKit✔️✔️: }
4037 // RDKit✔️✔️: const bool inserted = seen.insert(std::move(val)).second;
4038 // RDKit✔️✔️: if (inserted) {
4039 // RDKit✔️✔️: res.push_back(match);
4040 // RDKit✔️✔️: }
4041 // RDKit✔️✔️: }
4042 // RDKit✔️✔️: res.shrink_to_fit();
4043 // RDKit✔️✔️: matches = std::move(res);
4044 // RDKit✔️✔️: }
4045 // END RDKIT CPP FUNCTION
4046 //
4047 // Local complexity review: both versions allocate one atom-count-sized
4048 // signature per examined match and use expected O(1) hash insertion, for
4049 // O(matches * atom_count) time and space bounded by unique signatures.
4050 // Vec<bool> packs the same binary information as the source string. Moving
4051 // accepted Rust match values avoids the source copy and preserves order.
4052 let mut seen = HashSet::with_capacity(matches.len());
4053 let mut unique = Vec::with_capacity(matches.len());
4054 for matched in matches.drain(..) {
4055 let mut signature = vec![false; atom_count];
4056 for &atom_index in &matched.atom_mapping {
4057 signature[atom_index] = true;
4058 }
4059 if seen.insert(signature) {
4060 unique.push(matched);
4061 }
4062 }
4063 unique.shrink_to_fit();
4064 *matches = unique;
4065}
4066
4067fn query_contains_atomic_number(
4068 query: &crate::QueryNode<AtomQueryPredicate>,
4069 atomic_number: u8,
4070) -> bool {
4071 match query {
4072 crate::QueryNode::Predicate(AtomQueryPredicate::AtomicNumber(value)) => {
4073 *value == atomic_number
4074 }
4075 crate::QueryNode::And(children)
4076 | crate::QueryNode::Or(children)
4077 | crate::QueryNode::Xor(children) => children
4078 .iter()
4079 .any(|child| query_contains_atomic_number(child, atomic_number)),
4080 crate::QueryNode::Not(child) => query_contains_atomic_number(child, atomic_number),
4081 crate::QueryNode::Predicate(_) => false,
4082 }
4083}
4084
4085pub(crate) fn is_atom_terminal_r_group_or_query_hydrogen(
4086 molecule: &Molecule,
4087 atom_index: usize,
4088) -> bool {
4089 // BEGIN RDKIT CPP FUNCTION: third_party/rdkit/Code/GraphMol/Substruct/SubstructUtils.cpp :: isAtomTerminalRGroupOrQueryHydrogen
4090 // RDKit✔️✔️: bool isAtomTerminalRGroupOrQueryHydrogen(const Atom *atom) {
4091 // RDKit✔️✔️: return (atom->getDegree() == 1 && isAtomDummy(atom)) ||
4092 // RDKit✔️✔️: (atom->hasQuery() &&
4093 // RDKit✔️✔️: describeQuery(atom).find("AtomAtomicNum 1 = val") !=
4094 // RDKit✔️✔️: std::string::npos);
4095 // RDKit✔️✔️: }
4096 // END RDKIT CPP FUNCTION
4097 //
4098 // BEGIN RDKIT CPP FUNCTION: third_party/rdkit/Code/GraphMol/QueryOps.h :: isAtomDummy
4099 // RDKit✔️✔️: inline bool isAtomDummy(const Atom *a) {
4100 // RDKit✔️✔️: return (!a->hasQuery() && a->getAtomicNum() == 0) ||
4101 // RDKit✔️✔️: (a->hasQuery() && !a->getQuery()->getNegation() &&
4102 // RDKit✔️✔️: a->getQuery()->getDescription() == "AtomNull");
4103 // RDKit✔️✔️: }
4104 // END RDKIT CPP FUNCTION
4105 //
4106 // Local complexity review: degree is an indexed adjacency-slice length;
4107 // dummy classification is O(1), and the typed query traversal is O(n)
4108 // time/O(h) stack, matching describeQuery's traversal without allocating
4109 // its intermediate string. No molecule state or query node is cloned.
4110 let atom = &molecule.atoms()[atom_index];
4111 let is_dummy = match atom.query() {
4112 None => atom.atomic_number() == 0,
4113 Some(crate::QueryNode::Predicate(AtomQueryPredicate::Any)) => true,
4114 Some(_) => false,
4115 };
4116 (molecule
4117 .topology_block()
4118 .adjacency
4119 .neighbors_of(atom_index)
4120 .len()
4121 == 1
4122 && is_dummy)
4123 || atom
4124 .query()
4125 .is_some_and(|query| query_contains_atomic_number(query, 1))
4126}
4127
4128fn core_substitution_score(
4129 molecule: &Molecule,
4130 query: &Molecule,
4131 matched: &SubstructMatchResult,
4132) -> f64 {
4133 // BEGIN RDKIT CPP FUNCTION: third_party/rdkit/Code/GraphMol/Substruct/SubstructUtils.cpp :: detail::ScoreMatchesByDegreeOfCoreSubstitution
4134 // RDKit✔️✔️: class ScoreMatchesByDegreeOfCoreSubstitution {
4135 // RDKit✔️✔️: public:
4136 // RDKit✔️✔️: typedef std::pair<unsigned int, double> IdxScorePair;
4137 // RDKit✔️✔️: ScoreMatchesByDegreeOfCoreSubstitution(
4138 // RDKit✔️✔️: const RDKit::ROMol &mol, const RDKit::ROMol &query,
4139 // RDKit✔️✔️: const std::vector<RDKit::MatchVectType> &matches)
4140 // RDKit✔️✔️: : d_mol(mol),
4141 // RDKit✔️✔️: d_query(query),
4142 // RDKit✔️✔️: d_matches(matches),
4143 // RDKit✔️✔️: d_sumIndices(0.0),
4144 // RDKit✔️✔️: d_minIdx(-1),
4145 // RDKit✔️✔️: d_isSorted(false) {
4146 // RDKit✔️✔️: PRECONDITION(!matches.empty(), "matches must not be empty");
4147 // RDKit✔️✔️: auto na = d_mol.getNumAtoms();
4148 // RDKit✔️✔️: d_sumIndices = static_cast<double>(na * (na + 1) / 2);
4149 // RDKit✔️✔️: unsigned int i = 0;
4150 // RDKit✔️✔️: d_matchIdxVsScore.reserve(d_matches.size());
4151 // RDKit✔️✔️: for (const auto &match : d_matches) {
4152 // RDKit✔️✔️: d_matchIdxVsScore.emplace_back(i++, computeScore(match));
4153 // RDKit✔️✔️: }
4154 // RDKit✔️✔️: }
4155 // RDKit✔️✔️: const RDKit::MatchVectType &getMostSubstitutedCoreMatch() {
4156 // RDKit✔️✔️: if (d_minIdx == -1) {
4157 // RDKit✔️✔️: d_minIdx = std::min_element(d_matchIdxVsScore.begin(),
4158 // RDKit✔️✔️: d_matchIdxVsScore.end(), compare)
4159 // RDKit✔️✔️: ->first;
4160 // RDKit✔️✔️: }
4161 // RDKit✔️✔️: return d_matches.at(d_minIdx);
4162 // RDKit✔️✔️: }
4163 // RDKit✔️✔️: std::vector<MatchVectType> sortMatchesByDegreeOfCoreSubstitution() {
4164 // RDKit✔️✔️: if (!d_isSorted) {
4165 // RDKit✔️✔️: std::sort(d_matchIdxVsScore.begin(), d_matchIdxVsScore.end(), compare);
4166 // RDKit✔️✔️: d_isSorted = true;
4167 // RDKit✔️✔️: d_minIdx = d_matchIdxVsScore.front().first;
4168 // RDKit✔️✔️: }
4169 // RDKit✔️✔️: std::vector<MatchVectType> res(d_matches.size());
4170 // RDKit✔️✔️: std::transform(
4171 // RDKit✔️✔️: d_matchIdxVsScore.begin(), d_matchIdxVsScore.end(), res.begin(),
4172 // RDKit✔️✔️: [this](const IdxScorePair &pair) { return d_matches.at(pair.first); });
4173 // RDKit✔️✔️: return res;
4174 // RDKit✔️✔️: }
4175 // RDKit✔️✔️:
4176 // RDKit✔️✔️: private:
4177 // RDKit✔️✔️: static bool compare(const IdxScorePair &aPair, const IdxScorePair &bPair) {
4178 // RDKit✔️✔️: return (aPair.second < bPair.second);
4179 // RDKit✔️✔️: }
4180 // RDKit✔️✔️: bool doesRGroupMatchHydrogen(const std::pair<int, int> &pair) const {
4181 // RDKit✔️✔️: const auto queryAtom = d_query.getAtomWithIdx(pair.first);
4182 // RDKit✔️✔️: const auto molAtom = d_mol.getAtomWithIdx(pair.second);
4183 // RDKit✔️✔️: return (molAtom->getAtomicNum() == 1 &&
4184 // RDKit✔️✔️: isAtomTerminalRGroupOrQueryHydrogen(queryAtom));
4185 // RDKit✔️✔️: }
4186 // RDKit✔️✔️: double computeScore(const RDKit::MatchVectType &match) const {
4187 // RDKit✔️✔️: double penalty = 0.0;
4188 // RDKit✔️✔️: double i = 0.0;
4189 // RDKit✔️✔️: for (const auto &pair : match) {
4190 // RDKit✔️✔️: i += static_cast<double>(pair.second);
4191 // RDKit✔️✔️: if (doesRGroupMatchHydrogen(pair)) {
4192 // RDKit✔️✔️: penalty += 1.0;
4193 // RDKit✔️✔️: }
4194 // RDKit✔️✔️: }
4195 // RDKit✔️✔️: penalty += i / d_sumIndices;
4196 // RDKit✔️✔️: return penalty;
4197 // RDKit✔️✔️: }
4198 // RDKit✔️✔️: const RDKit::ROMol &d_mol;
4199 // RDKit✔️✔️: const RDKit::ROMol &d_query;
4200 // RDKit✔️✔️: const std::vector<RDKit::MatchVectType> &d_matches;
4201 // RDKit✔️✔️: std::vector<IdxScorePair> d_matchIdxVsScore;
4202 // RDKit✔️✔️: double d_sumIndices;
4203 // RDKit✔️✔️: int d_minIdx;
4204 // RDKit✔️✔️: bool d_isSorted;
4205 // RDKit✔️✔️: };
4206 // END RDKIT CPP FUNCTION
4207 //
4208 // The Rust wrappers compute and retain the same per-match scores without
4209 // materializing a stateful scorer object.
4210 let atom_count = molecule.num_atoms();
4211 let sum_indices = (atom_count * (atom_count + 1) / 2) as f64;
4212 let mut penalty = 0.0;
4213 let mut index_sum = 0.0;
4214 for (query_index, &molecule_index) in matched.atom_mapping.iter().enumerate() {
4215 index_sum += molecule_index as f64;
4216 if molecule.atoms()[molecule_index].atomic_number() == 1
4217 && is_atom_terminal_r_group_or_query_hydrogen(query, query_index)
4218 {
4219 penalty += 1.0;
4220 }
4221 }
4222 penalty + index_sum / sum_indices
4223}
4224
4225fn get_most_substituted_core_match<'a>(
4226 molecule: &Molecule,
4227 query: &Molecule,
4228 matches: &'a [SubstructMatchResult],
4229) -> &'a SubstructMatchResult {
4230 // BEGIN RDKIT CPP FUNCTION: third_party/rdkit/Code/GraphMol/Substruct/SubstructUtils.cpp :: getMostSubstitutedCoreMatch
4231 // RDKit✔️✔️: const MatchVectType &getMostSubstitutedCoreMatch(
4232 // RDKit✔️✔️: const ROMol &mol, const ROMol &core,
4233 // RDKit✔️✔️: const std::vector<MatchVectType> &matches) {
4234 // RDKit✔️✔️: detail::ScoreMatchesByDegreeOfCoreSubstitution matchScorer(mol, core,
4235 // RDKit✔️✔️: matches);
4236 // RDKit✔️✔️: return matchScorer.getMostSubstitutedCoreMatch();
4237 // RDKit✔️✔️: }
4238 // END RDKIT CPP FUNCTION
4239 //
4240 // The canonical scorer above reproduces the complete source helper class.
4241 // Local complexity review: one linear score pass and min selection gives
4242 // O(matches * query_atoms) time and O(1) auxiliary space, equivalent to
4243 // constructing and scanning RDKit's score vector, with fewer allocations.
4244 assert!(!matches.is_empty(), "matches must not be empty");
4245 matches
4246 .iter()
4247 .min_by(|left, right| {
4248 core_substitution_score(molecule, query, left)
4249 .total_cmp(&core_substitution_score(molecule, query, right))
4250 })
4251 .expect("non-empty matches")
4252}
4253
4254fn sort_matches_by_degree_of_core_substitution(
4255 molecule: &Molecule,
4256 query: &Molecule,
4257 matches: &[SubstructMatchResult],
4258) -> Vec<SubstructMatchResult> {
4259 // BEGIN RDKIT CPP FUNCTION: third_party/rdkit/Code/GraphMol/Substruct/SubstructUtils.cpp :: sortMatchesByDegreeOfCoreSubstitution
4260 // RDKit✔️✔️: std::vector<MatchVectType> sortMatchesByDegreeOfCoreSubstitution(
4261 // RDKit✔️✔️: const ROMol &mol, const ROMol &core,
4262 // RDKit✔️✔️: const std::vector<MatchVectType> &matches) {
4263 // RDKit✔️✔️: detail::ScoreMatchesByDegreeOfCoreSubstitution matchScorer(mol, core,
4264 // RDKit✔️✔️: matches);
4265 // RDKit✔️✔️: return matchScorer.sortMatchesByDegreeOfCoreSubstitution();
4266 // RDKit✔️✔️: }
4267 // END RDKIT CPP FUNCTION
4268 //
4269 // Local complexity review: scores are computed once and the indexed rows
4270 // are sorted in O(matches log matches), matching the source helper. The
4271 // returned mappings are cloned once, as in RDKit's result transform.
4272 assert!(!matches.is_empty(), "matches must not be empty");
4273 let mut scored = matches
4274 .iter()
4275 .enumerate()
4276 .map(|(index, matched)| (index, core_substitution_score(molecule, query, matched)))
4277 .collect::<Vec<_>>();
4278 scored.sort_by(|left, right| left.1.total_cmp(&right.1));
4279 scored
4280 .into_iter()
4281 .map(|(index, _)| matches[index].clone())
4282 .collect()
4283}
4284
4285fn substruct_match_impl(
4286 mol: &Molecule,
4287 query: &Molecule,
4288 params: &SubstructMatchParams,
4289) -> SubstructMatchResultList {
4290 // RDKit✔️❌: std::vector<MatchVectType> SubstructMatch(
4291 // RDKit✔️❌: const ROMol &mol, const ROMol &query,
4292 // RDKit✔️❌: const SubstructMatchParameters ¶ms) {
4293 // RDKit✔️❌: std::vector<MatchVectType> matches;
4294 // RDKit✔️❌: const auto &mNumAtoms = mol.getNumAtoms();
4295 // RDKit✔️❌: const auto &qNumAtoms = query.getNumAtoms();
4296 // RDKit✔️❌: if (!mNumAtoms || !qNumAtoms || qNumAtoms > mNumAtoms) {
4297 // RDKit✔️❌: return matches;
4298 // RDKit✔️❌: }
4299 // RDKit✔️❌:
4300 // RDKit✔️❌: detail::RecursiveLocker locker(query, params.recursionPossible);
4301 // RDKit✔️❌:
4302 // RDKit✔️❌: if (params.recursionPossible) {
4303 // RDKit✔️❌: detail::SUBQUERY_MAP subqueryMap;
4304 // RDKit✔️❌: ROMol::ConstAtomIterator atIt;
4305 // RDKit✔️❌: for (const auto atom : query.atoms()) {
4306 // RDKit✔️❌: if (atom->hasQuery()) {
4307 // RDKit✔️❌: detail::MatchSubqueries(mol, atom->getQuery(), params, subqueryMap,
4308 // RDKit✔️❌: locker.locked);
4309 // RDKit✔️❌: }
4310 // RDKit✔️❌: }
4311 // RDKit✔️❌: }
4312 // RDKit✔️❌:
4313 // RDKit✔️❌: detail::AtomLabelFunctor atomLabeler(query, mol, params);
4314 // RDKit✔️❌: detail::BondLabelFunctor bondLabeler(query, mol, params);
4315 // RDKit✔️❌: MolMatchFinalCheckFunctor matchChecker(query, mol, params);
4316 // RDKit✔️❌:
4317 // RDKit✔️❌: std::vector<detail::ssPairType> pms;
4318 // RDKit✔️❌: bool found =
4319 // RDKit✔️❌: boost::vf2_all(query.getTopology(), mol.getTopology(), atomLabeler,
4320 // RDKit✔️❌: bondLabeler, matchChecker, pms, params.maxMatches);
4321 // RDKit✔️❌: if (found) {
4322 // RDKit✔️❌: const unsigned int nQueryAtoms = query.getNumAtoms();
4323 // RDKit✔️❌: matches.reserve(pms.size());
4324 // RDKit✔️❌: MatchVectType matchVect(nQueryAtoms);
4325 // RDKit✔️❌: for (const auto &pairs : pms) {
4326 // RDKit✔️❌: for (const auto &pair : pairs) {
4327 // RDKit✔️❌: matchVect[pair.first] = pair;
4328 // RDKit✔️❌: }
4329 // RDKit✔️❌: matches.push_back(matchVect);
4330 // RDKit✔️❌: }
4331 // RDKit✔️❌: }
4332 // RDKit✔️❌: return matches;
4333 // RDKit✔️❌: }
4334 // Complexity review: preflight adds one linear query-tree scan for
4335 // fail-closed unsupported leaves. Recursive preparation, VF2 search, and
4336 // result materialization otherwise retain RDKit's asymptotic behavior.
4337 // The second marker remains ❌ because Rust's VF2 result path allocates
4338 // mapping Vecs at goal checks, as documented on the canonical VF2 core.
4339 preflight_query_molecule(query)?;
4340 if mol.num_atoms() == 0 || query.num_atoms() == 0 || query.num_atoms() > mol.num_atoms() {
4341 return Ok(Vec::new());
4342 }
4343 let mut recursive_locker = RecursiveLocker::new(query, params.recursion_possible);
4344 if params.recursion_possible {
4345 populate_recursive_query_match_cache(mol, query, params, &mut recursive_locker.cache)?;
4346 }
4347 substruct_match_impl_with_recursive_cache(mol, query, params, Some(&recursive_locker.cache))
4348}
4349
4350/// Check if a molecule contains a substructure match for the given query.
4351///
4352/// This is the public API for `has_substruct_match`.
4353/// RDKit✔️❌: VF2-based substructure matching ported from vf2.hpp + SubstructMatch.cpp.
4354pub fn has_substruct_match(mol: &Molecule, query: &Molecule) -> bool {
4355 let params = SubstructMatchParams::default();
4356 let mut params = params;
4357 params.max_matches = 1;
4358 substruct_match_impl(mol, query, ¶ms)
4359 .map(|matches| !matches.is_empty())
4360 .unwrap_or(false)
4361}
4362
4363/// Get the first substructure match, if any.
4364///
4365/// This is the public API for `get_substruct_match`.
4366/// RDKit✔️❌: VF2-based substructure matching ported from vf2.hpp + SubstructMatch.cpp.
4367pub fn get_substruct_match(mol: &Molecule, query: &Molecule) -> Option<SubstructMatchResult> {
4368 let params = SubstructMatchParams::default();
4369 let mut params = params;
4370 params.max_matches = 1;
4371 substruct_match_impl(mol, query, ¶ms)
4372 .ok()
4373 .and_then(|matches| matches.into_iter().next())
4374}
4375
4376/// Get all substructure matches with default parameters.
4377///
4378/// This is the public API for `get_substruct_matches`.
4379/// RDKit✔️❌: VF2-based substructure matching ported from vf2.hpp + SubstructMatch.cpp.
4380pub fn get_substruct_matches(mol: &Molecule, query: &Molecule) -> Vec<SubstructMatchResult> {
4381 let params = SubstructMatchParams::default();
4382 substruct_match_impl(mol, query, ¶ms).unwrap_or_default()
4383}
4384
4385/// Get all substructure matches with custom parameters.
4386///
4387/// This is the public API for `get_substruct_matches_with_params`.
4388/// RDKit✔️❌: VF2-based substructure matching ported from vf2.hpp + SubstructMatch.cpp.
4389pub fn get_substruct_matches_with_params(
4390 mol: &Molecule,
4391 query: &Molecule,
4392 params: &SubstructMatchParams,
4393) -> Vec<SubstructMatchResult> {
4394 substruct_match_impl(mol, query, params).unwrap_or_default()
4395}
4396
4397/// Get all substructure matches with custom parameters and structured
4398/// unsupported-feature errors for source-porting callers.
4399pub fn try_get_substruct_matches_with_params(
4400 mol: &Molecule,
4401 query: &Molecule,
4402 params: &SubstructMatchParams,
4403) -> SubstructMatchResultList {
4404 substruct_match_impl(mol, query, params)
4405}
4406
4407pub(crate) fn try_get_substruct_matches_with_params_and_context(
4408 mol: &Molecule,
4409 query: &Molecule,
4410 params: &SubstructMatchParams,
4411 query_context: &QueryMatchContext,
4412) -> SubstructMatchResultList {
4413 // This narrow entry retains the canonical preflight, recursive-query
4414 // preparation, VF2 implementation, final checks, and result ordering. It
4415 // only lets callers that run several immutable queries against one target
4416 // reuse the target-derived match context, as RDKit reuses ROMol state.
4417 preflight_query_molecule(query)?;
4418 if mol.num_atoms() == 0 || query.num_atoms() == 0 || query.num_atoms() > mol.num_atoms() {
4419 return Ok(Vec::new());
4420 }
4421 let mut recursive_locker = RecursiveLocker::new(query, params.recursion_possible);
4422 if params.recursion_possible {
4423 populate_recursive_query_match_cache(mol, query, params, &mut recursive_locker.cache)?;
4424 }
4425 substruct_match_impl_with_recursive_cache_and_context(
4426 mol,
4427 query,
4428 params,
4429 Some(&recursive_locker.cache),
4430 query_context,
4431 )
4432}
4433
4434// ---------------------------------------------------------------------------
4435// Tests
4436// ---------------------------------------------------------------------------
4437
4438#[cfg(test)]
4439mod tests {
4440 use super::*;
4441 use crate::MoleculeBuilder;
4442 use crate::search::smarts_parse::compile_query_fixture;
4443
4444 #[test]
4445 fn smarts_ring_connectivity_zero_rejects_ring_atoms() {
4446 let chain = Molecule::from_smiles("CCC").expect("chain");
4447 let ring = Molecule::from_smiles("C1CCCCC1").expect("ring");
4448 let no_ring_bonds = compile_query_fixture("[Cx0]").expect("x0 query");
4449 let has_ring_bond = compile_query_fixture("[Cx]").expect("x query");
4450
4451 assert!(!get_substruct_matches(&chain, &no_ring_bonds).is_empty());
4452 assert!(get_substruct_matches(&ring, &no_ring_bonds).is_empty());
4453 assert!(get_substruct_matches(&chain, &has_ring_bond).is_empty());
4454 assert!(!get_substruct_matches(&ring, &has_ring_bond).is_empty());
4455 }
4456
4457 #[test]
4458 fn shared_count_swaps_substruct_preserves_none_failure_mapping() {
4459 assert_eq!(count_swaps_to_interconvert_i32(&[1, 2], &[1]), None);
4460 assert_eq!(count_swaps_to_interconvert_i32(&[1, 2], &[1, 3]), None);
4461 }
4462
4463 fn make_mol_c() -> Molecule {
4464 // Methane: C
4465 let mut builder = MoleculeBuilder::new();
4466 builder.add_atom(crate::AtomSpec::new(crate::Element::C));
4467 builder.build().expect("build methane")
4468 }
4469
4470 fn make_mol_cc() -> Molecule {
4471 // Ethane: CC
4472 let mut builder = MoleculeBuilder::new();
4473 let c0 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
4474 let c1 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
4475 builder
4476 .add_bond(crate::BondSpec::new(c0, c1, BondOrder::Single))
4477 .expect("add bond");
4478 builder.build().expect("build ethane")
4479 }
4480
4481 fn make_mol_cco() -> Molecule {
4482 // Ethanol: CCO
4483 let mut builder = MoleculeBuilder::new();
4484 let c0 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
4485 let c1 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
4486 let o = builder.add_atom(crate::AtomSpec::new(crate::Element::O));
4487 builder
4488 .add_bond(crate::BondSpec::new(c0, c1, BondOrder::Single))
4489 .expect("add C-C bond");
4490 builder
4491 .add_bond(crate::BondSpec::new(c1, o, BondOrder::Single))
4492 .expect("add C-O bond");
4493 builder.build().expect("build ethanol")
4494 }
4495
4496 fn make_mol_coc() -> Molecule {
4497 // Dimethyl ether: COC
4498 let mut builder = MoleculeBuilder::new();
4499 let c0 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
4500 let o = builder.add_atom(crate::AtomSpec::new(crate::Element::O));
4501 let c1 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
4502 builder
4503 .add_bond(crate::BondSpec::new(c0, o, BondOrder::Single))
4504 .expect("add C-O bond 1");
4505 builder
4506 .add_bond(crate::BondSpec::new(o, c1, BondOrder::Single))
4507 .expect("add O-C bond 2");
4508 builder.build().expect("build dimethyl ether")
4509 }
4510
4511 #[test]
4512 fn test_has_substruct_match_self() {
4513 let c = make_mol_c();
4514 assert!(
4515 has_substruct_match(&c, &c),
4516 "a molecule should match itself"
4517 );
4518 }
4519
4520 #[test]
4521 fn test_has_substruct_match_cc_in_cco() {
4522 let cc = make_mol_cc();
4523 let cco = make_mol_cco();
4524 assert!(
4525 has_substruct_match(&cco, &cc),
4526 "CCO should contain CC as substructure"
4527 );
4528 }
4529
4530 #[test]
4531 fn test_has_substruct_match_no_match() {
4532 let c = make_mol_c();
4533 let cco = make_mol_cco();
4534 assert!(
4535 !has_substruct_match(&c, &cco),
4536 "a single carbon should not contain CCO"
4537 );
4538 }
4539
4540 #[test]
4541 fn test_get_substruct_match_self() {
4542 let cco = make_mol_cco();
4543 let result = get_substruct_match(&cco, &cco);
4544 assert!(result.is_some(), "self-match should return Some");
4545 let result = result.unwrap();
4546 assert_eq!(result.atom_mapping.len(), 3);
4547 // Identity mapping: 0->0, 1->1, 2->2
4548 for (qa, ma) in result.atom_mapping.iter().enumerate() {
4549 assert_eq!(*ma, qa, "self-match should have identity mapping");
4550 }
4551 }
4552
4553 #[test]
4554 fn test_get_substruct_match_cc_in_cco() {
4555 let cc = make_mol_cc();
4556 let cco = make_mol_cco();
4557 let result = get_substruct_match(&cco, &cc);
4558 assert!(result.is_some(), "CC should match in CCO");
4559 }
4560
4561 #[test]
4562 fn test_get_substruct_match_no_match() {
4563 let c = make_mol_c();
4564 let cco = make_mol_cco();
4565 let result = get_substruct_match(&c, &cco);
4566 assert!(
4567 result.is_none(),
4568 "C should not match CCO (query larger than mol)"
4569 );
4570 }
4571
4572 #[test]
4573 fn test_get_substruct_matches_cco_in_cco() {
4574 let cco = make_mol_cco();
4575 let matches = get_substruct_matches(&cco, &cco);
4576 assert!(!matches.is_empty(), "should find at least self-match");
4577 }
4578
4579 #[test]
4580 fn test_substruct_coc_matches_cco() {
4581 // COC (dimethyl ether) should not match CCO (ethanol) — different topology.
4582 let coc = make_mol_coc();
4583 let cco = make_mol_cco();
4584 assert!(
4585 !has_substruct_match(&cco, &coc),
4586 "CCO should not match COC topology"
4587 );
4588 // But CO should match CCO (CO is a substructure of CCO).
4589 let mut builder = MoleculeBuilder::new();
4590 let c = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
4591 let o = builder.add_atom(crate::AtomSpec::new(crate::Element::O));
4592 builder
4593 .add_bond(crate::BondSpec::new(c, o, BondOrder::Single))
4594 .expect("add CO bond");
4595 let co = builder.build().expect("build CO");
4596 assert!(has_substruct_match(&cco, &co), "CCO should match CO");
4597 }
4598
4599 #[test]
4600 fn test_has_substruct_match_empty_mol() {
4601 let empty = Molecule::new();
4602 let c = make_mol_c();
4603 assert!(
4604 !has_substruct_match(&empty, &c),
4605 "empty molecule should not match anything"
4606 );
4607 assert!(
4608 !has_substruct_match(&c, &empty),
4609 "molecule should not match empty query"
4610 );
4611 }
4612
4613 #[test]
4614 fn test_substruct_match_params_max_matches() {
4615 let c = make_mol_c();
4616 let params = SubstructMatchParams {
4617 max_matches: 1,
4618 uniquify: true,
4619 use_chirality: false,
4620 specified_stereo_query_matches_unspecified: false,
4621 ..Default::default()
4622 };
4623 let matches = get_substruct_matches_with_params(&c, &c, ¶ms);
4624 assert_eq!(matches.len(), 1, "max_matches=1 should return one match");
4625 }
4626
4627 #[test]
4628 fn feature_smarts_substruct_matches_required_query_semantics() {
4629 let cases = [
4630 (
4631 "Donor",
4632 "[$([N;!H0;v3,v4&+1]),$([O,S;H1;+0]),n&H1&+0]",
4633 "CCO",
4634 vec![2],
4635 vec![2],
4636 ),
4637 (
4638 "Acceptor",
4639 "[$([O,S;H1;v2;!$(*-*=[O,N,P,S])]),$([O,S;H0;v2]),$([O,S;-]),$([N;v3;!$(N-*=[O,N,P,S])]),n&H0&+0,$([o,s;+0;!$([o,s]:n);!$([o,s]:c:n)])]",
4640 "CC(=O)C",
4641 vec![2],
4642 vec![2],
4643 ),
4644 (
4645 "Aromatic",
4646 "[a]",
4647 "c1ccccc1",
4648 vec![0],
4649 vec![0, 1, 2, 3, 4, 5],
4650 ),
4651 ("Halogen", "[F,Cl,Br,I]", "CCl", vec![1], vec![1]),
4652 (
4653 "Basic",
4654 "[#7;+,$([N;H2&+0][$([C,a]);!$([C,a](=O))]),$([N;H1&+0]([$([C,a]);!$([C,a](=O))])[$([C,a]);!$([C,a](=O))]),$([N;H0&+0]([C;!$(C(=O))])([C;!$(C(=O))])[C;!$(C(=O))])]",
4655 "[NH4+]",
4656 vec![0],
4657 vec![0],
4658 ),
4659 (
4660 "Acidic",
4661 "[$([C,S](=[O,S,P])-[O;H1,-1])]",
4662 "CC(=O)O",
4663 vec![1],
4664 vec![1],
4665 ),
4666 ];
4667
4668 for (name, smarts, smiles, expected_first, expected_atoms) in cases {
4669 let mol = Molecule::from_smiles_with_sanitize(smiles, false)
4670 .unwrap_or_else(|_| panic!("{name} molecule should parse"));
4671 let query = compile_query_fixture(smarts)
4672 .unwrap_or_else(|_| panic!("{name} SMARTS should build query molecule"));
4673 let matches = get_substruct_matches(&mol, &query);
4674 assert!(
4675 !matches.is_empty(),
4676 "{name} should produce at least one match"
4677 );
4678 assert_eq!(
4679 matches[0].atom_mapping, expected_first,
4680 "{name} first match atom mapping"
4681 );
4682 let mut atom_indices: Vec<usize> = matches
4683 .iter()
4684 .flat_map(|matched| matched.atom_mapping.iter().copied())
4685 .filter(|idx| *idx != NULL_NODE)
4686 .collect();
4687 atom_indices.sort_unstable();
4688 atom_indices.dedup();
4689 assert_eq!(atom_indices, expected_atoms, "{name} feature SMARTS");
4690 }
4691 }
4692
4693 #[test]
4694 fn lipinski_hba_recursive_smarts_matches_rdkit_root_semantics() {
4695 const HBA: &str = "[$([O,S;H1;v2]-[!$(*=[O,N,P,S])]),$([O,S;H0;v2]),$([O,S;-]),$([N;v3;!$(N-*=!@[O,N,P,S])]),$([nH0X2,o,s;+0])]";
4696 let cases = [
4697 (
4698 "alcohol oxygen recursive branch",
4699 "[O,S;H1;v2]-[!$(*=[O,N,P,S])]",
4700 "CCO",
4701 vec![vec![2, 1]],
4702 ),
4703 (
4704 "carboxylic acid oxygen rejected by negated recursive neighbor",
4705 "[O,S;H1;v2]-[!$(*=[O,N,P,S])]",
4706 "CC(=O)O",
4707 Vec::<Vec<usize>>::new(),
4708 ),
4709 (
4710 "amine nitrogen recursive branch",
4711 "[N;v3;!$(N-*=!@[O,N,P,S])]",
4712 "CCN",
4713 vec![vec![2]],
4714 ),
4715 (
4716 "amide nitrogen rejected by mixed bond recursive query",
4717 "[N;v3;!$(N-*=!@[O,N,P,S])]",
4718 "CC(=O)N",
4719 Vec::<Vec<usize>>::new(),
4720 ),
4721 (
4722 "mixed single-double non-ring bond query",
4723 "N-*=!@[O,N,P,S]",
4724 "CC(=O)N",
4725 vec![vec![3, 1, 2]],
4726 ),
4727 ("full HBA ethanol", HBA, "CCO", vec![vec![2]]),
4728 ("full HBA carboxylic acid", HBA, "CC(=O)O", vec![vec![2]]),
4729 ("full HBA amide", HBA, "CC(=O)N", vec![vec![2]]),
4730 ("full HBA pyridine", HBA, "c1ccncc1", vec![vec![3]]),
4731 ("full HBA furan", HBA, "c1ccoc1", vec![vec![3]]),
4732 ];
4733
4734 for (name, smarts, smiles, expected) in cases {
4735 let mol = Molecule::from_smiles_with_sanitize(smiles, true)
4736 .unwrap_or_else(|_| panic!("{name} molecule should parse"));
4737 let query = compile_query_fixture(smarts)
4738 .unwrap_or_else(|_| panic!("{name} SMARTS should build"));
4739 let matches = get_substruct_matches(&mol, &query);
4740 let atom_mappings = matches
4741 .iter()
4742 .map(|matched| matched.atom_mapping.clone())
4743 .collect::<Vec<_>>();
4744 assert_eq!(atom_mappings, expected, "{name}");
4745 }
4746 }
4747
4748 #[test]
4749 fn smarts_recursive_compiled_query() {
4750 let query = compile_query_fixture("[$(C=O)_101,$(C=O)_101]")
4751 .expect("recursive SMARTS should compile once during parsing");
4752 let molecule = Molecule::from_smiles("CC(=O)C").expect("acetone fixture");
4753 let mut cache = RecursiveQueryMatchCache::new();
4754 populate_recursive_query_match_cache(
4755 &molecule,
4756 &query,
4757 &SubstructMatchParams::default(),
4758 &mut cache,
4759 )
4760 .expect("compiled recursive queries should populate the match cache");
4761
4762 assert_eq!(
4763 cache.len(),
4764 1,
4765 "equal serial numbers share one compiled result"
4766 );
4767 assert_eq!(
4768 get_substruct_matches(&molecule, &query)[0].atom_mapping,
4769 vec![1]
4770 );
4771 }
4772
4773 #[test]
4774 fn smarts_match_recursive() {
4775 let molecule = Molecule::from_smiles("CC(=O)C").expect("acetone fixture");
4776 let rooted_query = compile_query_fixture("C=O")
4777 .expect("inner query")
4778 .with_prop("_queryRootAtom", "1");
4779 let mut cache = RecursiveQueryMatchCache::new();
4780 let starts = recursive_matcher(
4781 &molecule,
4782 &rooted_query,
4783 &SubstructMatchParams::default(),
4784 &mut cache,
4785 )
4786 .expect("rooted recursive matcher");
4787 assert_eq!(
4788 starts
4789 .iter()
4790 .enumerate()
4791 .filter_map(|(index, matched)| matched.then_some(index))
4792 .collect::<Vec<_>>(),
4793 vec![2]
4794 );
4795
4796 let propane = Molecule::from_smiles("CCC").expect("propane fixture");
4797 let carbon = compile_query_fixture("C").expect("carbon query");
4798 let params = SubstructMatchParams {
4799 max_matches: 1,
4800 max_recursive_matches: 3,
4801 ..SubstructMatchParams::default()
4802 };
4803 let starts = recursive_matcher(
4804 &propane,
4805 &carbon,
4806 ¶ms,
4807 &mut RecursiveQueryMatchCache::new(),
4808 )
4809 .expect("recursive match limit");
4810 assert_eq!(starts, vec![true, true, true]);
4811 }
4812
4813 #[test]
4814 fn smarts_match_subqueries_execute() {
4815 let molecule = Molecule::from_smiles("CC(=O)C").expect("acetone fixture");
4816 let query =
4817 compile_query_fixture("[$(C=O)_101,$(C=O)_101]").expect("serial recursive query");
4818 let query_node = query.atoms()[0].query().expect("atom query tree");
4819 let mut cache = RecursiveQueryMatchCache::new();
4820 match_subqueries(
4821 &molecule,
4822 query_node,
4823 &SubstructMatchParams::default(),
4824 &mut cache,
4825 )
4826 .expect("execute recursive query tree");
4827
4828 assert_eq!(cache.len(), 1, "equal serials reuse one result");
4829 assert_eq!(
4830 cache.get(&RecursiveQueryCacheKey::Serial(101)),
4831 Some(&vec![false, true, false, false])
4832 );
4833 }
4834
4835 #[test]
4836 fn smarts_match_recursive_lock() {
4837 let molecule = Molecule::from_smiles("CC(=O)C").expect("acetone fixture");
4838 let query = compile_query_fixture("[$(C=O)]").expect("recursive SMARTS query");
4839 let enabled = SubstructMatchParams::default();
4840 assert_eq!(
4841 try_get_substruct_matches_with_params(&molecule, &query, &enabled)
4842 .expect("enabled recursive match")[0]
4843 .atom_mapping,
4844 vec![1]
4845 );
4846
4847 let disabled = SubstructMatchParams {
4848 recursion_possible: false,
4849 ..SubstructMatchParams::default()
4850 };
4851 assert!(
4852 try_get_substruct_matches_with_params(&molecule, &query, &disabled)
4853 .expect("disabled recursion is a non-match")
4854 .is_empty()
4855 );
4856
4857 assert_eq!(
4858 try_get_substruct_matches_with_params(&molecule, &query, &enabled)
4859 .expect("recursive state is rebuilt after scoped cleanup")[0]
4860 .atom_mapping,
4861 vec![1]
4862 );
4863 }
4864
4865 #[test]
4866 fn smarts_match_entry() {
4867 let empty = Molecule::new();
4868 let carbon = Molecule::from_smiles("C").expect("carbon fixture");
4869 assert!(
4870 try_get_substruct_matches_with_params(
4871 &empty,
4872 &carbon,
4873 &SubstructMatchParams::default(),
4874 )
4875 .expect("empty target")
4876 .is_empty()
4877 );
4878 assert!(
4879 try_get_substruct_matches_with_params(
4880 &carbon,
4881 &empty,
4882 &SubstructMatchParams::default(),
4883 )
4884 .expect("empty query")
4885 .is_empty()
4886 );
4887
4888 let ethane = Molecule::from_smiles("CC").expect("ethane fixture");
4889 assert!(
4890 try_get_substruct_matches_with_params(
4891 &carbon,
4892 ðane,
4893 &SubstructMatchParams::default(),
4894 )
4895 .expect("oversized query")
4896 .is_empty()
4897 );
4898
4899 let propane = Molecule::from_smiles("CCC").expect("propane fixture");
4900 let params = SubstructMatchParams {
4901 max_matches: 1,
4902 uniquify: false,
4903 ..SubstructMatchParams::default()
4904 };
4905 let matches = try_get_substruct_matches_with_params(&propane, ðane, ¶ms)
4906 .expect("bounded entry match");
4907 assert_eq!(matches.len(), 1);
4908 assert_eq!(matches[0].atom_mapping.len(), ethane.num_atoms());
4909 assert_eq!(matches[0].atom_mapping, vec![0, 1]);
4910 }
4911
4912 #[test]
4913 fn smarts_unsupported_query_errors() {
4914 let target = Molecule::from_smiles("CC").expect("target fixture");
4915 let params = SubstructMatchParams::default();
4916
4917 let mut atom_builder = MoleculeBuilder::new();
4918 atom_builder.add_atom(crate::AtomSpec::new(crate::Element::C).with_query(
4919 crate::QueryNode::and(vec![
4920 crate::QueryNode::predicate(AtomQueryPredicate::Any),
4921 crate::QueryNode::not(crate::QueryNode::predicate(
4922 AtomQueryPredicate::UnsupportedFeature("unsupported atom leaf"),
4923 )),
4924 ]),
4925 ));
4926 let atom_query = atom_builder.build().expect("atom query fixture");
4927 assert_eq!(
4928 try_get_substruct_matches_with_params(&target, &atom_query, ¶ms),
4929 Err(SubstructMatchError::Unsupported {
4930 branch: "unsupported atom leaf",
4931 rdkit_function: "QueryAtom::Match",
4932 })
4933 );
4934
4935 let mut bond_builder = MoleculeBuilder::new();
4936 let begin = bond_builder.add_atom(crate::AtomSpec::new(crate::Element::C));
4937 let end = bond_builder.add_atom(crate::AtomSpec::new(crate::Element::C));
4938 bond_builder
4939 .add_bond(
4940 crate::BondSpec::new(begin, end, BondOrder::Single).with_query(
4941 crate::QueryNode::or(vec![
4942 crate::QueryNode::predicate(BondQueryPredicate::Any),
4943 crate::QueryNode::predicate(BondQueryPredicate::UnsupportedFeature(
4944 "unsupported bond leaf",
4945 )),
4946 ]),
4947 ),
4948 )
4949 .expect("bond query edge");
4950 let bond_query = bond_builder.build().expect("bond query fixture");
4951 assert_eq!(
4952 try_get_substruct_matches_with_params(&target, &bond_query, ¶ms),
4953 Err(SubstructMatchError::Unsupported {
4954 branch: "unsupported bond leaf",
4955 rdkit_function: "QueryBond::Match",
4956 })
4957 );
4958
4959 let mut inner_builder = MoleculeBuilder::new();
4960 inner_builder.add_atom(crate::AtomSpec::new(crate::Element::C).with_query(
4961 crate::QueryNode::predicate(AtomQueryPredicate::UnsupportedFeature(
4962 "unsupported recursive leaf",
4963 )),
4964 ));
4965 let recursive_query = crate::search::query::RecursiveStructureQuery::from_molecule(
4966 inner_builder.build().expect("inner query fixture"),
4967 0,
4968 );
4969 let mut outer_builder = MoleculeBuilder::new();
4970 outer_builder.add_atom(crate::AtomSpec::new(crate::Element::DUMMY).with_query(
4971 crate::QueryNode::predicate(AtomQueryPredicate::RecursiveSmarts(recursive_query)),
4972 ));
4973 let outer_query = outer_builder.build().expect("outer query fixture");
4974 assert_eq!(
4975 try_get_substruct_matches_with_params(&target, &outer_query, ¶ms),
4976 Err(SubstructMatchError::Unsupported {
4977 branch: "unsupported recursive leaf",
4978 rdkit_function: "QueryAtom::Match",
4979 })
4980 );
4981 }
4982
4983 #[test]
4984 fn smarts_substruct_property_compat() {
4985 fn properties(entries: &[(&str, &str)]) -> BTreeMap<String, String> {
4986 entries
4987 .iter()
4988 .map(|(key, value)| ((*key).to_owned(), (*value).to_owned()))
4989 .collect()
4990 }
4991
4992 let requested = vec!["test_prop".to_owned()];
4993 let empty = properties(&[]);
4994 let one = properties(&[("test_prop", "1")]);
4995 let same = properties(&[("test_prop", "1"), ("ignored", "left")]);
4996 let different = properties(&[("test_prop", "2")]);
4997 let unrequested_difference = properties(&[("ignored", "right")]);
4998
4999 assert!(property_compat(&empty, &empty, &requested));
5000 assert!(property_compat(&one, &same, &requested));
5001 assert!(!property_compat(&one, &different, &requested));
5002 assert!(!property_compat(&one, &empty, &requested));
5003 assert!(!property_compat(&empty, &one, &requested));
5004 assert!(property_compat(&empty, &unrequested_difference, &requested));
5005 assert!(property_compat(&one, &different, &[]));
5006 assert!(!property_compat(
5007 &properties(&[("first", "same"), ("second", "left")]),
5008 &properties(&[("first", "same"), ("second", "right")]),
5009 &["first".to_owned(), "second".to_owned()],
5010 ));
5011 }
5012
5013 #[test]
5014 fn smarts_substruct_atom_compat() {
5015 fn carbon_chain(atom_count: usize, first_property: Option<&str>) -> Molecule {
5016 let mut builder = MoleculeBuilder::new();
5017 let mut atoms = Vec::with_capacity(atom_count);
5018 for atom_index in 0..atom_count {
5019 let mut atom = crate::AtomSpec::new(crate::Element::C);
5020 if atom_index == 0
5021 && let Some(value) = first_property
5022 {
5023 atom = atom.with_prop("test_prop", value);
5024 }
5025 atoms.push(builder.add_atom(atom));
5026 }
5027 for pair in atoms.windows(2) {
5028 builder
5029 .add_bond(crate::BondSpec::new(pair[0], pair[1], BondOrder::Single))
5030 .expect("chain bond");
5031 }
5032 builder.build().expect("carbon chain")
5033 }
5034
5035 let mut property_params = SubstructMatchParams::default();
5036 property_params.atom_properties = vec!["test_prop".to_owned()];
5037 let cases = [
5038 (None, None, 7),
5039 (Some("1"), Some("1"), 1),
5040 (Some("1"), None, 6),
5041 (None, Some("1"), 0),
5042 (Some("1"), Some("2"), 0),
5043 ];
5044 for (target_property, query_property, expected) in cases {
5045 let target = carbon_chain(9, target_property);
5046 let query = carbon_chain(3, query_property);
5047 assert_eq!(
5048 get_substruct_matches_with_params(&target, &query, &property_params).len(),
5049 expected,
5050 "target={target_property:?}, query={query_property:?}"
5051 );
5052 }
5053
5054 let query_query_molecule = |predicate| {
5055 let mut builder = MoleculeBuilder::new();
5056 builder.add_atom(
5057 crate::AtomSpec::new(crate::Element::C)
5058 .with_query(crate::QueryNode::predicate(predicate)),
5059 );
5060 builder.build().expect("single query atom")
5061 };
5062 let query = query_query_molecule(AtomQueryPredicate::AtomicNumber(6));
5063 let target = query_query_molecule(AtomQueryPredicate::AtomicNumber(8));
5064 assert!(has_substruct_match(&target, &query));
5065 let mut query_query_params = SubstructMatchParams::default();
5066 query_query_params.use_query_query_matches = true;
5067 assert!(get_substruct_matches_with_params(&target, &query, &query_query_params).is_empty());
5068
5069 let carbon = carbon_chain(1, None);
5070 let mut oxygen_builder = MoleculeBuilder::new();
5071 oxygen_builder.add_atom(crate::AtomSpec::new(crate::Element::O));
5072 let oxygen = oxygen_builder.build().expect("oxygen query");
5073
5074 let mut callback_params = SubstructMatchParams::default();
5075 let expected_query_index = 0;
5076 callback_params.extra_atom_check = Some(Arc::new(move |_, query_atom, _, mol_atom| {
5077 query_atom.id().index() == expected_query_index && mol_atom.atomic_number() == 6
5078 }));
5079 callback_params.extra_atom_check_overrides_default_check = true;
5080 assert_eq!(
5081 get_substruct_matches_with_params(&carbon, &oxygen, &callback_params).len(),
5082 1
5083 );
5084
5085 callback_params.extra_atom_check_overrides_default_check = false;
5086 assert!(get_substruct_matches_with_params(&carbon, &oxygen, &callback_params).is_empty());
5087
5088 callback_params.extra_atom_check = Some(Arc::new(|_, _, _, _| false));
5089 assert!(get_substruct_matches_with_params(&carbon, &carbon, &callback_params).is_empty());
5090 }
5091
5092 #[test]
5093 fn smarts_match_atom_coords() {
5094 fn one_atom_with_conformers(conformers: &[(usize, [f64; 3])]) -> Molecule {
5095 let mut builder = MoleculeBuilder::new();
5096 builder.add_atom(crate::AtomSpec::new(crate::Element::C));
5097 for &(id, position) in conformers {
5098 builder
5099 .add_conformer(crate::Conformer3D::new(id, vec![position], true))
5100 .expect("add conformer");
5101 }
5102 builder.build().expect("coordinate fixture")
5103 }
5104
5105 let query = one_atom_with_conformers(&[(0, [0.0, 0.0, 0.1]), (7, [5.0, 0.0, 0.0])]);
5106 let target = one_atom_with_conformers(&[(0, [0.0, 0.0, 0.0]), (9, [5.1, 0.0, 0.0])]);
5107 let missing = one_atom_with_conformers(&[]);
5108
5109 let default_matcher = AtomCoordsMatchFunctor::default();
5110 assert!(!default_matcher.matches(&query, &query.atoms()[0], &target, &target.atoms()[0],));
5111 assert!(
5112 !default_matcher.matches(&query, &query.atoms()[0], &missing, &missing.atoms()[0],)
5113 );
5114
5115 let matcher = AtomCoordsMatchFunctor::new(9, 7, 0.15);
5116 assert!(matcher.matches(&query, &query.atoms()[0], &target, &target.atoms()[0],));
5117 let mut params = SubstructMatchParams::default();
5118 params.extra_atom_check = Some(Arc::new(move |query_mol, query_atom, mol, mol_atom| {
5119 matcher.matches(query_mol, query_atom, mol, mol_atom)
5120 }));
5121 assert_eq!(
5122 try_get_substruct_matches_with_params(&target, &query, ¶ms)
5123 .expect("coordinate-constrained match")
5124 .len(),
5125 1
5126 );
5127 }
5128
5129 #[test]
5130 fn smarts_substruct_chiral_atom_compat() {
5131 fn atom(element: crate::Element, cip: Option<&str>) -> Molecule {
5132 let mut spec = crate::AtomSpec::new(element);
5133 if let Some(cip) = cip {
5134 spec = spec.with_prop("_CIPCode", cip);
5135 }
5136 let mut builder = MoleculeBuilder::new();
5137 builder.add_atom(spec);
5138 builder.build().expect("single atom fixture")
5139 }
5140
5141 let carbon = atom(crate::Element::C, None);
5142 let oxygen = atom(crate::Element::O, None);
5143 assert!(!chiral_atom_compat(
5144 &carbon.atoms()[0],
5145 &carbon,
5146 &oxygen.atoms()[0],
5147 &oxygen,
5148 ));
5149
5150 assert!(chiral_atom_compat(
5151 &carbon.atoms()[0],
5152 &carbon,
5153 &carbon.atoms()[0],
5154 &carbon,
5155 ));
5156
5157 let carbon_r = atom(crate::Element::C, Some("R"));
5158 let another_carbon_r = atom(crate::Element::C, Some("R"));
5159 let carbon_s = atom(crate::Element::C, Some("S"));
5160 assert!(chiral_atom_compat(
5161 &carbon_r.atoms()[0],
5162 &carbon_r,
5163 &another_carbon_r.atoms()[0],
5164 &another_carbon_r,
5165 ));
5166 assert!(!chiral_atom_compat(
5167 &carbon_r.atoms()[0],
5168 &carbon_r,
5169 &carbon_s.atoms()[0],
5170 &carbon_s,
5171 ));
5172 assert!(!chiral_atom_compat(
5173 &carbon_r.atoms()[0],
5174 &carbon_r,
5175 &carbon.atoms()[0],
5176 &carbon,
5177 ));
5178 assert!(!chiral_atom_compat(
5179 &carbon.atoms()[0],
5180 &carbon,
5181 &carbon_r.atoms()[0],
5182 &carbon_r,
5183 ));
5184 }
5185
5186 #[test]
5187 fn smarts_substruct_bond_compat() {
5188 fn two_atom_molecule(
5189 begin: crate::Element,
5190 end: crate::Element,
5191 bond: crate::BondSpec,
5192 ) -> Molecule {
5193 let mut builder = MoleculeBuilder::new();
5194 builder.add_atom(crate::AtomSpec::new(begin));
5195 builder.add_atom(crate::AtomSpec::new(end));
5196 builder.add_bond(bond).expect("two-atom bond");
5197 builder.build().expect("two-atom molecule")
5198 }
5199
5200 fn compatible(query: &Molecule, target: &Molecule, params: &SubstructMatchParams) -> bool {
5201 bond_compat(
5202 &query.bonds()[0],
5203 query,
5204 &target.bonds()[0],
5205 target,
5206 params,
5207 &build_query_match_context(target),
5208 )
5209 }
5210
5211 let single = two_atom_molecule(
5212 crate::Element::C,
5213 crate::Element::C,
5214 crate::BondSpec::new(
5215 crate::AtomId::new(0),
5216 crate::AtomId::new(1),
5217 BondOrder::Single,
5218 ),
5219 );
5220 let double = two_atom_molecule(
5221 crate::Element::C,
5222 crate::Element::C,
5223 crate::BondSpec::new(
5224 crate::AtomId::new(0),
5225 crate::AtomId::new(1),
5226 BondOrder::Double,
5227 ),
5228 );
5229 let unspecified = two_atom_molecule(
5230 crate::Element::C,
5231 crate::Element::C,
5232 crate::BondSpec::new(
5233 crate::AtomId::new(0),
5234 crate::AtomId::new(1),
5235 BondOrder::Unspecified,
5236 ),
5237 );
5238 assert!(!compatible(
5239 &single,
5240 &double,
5241 &SubstructMatchParams::default()
5242 ));
5243 assert!(compatible(
5244 &unspecified,
5245 &double,
5246 &SubstructMatchParams::default()
5247 ));
5248
5249 let aromatic = two_atom_molecule(
5250 crate::Element::C,
5251 crate::Element::C,
5252 crate::BondSpec::new(
5253 crate::AtomId::new(0),
5254 crate::AtomId::new(1),
5255 BondOrder::Aromatic,
5256 )
5257 .with_aromatic(true),
5258 );
5259 let conjugated_single = two_atom_molecule(
5260 crate::Element::C,
5261 crate::Element::C,
5262 crate::BondSpec::new(
5263 crate::AtomId::new(0),
5264 crate::AtomId::new(1),
5265 BondOrder::Single,
5266 )
5267 .with_conjugated(true),
5268 );
5269 let mut params = SubstructMatchParams::default();
5270 params.aromatic_matches_conjugated = true;
5271 assert!(compatible(&aromatic, &conjugated_single, ¶ms));
5272 assert!(!compatible(&aromatic, &single, ¶ms));
5273 params.aromatic_matches_conjugated = false;
5274 params.aromatic_matches_single_or_double = true;
5275 assert!(compatible(&aromatic, &single, ¶ms));
5276 assert!(compatible(&aromatic, &double, ¶ms));
5277
5278 let query_single = two_atom_molecule(
5279 crate::Element::C,
5280 crate::Element::C,
5281 crate::BondSpec::new(
5282 crate::AtomId::new(0),
5283 crate::AtomId::new(1),
5284 BondOrder::Single,
5285 )
5286 .with_query(crate::QueryNode::predicate(BondQueryPredicate::Order(
5287 BondOrder::Single,
5288 ))),
5289 );
5290 let query_double = two_atom_molecule(
5291 crate::Element::C,
5292 crate::Element::C,
5293 crate::BondSpec::new(
5294 crate::AtomId::new(0),
5295 crate::AtomId::new(1),
5296 BondOrder::Double,
5297 )
5298 .with_query(crate::QueryNode::predicate(BondQueryPredicate::Order(
5299 BondOrder::Double,
5300 ))),
5301 );
5302 let mut query_query_params = SubstructMatchParams::default();
5303 query_query_params.use_query_query_matches = true;
5304 assert!(!compatible(
5305 &query_single,
5306 &query_double,
5307 &query_query_params
5308 ));
5309
5310 let property_single = two_atom_molecule(
5311 crate::Element::C,
5312 crate::Element::C,
5313 crate::BondSpec::new(
5314 crate::AtomId::new(0),
5315 crate::AtomId::new(1),
5316 BondOrder::Single,
5317 )
5318 .with_prop("test_prop", "left"),
5319 );
5320 let mut property_params = SubstructMatchParams::default();
5321 property_params.bond_properties = vec!["test_prop".to_owned()];
5322 assert!(!compatible(&property_single, &single, &property_params));
5323
5324 let mut callback_params = SubstructMatchParams::default();
5325 callback_params.extra_bond_check = Some(Arc::new(|query, target| {
5326 query.order() == BondOrder::Single && target.order() == BondOrder::Double
5327 }));
5328 callback_params.extra_bond_check_overrides_default_check = true;
5329 assert!(compatible(&single, &double, &callback_params));
5330 callback_params.extra_bond_check_overrides_default_check = false;
5331 assert!(!compatible(&single, &double, &callback_params));
5332 callback_params.extra_bond_check = Some(Arc::new(|_, _| false));
5333 assert!(!compatible(&single, &single, &callback_params));
5334
5335 let dative_cn = two_atom_molecule(
5336 crate::Element::C,
5337 crate::Element::N,
5338 crate::BondSpec::new(
5339 crate::AtomId::new(0),
5340 crate::AtomId::new(1),
5341 BondOrder::Dative,
5342 ),
5343 );
5344 let dative_nc = two_atom_molecule(
5345 crate::Element::N,
5346 crate::Element::C,
5347 crate::BondSpec::new(
5348 crate::AtomId::new(0),
5349 crate::AtomId::new(1),
5350 BondOrder::Dative,
5351 ),
5352 );
5353 assert!(compatible(
5354 &dative_cn,
5355 &dative_cn,
5356 &SubstructMatchParams::default()
5357 ));
5358 assert!(!compatible(
5359 &dative_cn,
5360 &dative_nc,
5361 &SubstructMatchParams::default()
5362 ));
5363 }
5364
5365 #[test]
5366 fn smarts_substruct_remove_duplicates() {
5367 let result = |atom_mapping: &[usize], bond_mapping: &[usize]| SubstructMatchResult {
5368 atom_mapping: atom_mapping.to_vec(),
5369 bond_mapping: bond_mapping.to_vec(),
5370 };
5371 let first = result(&[0, 1, 2, 3], &[10, 11, 12]);
5372 let same_atom_set_different_path = result(&[3, 2, 1, 0], &[20, 21, 22]);
5373 let distinct = result(&[0, 1, 2, 4], &[30, 31, 32]);
5374 let repeated_distinct = result(&[4, 2, 1, 0], &[40, 41, 42]);
5375 let mut matches = vec![
5376 first.clone(),
5377 same_atom_set_different_path,
5378 distinct.clone(),
5379 repeated_distinct,
5380 ];
5381
5382 remove_duplicates(&mut matches, 5);
5383
5384 assert_eq!(matches, vec![first, distinct]);
5385 assert_eq!(matches.capacity(), matches.len());
5386 }
5387
5388 fn core_substitution_fixtures() -> (Molecule, Molecule, Vec<SubstructMatchResult>) {
5389 let mut molecule_builder = MoleculeBuilder::new();
5390 let carbon_zero = molecule_builder.add_atom(crate::AtomSpec::new(crate::Element::C));
5391 let hydrogen = molecule_builder.add_atom(crate::AtomSpec::new(crate::Element::H));
5392 let carbon_two = molecule_builder.add_atom(crate::AtomSpec::new(crate::Element::C));
5393 molecule_builder
5394 .add_bond(crate::BondSpec::new(
5395 carbon_zero,
5396 hydrogen,
5397 BondOrder::Single,
5398 ))
5399 .expect("C-H bond");
5400 molecule_builder
5401 .add_bond(crate::BondSpec::new(
5402 carbon_zero,
5403 carbon_two,
5404 BondOrder::Single,
5405 ))
5406 .expect("C-C bond");
5407 let molecule = molecule_builder.build().expect("target fixture");
5408
5409 let mut query_builder = MoleculeBuilder::new();
5410 let dummy = query_builder.add_atom(crate::AtomSpec::new(crate::Element::DUMMY));
5411 let carbon = query_builder.add_atom(crate::AtomSpec::new(crate::Element::C));
5412 query_builder
5413 .add_bond(crate::BondSpec::new(dummy, carbon, BondOrder::Single))
5414 .expect("query bond");
5415 let query = query_builder.build().expect("query fixture");
5416
5417 let hydrogen_match = SubstructMatchResult {
5418 atom_mapping: vec![1, 0],
5419 bond_mapping: vec![0],
5420 };
5421 let substituted_match = SubstructMatchResult {
5422 atom_mapping: vec![2, 0],
5423 bond_mapping: vec![1],
5424 };
5425 (molecule, query, vec![hydrogen_match, substituted_match])
5426 }
5427
5428 #[test]
5429 fn smarts_substruct_get_most_substituted_core_match() {
5430 let (molecule, query, matches) = core_substitution_fixtures();
5431 assert_eq!(
5432 get_most_substituted_core_match(&molecule, &query, &matches),
5433 &matches[1]
5434 );
5435 assert!(core_substitution_score(&molecule, &query, &matches[1]) < 1.0);
5436 assert!(core_substitution_score(&molecule, &query, &matches[0]) >= 1.0);
5437 }
5438
5439 #[test]
5440 #[should_panic(expected = "matches must not be empty")]
5441 fn smarts_substruct_get_most_substituted_core_match_rejects_empty() {
5442 let (molecule, query, _) = core_substitution_fixtures();
5443 let _ = get_most_substituted_core_match(&molecule, &query, &[]);
5444 }
5445
5446 #[test]
5447 fn smarts_substruct_sort_matches_by_degree_of_core_substitution() {
5448 let (molecule, query, matches) = core_substitution_fixtures();
5449 let sorted = sort_matches_by_degree_of_core_substitution(&molecule, &query, &matches);
5450 assert_eq!(sorted, vec![matches[1].clone(), matches[0].clone()]);
5451 assert_eq!(matches[0].atom_mapping, vec![1, 0]);
5452 }
5453
5454 #[test]
5455 fn smarts_substruct_is_atom_terminal_r_group_or_query_hydrogen() {
5456 let (_, terminal_dummy_query, _) = core_substitution_fixtures();
5457 assert!(is_atom_terminal_r_group_or_query_hydrogen(
5458 &terminal_dummy_query,
5459 0
5460 ));
5461 assert!(!is_atom_terminal_r_group_or_query_hydrogen(
5462 &terminal_dummy_query,
5463 1
5464 ));
5465
5466 let mut hydrogen_query_builder = MoleculeBuilder::new();
5467 hydrogen_query_builder.add_atom(crate::AtomSpec::new(crate::Element::DUMMY).with_query(
5468 crate::QueryNode::predicate(AtomQueryPredicate::AtomicNumber(1)),
5469 ));
5470 let hydrogen_query = hydrogen_query_builder
5471 .build()
5472 .expect("hydrogen query fixture");
5473 assert!(is_atom_terminal_r_group_or_query_hydrogen(
5474 &hydrogen_query,
5475 0
5476 ));
5477
5478 let mut nonterminal_dummy_builder = MoleculeBuilder::new();
5479 let dummy = nonterminal_dummy_builder.add_atom(crate::AtomSpec::new(crate::Element::DUMMY));
5480 let carbon_one =
5481 nonterminal_dummy_builder.add_atom(crate::AtomSpec::new(crate::Element::C));
5482 let carbon_two =
5483 nonterminal_dummy_builder.add_atom(crate::AtomSpec::new(crate::Element::C));
5484 nonterminal_dummy_builder
5485 .add_bond(crate::BondSpec::new(dummy, carbon_one, BondOrder::Single))
5486 .expect("first dummy bond");
5487 nonterminal_dummy_builder
5488 .add_bond(crate::BondSpec::new(dummy, carbon_two, BondOrder::Single))
5489 .expect("second dummy bond");
5490 let nonterminal_dummy = nonterminal_dummy_builder
5491 .build()
5492 .expect("nonterminal dummy fixture");
5493 assert!(!is_atom_terminal_r_group_or_query_hydrogen(
5494 &nonterminal_dummy,
5495 0
5496 ));
5497 }
5498
5499 #[test]
5500 fn smarts_substruct_update_substruct_match_params_from_j_s_o_n() {
5501 let mut params = SubstructMatchParams::default();
5502 params.max_matches = 77;
5503 update_substruct_match_params_from_json(&mut params, "").expect("empty JSON no-op");
5504 assert_eq!(params.max_matches, 77);
5505
5506 update_substruct_match_params_from_json(
5507 &mut params,
5508 r#"{
5509 "useChirality": true,
5510 "useEnhancedStereo": "true",
5511 "aromaticMatchesConjugated": true,
5512 "useQueryQueryMatches": "1",
5513 "recursionPossible": false,
5514 "uniquify": "false",
5515 "maxMatches": 12,
5516 "maxRecursiveMatches": "34",
5517 "numThreads": -2,
5518 "specifiedStereoQueryMatchesUnspecified": true,
5519 "aromaticMatchesSingleOrDouble": "true",
5520 "unknownOption": "ignored"
5521 }"#,
5522 )
5523 .expect("source JSON fields");
5524 assert!(params.use_chirality);
5525 assert!(params.use_enhanced_stereo);
5526 assert!(params.aromatic_matches_conjugated);
5527 assert!(params.use_query_query_matches);
5528 assert!(!params.recursion_possible);
5529 assert!(!params.uniquify);
5530 assert_eq!(params.max_matches, 12);
5531 assert_eq!(params.max_recursive_matches, 34);
5532 assert_eq!(params.num_threads, -2);
5533 assert!(params.specified_stereo_query_matches_unspecified);
5534 assert!(params.aromatic_matches_single_or_double);
5535
5536 let before = params.clone();
5537 assert!(
5538 update_substruct_match_params_from_json(
5539 &mut params,
5540 r#"{"useChirality": false, "maxMatches": "invalid"}"#,
5541 )
5542 .is_err()
5543 );
5544 assert_eq!(params.use_chirality, before.use_chirality);
5545 assert_eq!(params.max_matches, before.max_matches);
5546 }
5547
5548 #[test]
5549 fn smarts_substruct_substruct_match_params_to_j_s_o_n() {
5550 let mut params = SubstructMatchParams::default();
5551 params.use_chirality = true;
5552 params.use_enhanced_stereo = true;
5553 params.aromatic_matches_conjugated = true;
5554 params.use_query_query_matches = true;
5555 params.recursion_possible = false;
5556 params.uniquify = false;
5557 params.max_matches = 12;
5558 params.max_recursive_matches = 34;
5559 params.num_threads = -2;
5560 params.specified_stereo_query_matches_unspecified = true;
5561 params.aromatic_matches_single_or_double = true;
5562 params.atom_properties.push("notSerialized".to_owned());
5563 params.bond_properties.push("notSerialized".to_owned());
5564
5565 let json = substruct_match_params_to_json(¶ms);
5566 let value: serde_json::Value = serde_json::from_str(&json).expect("writer JSON");
5567 let object = value.as_object().expect("JSON object");
5568 assert_eq!(object.len(), 11);
5569 assert_eq!(object["useChirality"], "true");
5570 assert_eq!(object["maxMatches"], "12");
5571 assert_eq!(object["numThreads"], "-2");
5572 assert!(!object.contains_key("atomProperties"));
5573 assert!(!object.contains_key("bondProperties"));
5574
5575 let mut roundtrip = SubstructMatchParams::default();
5576 update_substruct_match_params_from_json(&mut roundtrip, &json).expect("writer roundtrip");
5577 assert_eq!(roundtrip.use_chirality, params.use_chirality);
5578 assert_eq!(roundtrip.use_enhanced_stereo, params.use_enhanced_stereo);
5579 assert_eq!(
5580 roundtrip.aromatic_matches_conjugated,
5581 params.aromatic_matches_conjugated
5582 );
5583 assert_eq!(
5584 roundtrip.use_query_query_matches,
5585 params.use_query_query_matches
5586 );
5587 assert_eq!(roundtrip.recursion_possible, params.recursion_possible);
5588 assert_eq!(roundtrip.uniquify, params.uniquify);
5589 assert_eq!(roundtrip.max_matches, params.max_matches);
5590 assert_eq!(
5591 roundtrip.max_recursive_matches,
5592 params.max_recursive_matches
5593 );
5594 assert_eq!(roundtrip.num_threads, params.num_threads);
5595 assert_eq!(
5596 roundtrip.specified_stereo_query_matches_unspecified,
5597 params.specified_stereo_query_matches_unspecified
5598 );
5599 assert_eq!(
5600 roundtrip.aromatic_matches_single_or_double,
5601 params.aromatic_matches_single_or_double
5602 );
5603 }
5604
5605 #[test]
5606 fn smarts_substruct_match_parameters() {
5607 let params = SubstructMatchParams::default();
5608 assert!(!params.use_chirality);
5609 assert!(!params.use_enhanced_stereo);
5610 assert!(!params.use_generic_matchers);
5611 assert!(params.recursion_possible);
5612 assert!(params.uniquify);
5613 assert_eq!(params.max_matches, 1000);
5614 assert_eq!(params.max_recursive_matches, 1000);
5615 assert_eq!(params.num_threads, 1);
5616 assert!(params.atom_properties.is_empty());
5617 assert!(params.bond_properties.is_empty());
5618 assert!(params.extra_atom_check.is_none());
5619 assert!(params.extra_bond_check.is_none());
5620 assert!(params.extra_final_check.is_none());
5621
5622 assert_eq!(
5623 check_substruct_match_overload_support(SubstructMatchOverload::Molecule),
5624 Ok(())
5625 );
5626 for (overload, expected_branch) in [
5627 (
5628 SubstructMatchOverload::MolBundle,
5629 "MolBundle substructure-match overloads",
5630 ),
5631 (
5632 SubstructMatchOverload::ResonanceMolSupplier,
5633 "resonance substructure-match overload",
5634 ),
5635 (
5636 SubstructMatchOverload::SubstructLibrary,
5637 "SubstructLibrary search overloads",
5638 ),
5639 ] {
5640 assert!(matches!(
5641 check_substruct_match_overload_support(overload),
5642 Err(SubstructMatchError::Unsupported { branch, .. }) if branch == expected_branch
5643 ));
5644 }
5645 }
5646
5647 #[test]
5648 fn maccs_patterns_substruct_matches_required_topology_semantics() {
5649 let cases = [
5650 (
5651 "four-membered ring",
5652 "*1~*~*~*~1",
5653 "C1CCC1",
5654 true,
5655 vec![0, 1, 2, 3],
5656 ),
5657 (
5658 "four-membered ring rejects chain",
5659 "*1~*~*~*~1",
5660 "CCCC",
5661 false,
5662 Vec::new(),
5663 ),
5664 ("ring bond", "*@*(@*)@*", "C12CC1C2", true, vec![0, 2, 1, 3]),
5665 (
5666 "non-ring oxygen bridge",
5667 "*!@[#8]!@*",
5668 "COC",
5669 true,
5670 vec![0, 1, 2],
5671 ),
5672 (
5673 "branch degree",
5674 "*~*(~*)(~*)~*",
5675 "CC(C)(C)C",
5676 true,
5677 vec![0, 1, 2, 3, 4],
5678 ),
5679 (
5680 "recursive ring closure",
5681 "[$([CH3]~*~*~[CH2]~*),$([CH3]~*1~*~[CH2]1)]",
5682 "CC1CC1",
5683 true,
5684 vec![0],
5685 ),
5686 ];
5687
5688 for (name, smarts, smiles, expected_match, expected_first) in cases {
5689 let mol = Molecule::from_smiles_with_sanitize(smiles, true)
5690 .unwrap_or_else(|_| panic!("{name} molecule should parse"));
5691 let query = compile_query_fixture(smarts)
5692 .unwrap_or_else(|_| panic!("{name} MACCS SMARTS should build query"));
5693 let matches = get_substruct_matches(&mol, &query);
5694 assert_eq!(
5695 !matches.is_empty(),
5696 expected_match,
5697 "{name} match truth value"
5698 );
5699 if expected_match {
5700 assert_eq!(
5701 matches[0].atom_mapping, expected_first,
5702 "{name} first match"
5703 );
5704 }
5705 }
5706 }
5707
5708 struct MaccsPatternGolden {
5709 bit: u16,
5710 smarts: &'static str,
5711 smiles: &'static str,
5712 first_match: &'static [usize],
5713 }
5714
5715 fn rdkit_maccs_pattern_positive_goldens() -> &'static [MaccsPatternGolden] {
5716 // RDKit source: MACCS.cpp::Patterns initializes these SMARTS strings
5717 // with `RDKit::SmartsToMol(...)`. `first_match` values were generated
5718 // from pinned RDKit 2026.03.1 using `Mol.GetSubstructMatch()`.
5719 &[
5720 MaccsPatternGolden {
5721 bit: 8,
5722 smarts: "[!#6!#1]1~*~*~*~1",
5723 smiles: "O1CCC1",
5724 first_match: &[0, 1, 2, 3],
5725 },
5726 MaccsPatternGolden {
5727 bit: 11,
5728 smarts: "*1~*~*~*~1",
5729 smiles: "C1CCC1",
5730 first_match: &[0, 1, 2, 3],
5731 },
5732 MaccsPatternGolden {
5733 bit: 13,
5734 smarts: "[#8]~[#7](~[#6])~[#6]",
5735 smiles: "ON(C)C",
5736 first_match: &[0, 1, 2, 3],
5737 },
5738 MaccsPatternGolden {
5739 bit: 14,
5740 smarts: "[#16]-[#16]",
5741 smiles: "CSSC",
5742 first_match: &[1, 2],
5743 },
5744 MaccsPatternGolden {
5745 bit: 15,
5746 smarts: "[#8]~[#6](~[#8])~[#8]",
5747 smiles: "O=C(O)O",
5748 first_match: &[0, 1, 2, 3],
5749 },
5750 MaccsPatternGolden {
5751 bit: 16,
5752 smarts: "[!#6!#1]1~*~*~1",
5753 smiles: "O1CC1",
5754 first_match: &[0, 1, 2],
5755 },
5756 MaccsPatternGolden {
5757 bit: 17,
5758 smarts: "[#6]#[#6]",
5759 smiles: "C#C",
5760 first_match: &[0, 1],
5761 },
5762 MaccsPatternGolden {
5763 bit: 19,
5764 smarts: "*1~*~*~*~*~*~*~1",
5765 smiles: "C1CCCCCC1",
5766 first_match: &[0, 1, 2, 3, 4, 5, 6],
5767 },
5768 MaccsPatternGolden {
5769 bit: 20,
5770 smarts: "[#14]",
5771 smiles: "[SiH4]",
5772 first_match: &[0],
5773 },
5774 MaccsPatternGolden {
5775 bit: 21,
5776 smarts: "[#6]=[#6](~[!#6!#1])~[!#6!#1]",
5777 smiles: "C=C(O)O",
5778 first_match: &[0, 1, 2, 3],
5779 },
5780 MaccsPatternGolden {
5781 bit: 22,
5782 smarts: "*1~*~*~1",
5783 smiles: "C1CC1",
5784 first_match: &[0, 1, 2],
5785 },
5786 MaccsPatternGolden {
5787 bit: 23,
5788 smarts: "[#7]~[#6](~[#8])~[#8]",
5789 smiles: "NC(=O)O",
5790 first_match: &[0, 1, 2, 3],
5791 },
5792 MaccsPatternGolden {
5793 bit: 24,
5794 smarts: "[#7]-[#8]",
5795 smiles: "ON(C)C",
5796 first_match: &[1, 0],
5797 },
5798 MaccsPatternGolden {
5799 bit: 25,
5800 smarts: "[#7]~[#6](~[#7])~[#7]",
5801 smiles: "NC(N)N",
5802 first_match: &[0, 1, 2, 3],
5803 },
5804 MaccsPatternGolden {
5805 bit: 26,
5806 smarts: "[#6]=@[#6](@*)@*",
5807 smiles: "C1=C2CCCC2C1",
5808 first_match: &[0, 1, 2, 5],
5809 },
5810 MaccsPatternGolden {
5811 bit: 28,
5812 smarts: "[!#6!#1]~[CH2]~[!#6!#1]",
5813 smiles: "OCO",
5814 first_match: &[0, 1, 2],
5815 },
5816 MaccsPatternGolden {
5817 bit: 30,
5818 smarts: "[#6]~[!#6!#1](~[#6])(~[#6])~*",
5819 smiles: "C[S](C)(C)C",
5820 first_match: &[0, 1, 2, 3, 4],
5821 },
5822 MaccsPatternGolden {
5823 bit: 31,
5824 smarts: "[!#6!#1]~[F,Cl,Br,I]",
5825 smiles: "N[Pt](Cl)(Cl)N",
5826 first_match: &[1, 2],
5827 },
5828 MaccsPatternGolden {
5829 bit: 32,
5830 smarts: "[#6]~[#16]~[#7]",
5831 smiles: "CSN",
5832 first_match: &[0, 1, 2],
5833 },
5834 MaccsPatternGolden {
5835 bit: 33,
5836 smarts: "[#7]~[#16]",
5837 smiles: "CSN",
5838 first_match: &[2, 1],
5839 },
5840 MaccsPatternGolden {
5841 bit: 34,
5842 smarts: "[CH2]=*",
5843 smiles: "C=C",
5844 first_match: &[0, 1],
5845 },
5846 MaccsPatternGolden {
5847 bit: 36,
5848 smarts: "[#16R]",
5849 smiles: "S1CC1",
5850 first_match: &[0],
5851 },
5852 MaccsPatternGolden {
5853 bit: 37,
5854 smarts: "[#7]~[#6](~[#8])~[#7]",
5855 smiles: "NC(=O)N",
5856 first_match: &[0, 1, 2, 3],
5857 },
5858 MaccsPatternGolden {
5859 bit: 38,
5860 smarts: "[#7]~[#6](~[#6])~[#7]",
5861 smiles: "NC(C)N",
5862 first_match: &[0, 1, 2, 3],
5863 },
5864 MaccsPatternGolden {
5865 bit: 39,
5866 smarts: "[#8]~[#16](~[#8])~[#8]",
5867 smiles: "COS(=O)(=O)O",
5868 first_match: &[1, 2, 3, 4],
5869 },
5870 MaccsPatternGolden {
5871 bit: 40,
5872 smarts: "[#16]-[#8]",
5873 smiles: "CSO",
5874 first_match: &[1, 2],
5875 },
5876 MaccsPatternGolden {
5877 bit: 41,
5878 smarts: "[#6]#[#7]",
5879 smiles: "C#N",
5880 first_match: &[0, 1],
5881 },
5882 MaccsPatternGolden {
5883 bit: 43,
5884 smarts: "[!#6!#1!H0]~*~[!#6!#1!H0]",
5885 smiles: "OCO",
5886 first_match: &[0, 1, 2],
5887 },
5888 MaccsPatternGolden {
5889 bit: 44,
5890 smarts: "[!#1;!#6;!#7;!#8;!#9;!#14;!#15;!#16;!#17;!#35;!#53]",
5891 smiles: "[SeH2]",
5892 first_match: &[0],
5893 },
5894 MaccsPatternGolden {
5895 bit: 45,
5896 smarts: "[#6]=[#6]~[#7]",
5897 smiles: "C=CN",
5898 first_match: &[0, 1, 2],
5899 },
5900 MaccsPatternGolden {
5901 bit: 47,
5902 smarts: "[#16]~*~[#7]",
5903 smiles: "SCN",
5904 first_match: &[0, 1, 2],
5905 },
5906 MaccsPatternGolden {
5907 bit: 48,
5908 smarts: "[#8]~[!#6!#1](~[#8])~[#8]",
5909 smiles: "COS(=O)(=O)O",
5910 first_match: &[1, 2, 3, 4],
5911 },
5912 MaccsPatternGolden {
5913 bit: 49,
5914 smarts: "[!+0]",
5915 smiles: "O=N(=O)O",
5916 first_match: &[0],
5917 },
5918 MaccsPatternGolden {
5919 bit: 50,
5920 smarts: "[#6]=[#6](~[#6])~[#6]",
5921 smiles: "CC(C)=C",
5922 first_match: &[3, 1, 0, 2],
5923 },
5924 MaccsPatternGolden {
5925 bit: 51,
5926 smarts: "[#6]~[#16]~[#8]",
5927 smiles: "CSO",
5928 first_match: &[0, 1, 2],
5929 },
5930 MaccsPatternGolden {
5931 bit: 52,
5932 smarts: "[#7]~[#7]",
5933 smiles: "NNO",
5934 first_match: &[0, 1],
5935 },
5936 MaccsPatternGolden {
5937 bit: 53,
5938 smarts: "[!#6!#1!H0]~*~*~*~[!#6!#1!H0]",
5939 smiles: "NCCCO",
5940 first_match: &[0, 1, 2, 3, 4],
5941 },
5942 MaccsPatternGolden {
5943 bit: 54,
5944 smarts: "[!#6!#1!H0]~*~*~[!#6!#1!H0]",
5945 smiles: "NCCN",
5946 first_match: &[0, 1, 2, 3],
5947 },
5948 MaccsPatternGolden {
5949 bit: 55,
5950 smarts: "[#8]~[#16]~[#8]",
5951 smiles: "CS(=O)(=O)C",
5952 first_match: &[2, 1, 3],
5953 },
5954 MaccsPatternGolden {
5955 bit: 56,
5956 smarts: "[#8]~[#7](~[#8])~[#6]",
5957 smiles: "ON(O)C",
5958 first_match: &[0, 1, 2, 3],
5959 },
5960 MaccsPatternGolden {
5961 bit: 57,
5962 smarts: "[#8R]",
5963 smiles: "O1CC1",
5964 first_match: &[0],
5965 },
5966 MaccsPatternGolden {
5967 bit: 58,
5968 smarts: "[!#6!#1]~[#16]~[!#6!#1]",
5969 smiles: "CS(=O)(=O)C",
5970 first_match: &[2, 1, 3],
5971 },
5972 MaccsPatternGolden {
5973 bit: 59,
5974 smarts: "[#16]!:*:*",
5975 smiles: "Sc1ccccc1",
5976 first_match: &[0, 1, 2],
5977 },
5978 MaccsPatternGolden {
5979 bit: 60,
5980 smarts: "[#16]=[#8]",
5981 smiles: "CS(=O)C",
5982 first_match: &[1, 2],
5983 },
5984 MaccsPatternGolden {
5985 bit: 61,
5986 smarts: "*~[#16](~*)~*",
5987 smiles: "C[S](C)(C)C",
5988 first_match: &[0, 1, 2, 3],
5989 },
5990 MaccsPatternGolden {
5991 bit: 62,
5992 smarts: "*@*!@*@*",
5993 smiles: "C1CC1C1CC1",
5994 first_match: &[0, 2, 3, 4],
5995 },
5996 MaccsPatternGolden {
5997 bit: 63,
5998 smarts: "[#7]=[#8]",
5999 smiles: "N=O",
6000 first_match: &[0, 1],
6001 },
6002 MaccsPatternGolden {
6003 bit: 64,
6004 smarts: "*@*!@[#16]",
6005 smiles: "Sc1ccccc1",
6006 first_match: &[2, 1, 0],
6007 },
6008 MaccsPatternGolden {
6009 bit: 65,
6010 smarts: "c:n",
6011 smiles: "c1ncccc1",
6012 first_match: &[0, 1],
6013 },
6014 MaccsPatternGolden {
6015 bit: 66,
6016 smarts: "[#6]~[#6](~[#6])(~[#6])~*",
6017 smiles: "CC(C)(C)C",
6018 first_match: &[0, 1, 2, 3, 4],
6019 },
6020 MaccsPatternGolden {
6021 bit: 67,
6022 smarts: "[!#6!#1]~[#16]",
6023 smiles: "CSSC",
6024 first_match: &[1, 2],
6025 },
6026 MaccsPatternGolden {
6027 bit: 68,
6028 smarts: "[!#6!#1!H0]~[!#6!#1!H0]",
6029 smiles: "NNO",
6030 first_match: &[0, 1],
6031 },
6032 MaccsPatternGolden {
6033 bit: 69,
6034 smarts: "[!#6!#1]~[!#6!#1!H0]",
6035 smiles: "CSN",
6036 first_match: &[1, 2],
6037 },
6038 MaccsPatternGolden {
6039 bit: 70,
6040 smarts: "[!#6!#1]~[#7]~[!#6!#1]",
6041 smiles: "NNO",
6042 first_match: &[0, 1, 2],
6043 },
6044 MaccsPatternGolden {
6045 bit: 71,
6046 smarts: "[#7]~[#8]",
6047 smiles: "N=O",
6048 first_match: &[0, 1],
6049 },
6050 MaccsPatternGolden {
6051 bit: 72,
6052 smarts: "[#8]~*~*~[#8]",
6053 smiles: "OCCO",
6054 first_match: &[0, 1, 2, 3],
6055 },
6056 MaccsPatternGolden {
6057 bit: 73,
6058 smarts: "[#16]=*",
6059 smiles: "CS(=O)C",
6060 first_match: &[1, 2],
6061 },
6062 MaccsPatternGolden {
6063 bit: 74,
6064 smarts: "[CH3]~*~[CH3]",
6065 smiles: "CCC",
6066 first_match: &[0, 1, 2],
6067 },
6068 MaccsPatternGolden {
6069 bit: 75,
6070 smarts: "*!@[#7]@*",
6071 smiles: "CN1CC1",
6072 first_match: &[0, 1, 2],
6073 },
6074 MaccsPatternGolden {
6075 bit: 76,
6076 smarts: "[#6]=[#6](~*)~*",
6077 smiles: "CC(C)=C",
6078 first_match: &[3, 1, 0, 2],
6079 },
6080 MaccsPatternGolden {
6081 bit: 77,
6082 smarts: "[#7]~*~[#7]",
6083 smiles: "NC(=O)N",
6084 first_match: &[0, 1, 3],
6085 },
6086 MaccsPatternGolden {
6087 bit: 78,
6088 smarts: "[#6]=[#7]",
6089 smiles: "C=N",
6090 first_match: &[0, 1],
6091 },
6092 MaccsPatternGolden {
6093 bit: 79,
6094 smarts: "[#7]~*~*~[#7]",
6095 smiles: "NCCN",
6096 first_match: &[0, 1, 2, 3],
6097 },
6098 MaccsPatternGolden {
6099 bit: 80,
6100 smarts: "[#7]~*~*~*~[#7]",
6101 smiles: "NCCCN",
6102 first_match: &[0, 1, 2, 3, 4],
6103 },
6104 MaccsPatternGolden {
6105 bit: 81,
6106 smarts: "[#16]~*(~*)~*",
6107 smiles: "Sc1ccccc1",
6108 first_match: &[0, 1, 2, 6],
6109 },
6110 MaccsPatternGolden {
6111 bit: 82,
6112 smarts: "*~[CH2]~[!#6!#1!H0]",
6113 smiles: "CCO",
6114 first_match: &[0, 1, 2],
6115 },
6116 MaccsPatternGolden {
6117 bit: 83,
6118 smarts: "[!#6!#1]1~*~*~*~*~1",
6119 smiles: "O1CCCC1",
6120 first_match: &[0, 1, 2, 3, 4],
6121 },
6122 MaccsPatternGolden {
6123 bit: 84,
6124 smarts: "[NH2]",
6125 smiles: "CCN",
6126 first_match: &[2],
6127 },
6128 MaccsPatternGolden {
6129 bit: 85,
6130 smarts: "[#6]~[#7](~[#6])~[#6]",
6131 smiles: "CN(C)C",
6132 first_match: &[0, 1, 2, 3],
6133 },
6134 MaccsPatternGolden {
6135 bit: 86,
6136 smarts: "[C;H2,H3][!#6!#1][C;H2,H3]",
6137 smiles: "COC",
6138 first_match: &[0, 1, 2],
6139 },
6140 MaccsPatternGolden {
6141 bit: 87,
6142 smarts: "[F,Cl,Br,I]!@*@*",
6143 smiles: "Clc1ccccc1",
6144 first_match: &[0, 1, 2],
6145 },
6146 MaccsPatternGolden {
6147 bit: 89,
6148 smarts: "[#8]~*~*~*~[#8]",
6149 smiles: "OCCCO",
6150 first_match: &[0, 1, 2, 3, 4],
6151 },
6152 MaccsPatternGolden {
6153 bit: 90,
6154 smarts: "[$([!#6!#1!H0]~*~*~[CH2]~*),$([!#6!#1!H0R]1@[R]@[R]@[CH2R]1),$([!#6!#1!H0]~[R]1@[R]@[CH2R]1)]",
6155 smiles: "N1CCC1",
6156 first_match: &[0],
6157 },
6158 MaccsPatternGolden {
6159 bit: 91,
6160 smarts: "[$([!#6!#1!H0]~*~*~*~[CH2]~*),$([!#6!#1!H0R]1@[R]@[R]@[R]@[CH2R]1),$([!#6!#1!H0]~[R]1@[R]@[R]@[CH2R]1),$([!#6!#1!H0]~*~[R]1@[R]@[CH2R]1)]",
6161 smiles: "NCCCCCN",
6162 first_match: &[0],
6163 },
6164 MaccsPatternGolden {
6165 bit: 92,
6166 smarts: "[#8]~[#6](~[#7])~[#6]",
6167 smiles: "CC(=O)N",
6168 first_match: &[2, 1, 3, 0],
6169 },
6170 MaccsPatternGolden {
6171 bit: 93,
6172 smarts: "[!#6!#1]~[CH3]",
6173 smiles: "COC",
6174 first_match: &[1, 0],
6175 },
6176 MaccsPatternGolden {
6177 bit: 94,
6178 smarts: "[!#6!#1]~[#7]",
6179 smiles: "CSN",
6180 first_match: &[1, 2],
6181 },
6182 MaccsPatternGolden {
6183 bit: 95,
6184 smarts: "[#7]~*~*~[#8]",
6185 smiles: "NCCO",
6186 first_match: &[0, 1, 2, 3],
6187 },
6188 MaccsPatternGolden {
6189 bit: 96,
6190 smarts: "*1~*~*~*~*~1",
6191 smiles: "C1CCCC1",
6192 first_match: &[0, 1, 2, 3, 4],
6193 },
6194 MaccsPatternGolden {
6195 bit: 97,
6196 smarts: "[#7]~*~*~*~[#8]",
6197 smiles: "NCCCO",
6198 first_match: &[0, 1, 2, 3, 4],
6199 },
6200 MaccsPatternGolden {
6201 bit: 98,
6202 smarts: "[!#6!#1]1~*~*~*~*~*~1",
6203 smiles: "O1CCCCC1",
6204 first_match: &[0, 1, 2, 3, 4, 5],
6205 },
6206 MaccsPatternGolden {
6207 bit: 99,
6208 smarts: "[#6]=[#6]",
6209 smiles: "C=C",
6210 first_match: &[0, 1],
6211 },
6212 MaccsPatternGolden {
6213 bit: 100,
6214 smarts: "*~[CH2]~[#7]",
6215 smiles: "CCN",
6216 first_match: &[0, 1, 2],
6217 },
6218 MaccsPatternGolden {
6219 bit: 101,
6220 smarts: "[$([R]1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@1),$([R]1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@1),$([R]1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@1),$([R]1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@1),$([R]1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@1),$([R]1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@1),$([R]1@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@[R]@1)]",
6221 smiles: "C1CCCCCCC1",
6222 first_match: &[0],
6223 },
6224 MaccsPatternGolden {
6225 bit: 102,
6226 smarts: "[!#6!#1]~[#8]",
6227 smiles: "CSO",
6228 first_match: &[1, 2],
6229 },
6230 MaccsPatternGolden {
6231 bit: 104,
6232 smarts: "[!#6!#1!H0]~*~[CH2]~*",
6233 smiles: "CCCO",
6234 first_match: &[3, 2, 1, 0],
6235 },
6236 MaccsPatternGolden {
6237 bit: 105,
6238 smarts: "*@*(@*)@*",
6239 smiles: "C12CC1C2",
6240 first_match: &[0, 2, 1, 3],
6241 },
6242 MaccsPatternGolden {
6243 bit: 106,
6244 smarts: "[!#6!#1]~*(~[!#6!#1])~[!#6!#1]",
6245 smiles: "COS(=O)(=O)O",
6246 first_match: &[1, 2, 3, 4],
6247 },
6248 MaccsPatternGolden {
6249 bit: 107,
6250 smarts: "[F,Cl,Br,I]~*(~*)~*",
6251 smiles: "CC(C)(C)Cl",
6252 first_match: &[4, 1, 0, 2],
6253 },
6254 MaccsPatternGolden {
6255 bit: 108,
6256 smarts: "[CH3]~*~*~*~[CH2]~*",
6257 smiles: "CCCCCC",
6258 first_match: &[0, 1, 2, 3, 4, 5],
6259 },
6260 MaccsPatternGolden {
6261 bit: 109,
6262 smarts: "*~[CH2]~[#8]",
6263 smiles: "CCO",
6264 first_match: &[0, 1, 2],
6265 },
6266 MaccsPatternGolden {
6267 bit: 110,
6268 smarts: "[#7]~[#6]~[#8]",
6269 smiles: "CC(=O)N",
6270 first_match: &[3, 1, 2],
6271 },
6272 MaccsPatternGolden {
6273 bit: 111,
6274 smarts: "[#7]~*~[CH2]~*",
6275 smiles: "CCCN",
6276 first_match: &[3, 2, 1, 0],
6277 },
6278 MaccsPatternGolden {
6279 bit: 112,
6280 smarts: "*~*(~*)(~*)~*",
6281 smiles: "CC(C)(C)C",
6282 first_match: &[0, 1, 2, 3, 4],
6283 },
6284 MaccsPatternGolden {
6285 bit: 113,
6286 smarts: "[#8]!:*:*",
6287 smiles: "Oc1ccccc1",
6288 first_match: &[0, 1, 2],
6289 },
6290 MaccsPatternGolden {
6291 bit: 114,
6292 smarts: "[CH3]~[CH2]~*",
6293 smiles: "CCC",
6294 first_match: &[0, 1, 2],
6295 },
6296 MaccsPatternGolden {
6297 bit: 115,
6298 smarts: "[CH3]~*~[CH2]~*",
6299 smiles: "CCCC",
6300 first_match: &[0, 1, 2, 3],
6301 },
6302 MaccsPatternGolden {
6303 bit: 116,
6304 smarts: "[$([CH3]~*~*~[CH2]~*),$([CH3]~*1~*~[CH2]1)]",
6305 smiles: "CCCCC",
6306 first_match: &[0],
6307 },
6308 MaccsPatternGolden {
6309 bit: 117,
6310 smarts: "[#7]~*~[#8]",
6311 smiles: "CC(=O)N",
6312 first_match: &[3, 1, 2],
6313 },
6314 MaccsPatternGolden {
6315 bit: 118,
6316 smarts: "[$(*~[CH2]~[CH2]~*),$(*1~[CH2]~[CH2]1)]",
6317 smiles: "CCCC",
6318 first_match: &[0],
6319 },
6320 MaccsPatternGolden {
6321 bit: 119,
6322 smarts: "[#7]=*",
6323 smiles: "N=O",
6324 first_match: &[0, 1],
6325 },
6326 MaccsPatternGolden {
6327 bit: 120,
6328 smarts: "[!#6R]",
6329 smiles: "O1CC1",
6330 first_match: &[0],
6331 },
6332 MaccsPatternGolden {
6333 bit: 121,
6334 smarts: "[#7R]",
6335 smiles: "N1CC1",
6336 first_match: &[0],
6337 },
6338 MaccsPatternGolden {
6339 bit: 122,
6340 smarts: "*~[#7](~*)~*",
6341 smiles: "ON(C)C",
6342 first_match: &[0, 1, 2, 3],
6343 },
6344 MaccsPatternGolden {
6345 bit: 123,
6346 smarts: "[#8]~[#6]~[#8]",
6347 smiles: "OCO",
6348 first_match: &[0, 1, 2],
6349 },
6350 MaccsPatternGolden {
6351 bit: 124,
6352 smarts: "[!#6!#1]~[!#6!#1]",
6353 smiles: "CSSC",
6354 first_match: &[1, 2],
6355 },
6356 MaccsPatternGolden {
6357 bit: 126,
6358 smarts: "*!@[#8]!@*",
6359 smiles: "COC",
6360 first_match: &[0, 1, 2],
6361 },
6362 MaccsPatternGolden {
6363 bit: 127,
6364 smarts: "*@*!@[#8]",
6365 smiles: "Oc1ccccc1",
6366 first_match: &[2, 1, 0],
6367 },
6368 MaccsPatternGolden {
6369 bit: 128,
6370 smarts: "[$(*~[CH2]~*~*~*~[CH2]~*),$([R]1@[CH2R]@[R]@[R]@[R]@[CH2R]1),$(*~[CH2]~[R]1@[R]@[R]@[CH2R]1),$(*~[CH2]~*~[R]1@[R]@[CH2R]1)]",
6371 smiles: "CCCCCCC",
6372 first_match: &[0],
6373 },
6374 MaccsPatternGolden {
6375 bit: 129,
6376 smarts: "[$(*~[CH2]~*~*~[CH2]~*),$([R]1@[CH2]@[R]@[R]@[CH2R]1),$(*~[CH2]~[R]1@[R]@[CH2R]1)]",
6377 smiles: "CCCCCC",
6378 first_match: &[0],
6379 },
6380 MaccsPatternGolden {
6381 bit: 131,
6382 smarts: "[!#6!#1!H0]",
6383 smiles: "CCO",
6384 first_match: &[2],
6385 },
6386 MaccsPatternGolden {
6387 bit: 132,
6388 smarts: "[#8]~*~[CH2]~*",
6389 smiles: "CCCO",
6390 first_match: &[3, 2, 1, 0],
6391 },
6392 MaccsPatternGolden {
6393 bit: 133,
6394 smarts: "*@*!@[#7]",
6395 smiles: "Nc1ccccc1",
6396 first_match: &[2, 1, 0],
6397 },
6398 MaccsPatternGolden {
6399 bit: 135,
6400 smarts: "[#7]!:*:*",
6401 smiles: "Nc1ccccc1",
6402 first_match: &[0, 1, 2],
6403 },
6404 MaccsPatternGolden {
6405 bit: 136,
6406 smarts: "[#8]=*",
6407 smiles: "CS(=O)C",
6408 first_match: &[2, 1],
6409 },
6410 MaccsPatternGolden {
6411 bit: 137,
6412 smarts: "[!C!cR]",
6413 smiles: "O1CC1",
6414 first_match: &[0],
6415 },
6416 MaccsPatternGolden {
6417 bit: 138,
6418 smarts: "[!#6!#1]~[CH2]~*",
6419 smiles: "CCO",
6420 first_match: &[2, 1, 0],
6421 },
6422 MaccsPatternGolden {
6423 bit: 139,
6424 smarts: "[O!H0]",
6425 smiles: "CCO",
6426 first_match: &[2],
6427 },
6428 MaccsPatternGolden {
6429 bit: 140,
6430 smarts: "[#8]",
6431 smiles: "CCO",
6432 first_match: &[2],
6433 },
6434 MaccsPatternGolden {
6435 bit: 141,
6436 smarts: "[CH3]",
6437 smiles: "CC",
6438 first_match: &[0],
6439 },
6440 MaccsPatternGolden {
6441 bit: 142,
6442 smarts: "[#7]",
6443 smiles: "C#N",
6444 first_match: &[1],
6445 },
6446 MaccsPatternGolden {
6447 bit: 144,
6448 smarts: "*!:*:*!:*",
6449 smiles: "Cc1ccccc1C",
6450 first_match: &[0, 1, 6, 7],
6451 },
6452 MaccsPatternGolden {
6453 bit: 145,
6454 smarts: "*1~*~*~*~*~*~1",
6455 smiles: "C1CCCCC1",
6456 first_match: &[0, 1, 2, 3, 4, 5],
6457 },
6458 MaccsPatternGolden {
6459 bit: 147,
6460 smarts: "[$(*~[CH2]~[CH2]~*),$([R]1@[CH2R]@[CH2R]1)]",
6461 smiles: "CCCC",
6462 first_match: &[0],
6463 },
6464 MaccsPatternGolden {
6465 bit: 148,
6466 smarts: "*~[!#6!#1](~*)~*",
6467 smiles: "C[S](C)(C)C",
6468 first_match: &[0, 1, 2, 3],
6469 },
6470 MaccsPatternGolden {
6471 bit: 149,
6472 smarts: "[C;H3,H4]",
6473 smiles: "C",
6474 first_match: &[0],
6475 },
6476 MaccsPatternGolden {
6477 bit: 150,
6478 smarts: "*!@*@*!@*",
6479 smiles: "Cc1ccccc1C",
6480 first_match: &[0, 1, 6, 7],
6481 },
6482 MaccsPatternGolden {
6483 bit: 151,
6484 smarts: "[#7!H0]",
6485 smiles: "CCN",
6486 first_match: &[2],
6487 },
6488 MaccsPatternGolden {
6489 bit: 152,
6490 smarts: "[#8]~[#6](~[#6])~[#6]",
6491 smiles: "CC(C)(C)O",
6492 first_match: &[4, 1, 0, 2],
6493 },
6494 MaccsPatternGolden {
6495 bit: 154,
6496 smarts: "[#6]=[#8]",
6497 smiles: "CC(=O)O",
6498 first_match: &[1, 2],
6499 },
6500 MaccsPatternGolden {
6501 bit: 155,
6502 smarts: "*!@[CH2]!@*",
6503 smiles: "CCC",
6504 first_match: &[0, 1, 2],
6505 },
6506 MaccsPatternGolden {
6507 bit: 156,
6508 smarts: "[#7]~*(~*)~*",
6509 smiles: "CC(C)N",
6510 first_match: &[3, 1, 0, 2],
6511 },
6512 MaccsPatternGolden {
6513 bit: 157,
6514 smarts: "[#6]-[#8]",
6515 smiles: "CCO",
6516 first_match: &[1, 2],
6517 },
6518 MaccsPatternGolden {
6519 bit: 158,
6520 smarts: "[#6]-[#7]",
6521 smiles: "CCN",
6522 first_match: &[1, 2],
6523 },
6524 MaccsPatternGolden {
6525 bit: 162,
6526 smarts: "a",
6527 smiles: "c1ccccc1",
6528 first_match: &[0],
6529 },
6530 MaccsPatternGolden {
6531 bit: 165,
6532 smarts: "[R]",
6533 smiles: "C1CC1",
6534 first_match: &[0],
6535 },
6536 ]
6537 }
6538
6539 #[test]
6540 fn maccs_patterns_match_rdkit_positive_truth_and_first_atom_maps() {
6541 let goldens = rdkit_maccs_pattern_positive_goldens();
6542 assert_eq!(goldens.len(), 136);
6543
6544 for golden in goldens {
6545 let mol =
6546 Molecule::from_smiles_with_sanitize(golden.smiles, true).unwrap_or_else(|error| {
6547 panic!("MACCS bit {} target SMILES failed: {error}", golden.bit)
6548 });
6549 let query = compile_query_fixture(golden.smarts)
6550 .unwrap_or_else(|error| panic!("MACCS bit {} SMARTS failed: {error}", golden.bit));
6551 let matches = get_substruct_matches(&mol, &query);
6552 assert!(
6553 !matches.is_empty(),
6554 "MACCS bit {} should match RDKit positive target {} with SMARTS {}",
6555 golden.bit,
6556 golden.smiles,
6557 golden.smarts
6558 );
6559 assert_eq!(
6560 matches[0].atom_mapping, golden.first_match,
6561 "MACCS bit {} first RDKit atom map for {}",
6562 golden.bit, golden.smiles
6563 );
6564 }
6565 }
6566
6567 #[test]
6568 fn maccs_bit_030_requires_four_explicit_neighbors_like_rdkit() {
6569 let mol = Molecule::from_smiles("ON(C)C").expect("fixture should parse");
6570 let query = compile_query_fixture("[#6]~[!#6!#1](~[#6])(~[#6])~*")
6571 .expect("MACCS bit 30 SMARTS should build");
6572
6573 assert_eq!(query.num_atoms(), 5);
6574 assert_eq!(query.num_bonds(), 4);
6575 assert!(
6576 !has_substruct_match(&mol, &query),
6577 "RDKit does not match MACCS bit 30 against ON(C)C through the max-matches=1 path"
6578 );
6579 assert!(
6580 get_substruct_matches(&mol, &query).is_empty(),
6581 "RDKit does not match MACCS bit 30 against ON(C)C"
6582 );
6583 }
6584
6585 #[test]
6586 fn test_atom_matches_basic() {
6587 let mut builder = MoleculeBuilder::new();
6588 let c0 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
6589 let c1 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
6590 builder
6591 .add_bond(crate::BondSpec::new(c0, c1, BondOrder::Single))
6592 .expect("add bond");
6593 let mol = builder.build().expect("build");
6594 let q_atom = &mol.atoms()[0];
6595 let m_atom = &mol.atoms()[1];
6596 assert!(
6597 atom_matches(q_atom, &mol, m_atom, &mol),
6598 "two carbons should match"
6599 );
6600 }
6601
6602 #[test]
6603 fn test_atom_matches_different_elements() {
6604 let mut builder = MoleculeBuilder::new();
6605 let c = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
6606 let o = builder.add_atom(crate::AtomSpec::new(crate::Element::O));
6607 builder
6608 .add_bond(crate::BondSpec::new(c, o, BondOrder::Single))
6609 .expect("add bond");
6610 let mol = builder.build().expect("build");
6611 // Query atom = C, target atom = O — should not match (C != O, and
6612 // atomic number check should reject since 6 != 8).
6613 // But our atom_matches uses query atomic number: query=6, mol=8.
6614 // If query atomic number != 0, it must match. So C does not match O.
6615 let c_atom = &mol.atoms()[0]; // C
6616 let o_atom = &mol.atoms()[1]; // O
6617 assert!(
6618 !atom_matches(c_atom, &mol, o_atom, &mol),
6619 "C should not match O via basic atomic number check"
6620 );
6621 }
6622
6623 #[test]
6624 fn atom_matches_reproduces_rdkit_plain_atom_defaults() {
6625 fn one_atom(spec: crate::AtomSpec) -> Molecule {
6626 let mut builder = MoleculeBuilder::new();
6627 builder.add_atom(spec);
6628 builder.build().expect("one-atom fixture")
6629 }
6630
6631 let dummy = one_atom(crate::AtomSpec::new(crate::Element::DUMMY));
6632 let isotope_one = one_atom(crate::AtomSpec::new(crate::Element::DUMMY).with_isotope(1));
6633 let isotope_two = one_atom(crate::AtomSpec::new(crate::Element::DUMMY).with_isotope(2));
6634 assert!(atom_matches(
6635 &dummy.atoms()[0],
6636 &dummy,
6637 &isotope_one.atoms()[0],
6638 &isotope_one,
6639 ));
6640 assert!(atom_matches(
6641 &isotope_one.atoms()[0],
6642 &isotope_one,
6643 &dummy.atoms()[0],
6644 &dummy,
6645 ));
6646 assert!(!atom_matches(
6647 &isotope_one.atoms()[0],
6648 &isotope_one,
6649 &isotope_two.atoms()[0],
6650 &isotope_two,
6651 ));
6652
6653 let neutral_carbon = one_atom(crate::AtomSpec::new(crate::Element::C));
6654 let charged_carbon =
6655 one_atom(crate::AtomSpec::new(crate::Element::C).with_formal_charge(1));
6656 assert!(atom_matches(
6657 &neutral_carbon.atoms()[0],
6658 &neutral_carbon,
6659 &charged_carbon.atoms()[0],
6660 &charged_carbon,
6661 ));
6662 assert!(!atom_matches(
6663 &charged_carbon.atoms()[0],
6664 &charged_carbon,
6665 &neutral_carbon.atoms()[0],
6666 &neutral_carbon,
6667 ));
6668
6669 let radical_carbon =
6670 one_atom(crate::AtomSpec::new(crate::Element::C).with_radical_electrons(1));
6671 assert!(!atom_matches(
6672 &radical_carbon.atoms()[0],
6673 &radical_carbon,
6674 &neutral_carbon.atoms()[0],
6675 &neutral_carbon,
6676 ));
6677 }
6678
6679 #[test]
6680 fn test_bond_matches_single() {
6681 let mut builder = MoleculeBuilder::new();
6682 let c0 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
6683 let c1 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
6684 builder
6685 .add_bond(crate::BondSpec::new(c0, c1, BondOrder::Single))
6686 .expect("add bond");
6687 let mol = builder.build().expect("build");
6688 assert!(bond_compat(
6689 &mol.bonds()[0],
6690 &mol,
6691 &mol.bonds()[0],
6692 &mol,
6693 &SubstructMatchParams::default(),
6694 &build_query_match_context(&mol),
6695 ));
6696 }
6697
6698 #[test]
6699 fn test_vf2_graph_building() {
6700 let cc = make_mol_cc();
6701 let g = build_vf2_graph(&cc);
6702 assert_eq!(g.n_atoms, 2);
6703 assert_eq!(g.n_bonds, 1);
6704 assert_eq!(g.out_degree(0), 1);
6705 assert_eq!(g.out_degree(1), 1);
6706 assert_eq!(g.out_edges(0)[0].0, 1);
6707 assert_eq!(g.out_edges(1)[0].0, 0);
6708 }
6709
6710 #[test]
6711 fn smarts_vf2_other_index() {
6712 let cc = make_mol_cc();
6713 let graph = build_vf2_graph(&cc);
6714 assert_eq!(get_other_idx(&graph, 0, 0), 1);
6715 assert_eq!(get_other_idx(&graph, 0, 1), 0);
6716
6717 // RDKit returns source(edge) when `vertex` is not the source; it does
6718 // not validate that `vertex` is an endpoint.
6719 assert_eq!(get_other_idx(&graph, 0, 99), 0);
6720 }
6721
6722 #[test]
6723 fn test_sort_nodes_by_frequency_small() {
6724 let cc = make_mol_cc();
6725 let g = build_vf2_graph(&cc);
6726 let order = sort_nodes_by_frequency(&g);
6727 // Both nodes have degree 1, so order depends on sort stability.
6728 assert_eq!(order.len(), 2);
6729 // Both should be present.
6730 assert!(order.contains(&0));
6731 assert!(order.contains(&1));
6732 }
6733
6734 #[test]
6735 fn smarts_vf2_node_order() {
6736 let mut builder = MoleculeBuilder::new();
6737 let center = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
6738 let leaf1 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
6739 let leaf2 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
6740 let leaf3 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
6741 let isolated = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
6742 for leaf in [leaf1, leaf2, leaf3] {
6743 builder
6744 .add_bond(crate::BondSpec::new(center, leaf, BondOrder::Single))
6745 .expect("add star bond");
6746 }
6747 let molecule = builder.build().expect("build star and isolated atom");
6748
6749 let order = sort_nodes_by_frequency(&build_vf2_graph(&molecule));
6750 assert_eq!(order[0], center.index());
6751 assert_eq!(order[4], isolated.index());
6752 let mut middle = order[1..4].to_vec();
6753 middle.sort_unstable();
6754 assert_eq!(middle, vec![leaf1.index(), leaf2.index(), leaf3.index()]);
6755 }
6756
6757 #[test]
6758 fn smarts_vf2_node_compare_degree() {
6759 let lower_out = NodeInfo {
6760 id: 0,
6761 in_deg: 9,
6762 out_deg: 1,
6763 };
6764 let higher_out = NodeInfo {
6765 id: 1,
6766 in_deg: 0,
6767 out_deg: 2,
6768 };
6769 assert_eq!(
6770 node_info_cmp1(&lower_out, &higher_out),
6771 std::cmp::Ordering::Less
6772 );
6773
6774 let lower_in = NodeInfo {
6775 id: 2,
6776 in_deg: 1,
6777 out_deg: 3,
6778 };
6779 let higher_in = NodeInfo {
6780 id: 3,
6781 in_deg: 2,
6782 out_deg: 3,
6783 };
6784 assert_eq!(
6785 node_info_cmp1(&lower_in, &higher_in),
6786 std::cmp::Ordering::Less
6787 );
6788 assert_eq!(
6789 node_info_cmp1(&lower_in, &NodeInfo { id: 4, ..lower_in }),
6790 std::cmp::Ordering::Equal
6791 );
6792 }
6793
6794 #[test]
6795 fn smarts_vf2_node_compare_frequency() {
6796 let isolated = NodeInfo {
6797 id: 0,
6798 in_deg: 0,
6799 out_deg: 0,
6800 };
6801 let connected = NodeInfo {
6802 id: 1,
6803 in_deg: 2,
6804 out_deg: 9,
6805 };
6806 assert_eq!(
6807 node_info_cmp2(&isolated, &connected),
6808 std::cmp::Ordering::Greater
6809 );
6810 assert_eq!(
6811 node_info_cmp2(&connected, &isolated),
6812 std::cmp::Ordering::Less
6813 );
6814
6815 let rarer = NodeInfo {
6816 id: 2,
6817 in_deg: 8,
6818 out_deg: 1,
6819 };
6820 let common = NodeInfo {
6821 id: 3,
6822 in_deg: 1,
6823 out_deg: 2,
6824 };
6825 assert_eq!(node_info_cmp2(&rarer, &common), std::cmp::Ordering::Less);
6826
6827 let lower_valence = NodeInfo {
6828 id: 4,
6829 in_deg: 2,
6830 out_deg: 3,
6831 };
6832 let higher_valence = NodeInfo {
6833 id: 5,
6834 in_deg: 4,
6835 out_deg: 3,
6836 };
6837 assert_eq!(
6838 node_info_cmp2(&lower_valence, &higher_valence),
6839 std::cmp::Ordering::Less
6840 );
6841 assert_eq!(
6842 node_info_cmp2(
6843 &lower_valence,
6844 &NodeInfo {
6845 id: 6,
6846 ..lower_valence
6847 }
6848 ),
6849 std::cmp::Ordering::Equal
6850 );
6851 }
6852
6853 #[test]
6854 fn test_vf2_state_initial() {
6855 let cc = make_mol_cc();
6856 let g = build_vf2_graph(&cc);
6857 let state = Vf2SubState::new(&g, &g, true);
6858 assert!(!state.is_goal());
6859 assert!(!state.is_dead());
6860 assert_eq!(state.core_len, 0);
6861 }
6862
6863 #[test]
6864 fn smarts_vf2_state_new() {
6865 let query = build_vf2_graph(&make_mol_cc());
6866 let target_molecule = Molecule::from_smiles("CCC").expect("parse target");
6867 let target = build_vf2_graph(&target_molecule);
6868
6869 let unsorted = Vf2SubState::new(&query, &target, false);
6870 assert_eq!((unsorted.n1, unsorted.n2), (2, 3));
6871 assert_eq!(
6872 (unsorted.core_len, unsorted.t1_len, unsorted.t2_len),
6873 (0, 0, 0)
6874 );
6875 assert_eq!(unsorted.core_1, vec![NULL_NODE; 2]);
6876 assert_eq!(unsorted.core_2, vec![NULL_NODE; 3]);
6877 assert_eq!(unsorted.term_1, vec![0; 2]);
6878 assert_eq!(unsorted.term_2, vec![0; 3]);
6879 assert!(unsorted.debug_order().is_none());
6880
6881 let sorted = Vf2SubState::new(&query, &target, true);
6882 assert_eq!(
6883 sorted.debug_order(),
6884 Some(sort_nodes_by_frequency(&query).as_slice())
6885 );
6886 }
6887
6888 #[test]
6889 fn smarts_vf2_state_clone() {
6890 let molecule = make_mol_cc();
6891 let graph = build_vf2_graph(&molecule);
6892 let mut original = Vf2SubState::new(&graph, &graph, true);
6893 original.add_pair(0, 0);
6894
6895 let mut cloned = original.clone_state();
6896 assert_eq!(cloned.core_len, original.core_len);
6897 assert_eq!(cloned.core_1, original.core_1);
6898 assert_eq!(cloned.core_2, original.core_2);
6899 assert_eq!(cloned.term_1, original.term_1);
6900 assert_eq!(cloned.term_2, original.term_2);
6901 assert_eq!(cloned.order, original.order);
6902
6903 cloned.add_pair(1, 1);
6904 assert_eq!(cloned.core_len, 2);
6905 assert_eq!(original.core_len, 1);
6906 assert_eq!(original.core_1[1], NULL_NODE);
6907 }
6908
6909 #[test]
6910 fn smarts_vf2_goal() {
6911 let molecule = make_mol_cc();
6912 let graph = build_vf2_graph(&molecule);
6913 let mut state = Vf2SubState::new(&graph, &graph, false);
6914 assert!(!state.is_goal());
6915 state.add_pair(0, 0);
6916 assert!(!state.is_goal());
6917 state.add_pair(1, 1);
6918 assert!(state.is_goal());
6919 }
6920
6921 #[test]
6922 fn smarts_vf2_match_checks() {
6923 let molecule = make_mol_cc();
6924 let graph = build_vf2_graph(&molecule);
6925 let state = Vf2SubState::new(&graph, &graph, false);
6926 let mut seen = None;
6927 let mut check = |c1: &[NodeId], c2: &[NodeId]| {
6928 seen = Some((c1.to_vec(), c2.to_vec()));
6929 c1 == [0, 1] && c2 == [1, 0]
6930 };
6931 assert!(state.match_checks(&[0, 1], &[1, 0], &mut check));
6932 assert_eq!(seen, Some((vec![0, 1], vec![1, 0])));
6933 }
6934
6935 #[test]
6936 fn smarts_vf2_dead() {
6937 let query_molecule = Molecule::from_smiles("CCC").expect("parse query");
6938 let target_molecule = make_mol_cc();
6939 let query = build_vf2_graph(&query_molecule);
6940 let target = build_vf2_graph(&target_molecule);
6941 assert!(Vf2SubState::new(&query, &target, false).is_dead());
6942
6943 let mut terminal_dead = Vf2SubState::new(&target, &query, false);
6944 terminal_dead.t1_len = 2;
6945 terminal_dead.t2_len = 1;
6946 assert!(terminal_dead.is_dead());
6947 terminal_dead.t2_len = 2;
6948 assert!(!terminal_dead.is_dead());
6949 }
6950
6951 #[test]
6952 fn smarts_vf2_core_len() {
6953 let molecule = make_mol_cc();
6954 let graph = build_vf2_graph(&molecule);
6955 let mut state = Vf2SubState::new(&graph, &graph, false);
6956 assert_eq!(state.core_len(), 0);
6957 state.add_pair(0, 0);
6958 assert_eq!(state.core_len(), 1);
6959 state.add_pair(1, 1);
6960 assert_eq!(state.core_len(), 2);
6961 }
6962
6963 #[test]
6964 fn test_vf2_next_pair_initial() {
6965 let cc = make_mol_cc();
6966 let g = build_vf2_graph(&cc);
6967 let state = Vf2SubState::new(&g, &g, true);
6968 let mut pair = Vf2Pair::new();
6969 let has_next = state.next_pair(&mut pair);
6970 assert!(has_next, "should find a next pair");
6971 }
6972
6973 #[test]
6974 fn smarts_vf2_next_pair() {
6975 let mut builder = MoleculeBuilder::new();
6976 let center = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
6977 let leaf1 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
6978 let leaf2 = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
6979 for leaf in [leaf1, leaf2] {
6980 builder
6981 .add_bond(crate::BondSpec::new(center, leaf, BondOrder::Single))
6982 .expect("add star bond");
6983 }
6984 let molecule = builder.build().expect("build star");
6985 let graph = build_vf2_graph(&molecule);
6986
6987 let state = Vf2SubState::new(&graph, &graph, true);
6988 let mut pair = Vf2Pair::new();
6989 let mut initial_pairs = Vec::new();
6990 while state.next_pair(&mut pair) {
6991 initial_pairs.push((pair.n1, pair.n2));
6992 }
6993 assert_eq!(
6994 initial_pairs,
6995 vec![
6996 (center.index(), 0),
6997 (center.index(), 1),
6998 (center.index(), 2)
6999 ]
7000 );
7001
7002 let mut terminal_state = Vf2SubState::new(&graph, &graph, true);
7003 terminal_state.add_pair(center.index(), center.index());
7004 let mut terminal_pair = Vf2Pair::new();
7005 let mut target_neighbors = Vec::new();
7006 while terminal_state.next_pair(&mut terminal_pair) {
7007 assert!(terminal_pair.n1 == leaf1.index() || terminal_pair.n1 == leaf2.index());
7008 target_neighbors.push(terminal_pair.n2);
7009 }
7010 target_neighbors.sort_unstable();
7011 assert_eq!(target_neighbors, vec![leaf1.index(), leaf2.index()]);
7012 }
7013
7014 #[test]
7015 fn smarts_vf2_feasible_pair() {
7016 let query_molecule = make_mol_cc();
7017 let query = build_vf2_graph(&query_molecule);
7018
7019 let single_atom = Molecule::from_smiles("C").expect("parse single atom");
7020 let single = build_vf2_graph(&single_atom);
7021 let state = Vf2SubState::new(&query, &single, false);
7022 assert!(!state.is_feasible_pair(0, 0, &|_, _| true, &|_, _| true));
7023
7024 let target_molecule = Molecule::from_smiles("CC").expect("parse target");
7025 let target = build_vf2_graph(&target_molecule);
7026 let state = Vf2SubState::new(&query, &target, false);
7027 assert!(!state.is_feasible_pair(0, 0, &|_, _| false, &|_, _| true));
7028 assert!(state.is_feasible_pair(0, 0, &|_, _| true, &|_, _| true));
7029
7030 let mut mapped = Vf2SubState::new(&query, &target, false);
7031 mapped.add_pair(0, 0);
7032 assert!(!mapped.is_feasible_pair(1, 1, &|_, _| true, &|_, _| false));
7033 assert!(mapped.is_feasible_pair(1, 1, &|_, _| true, &|_, _| true));
7034
7035 let disconnected_molecule = Molecule::from_smiles("CC.CC").expect("parse fragments");
7036 let disconnected = build_vf2_graph(&disconnected_molecule);
7037 let mut missing_edge = Vf2SubState::new(&query, &disconnected, false);
7038 missing_edge.add_pair(0, 0);
7039 assert!(!missing_edge.is_feasible_pair(1, 2, &|_, _| true, &|_, _| true));
7040 }
7041
7042 #[test]
7043 fn smarts_vf2_add_pair() {
7044 let molecule = Molecule::from_smiles("CCC").expect("parse chain");
7045 let graph = build_vf2_graph(&molecule);
7046 let mut state = Vf2SubState::new(&graph, &graph, false);
7047
7048 state.add_pair(1, 1);
7049 assert_eq!(state.core_len, 1);
7050 assert_eq!(state.core_1, vec![NULL_NODE, 1, NULL_NODE]);
7051 assert_eq!(state.core_2, vec![NULL_NODE, 1, NULL_NODE]);
7052 assert_eq!(state.term_1, vec![1, 1, 1]);
7053 assert_eq!(state.term_2, vec![1, 1, 1]);
7054 assert_eq!((state.t1_len, state.t2_len), (3, 3));
7055
7056 state.add_pair(0, 0);
7057 assert_eq!(state.core_len, 2);
7058 assert_eq!(state.core_1, vec![0, 1, NULL_NODE]);
7059 assert_eq!(state.core_2, vec![0, 1, NULL_NODE]);
7060 assert_eq!(state.term_1, vec![1, 1, 1]);
7061 assert_eq!(state.term_2, vec![1, 1, 1]);
7062 assert_eq!((state.t1_len, state.t2_len), (3, 3));
7063 }
7064
7065 #[test]
7066 fn smarts_vf2_core_set() {
7067 let molecule = Molecule::from_smiles("CCC").expect("parse chain");
7068 let graph = build_vf2_graph(&molecule);
7069 let mut state = Vf2SubState::new(&graph, &graph, false);
7070 state.add_pair(2, 0);
7071 state.add_pair(0, 2);
7072
7073 let (c1, c2) = state.get_core_set();
7074 assert_eq!(c1, vec![0, 2]);
7075 assert_eq!(c2, vec![2, 0]);
7076 }
7077
7078 #[test]
7079 fn smarts_vf2_clone() {
7080 let molecule = Molecule::from_smiles("CCC").expect("parse chain");
7081 let graph = build_vf2_graph(&molecule);
7082 let mut state = Vf2SubState::new(&graph, &graph, true);
7083 state.add_pair(1, 1);
7084 let mut cloned = state.clone();
7085
7086 cloned.back_track(1, 1);
7087 assert_eq!(cloned.core_len, 0);
7088 assert_eq!(state.core_len, 1);
7089 assert_eq!(state.core_1[1], 1);
7090 assert_eq!(state.core_2[1], 1);
7091 }
7092
7093 #[test]
7094 fn smarts_vf2_backtrack() {
7095 let molecule = Molecule::from_smiles("CCC").expect("parse chain");
7096 let graph = build_vf2_graph(&molecule);
7097 let mut state = Vf2SubState::new(&graph, &graph, false);
7098 state.add_pair(1, 1);
7099 state.add_pair(0, 0);
7100 assert_eq!(state.core_len, 2);
7101
7102 state.back_track(0, 0);
7103 assert_eq!(state.core_len, 1);
7104 assert_eq!(state.core_1, vec![NULL_NODE, 1, NULL_NODE]);
7105 assert_eq!(state.core_2, vec![NULL_NODE, 1, NULL_NODE]);
7106 assert_eq!(state.term_1, vec![1, 1, 1]);
7107 assert_eq!(state.term_2, vec![1, 1, 1]);
7108 assert_eq!((state.t1_len, state.t2_len), (3, 3));
7109
7110 state.back_track(1, 1);
7111 assert_eq!(state.core_len, 0);
7112 assert_eq!(state.term_1, vec![0, 0, 0]);
7113 assert_eq!(state.term_2, vec![0, 0, 0]);
7114 assert_eq!((state.t1_len, state.t2_len), (0, 0));
7115 }
7116
7117 #[test]
7118 fn smarts_vf2_match_one() {
7119 let query_molecule = make_mol_cc();
7120 let target_molecule = Molecule::from_smiles("CCC").expect("parse target");
7121 let query = build_vf2_graph(&query_molecule);
7122 let target = build_vf2_graph(&target_molecule);
7123 let mut state = Vf2SubState::new(&query, &target, true);
7124 let mut checks = 0;
7125 let mut accept_second = |_: &[NodeId], _: &[NodeId]| {
7126 checks += 1;
7127 checks == 2
7128 };
7129 let (c1, c2) = state
7130 .match_one(&|_, _| true, &|_, _| true, Some(&mut accept_second))
7131 .expect("second complete mapping accepted");
7132 assert_eq!(c1, vec![0, 1]);
7133 assert_eq!(c2.len(), 2);
7134 assert_eq!(checks, 2);
7135
7136 let mut dead = Vf2SubState::new(&target, &query, true);
7137 let mut accept_all = |_: &[NodeId], _: &[NodeId]| true;
7138 assert!(
7139 dead.match_one(&|_, _| true, &|_, _| true, Some(&mut accept_all))
7140 .is_none()
7141 );
7142 }
7143
7144 #[test]
7145 fn smarts_vf2_match_all() {
7146 let query_molecule = make_mol_cc();
7147 let target_molecule = Molecule::from_smiles("CCC").expect("parse target");
7148 let query = build_vf2_graph(&query_molecule);
7149 let target = build_vf2_graph(&target_molecule);
7150
7151 let mut state = Vf2SubState::new(&query, &target, true);
7152 let mut results = Vec::new();
7153 let mut accept_all = |_: &[NodeId], _: &[NodeId]| true;
7154 assert!(!state.match_all(
7155 &|_, _| true,
7156 &|_, _| true,
7157 Some(&mut accept_all),
7158 &mut results,
7159 0,
7160 ));
7161 assert_eq!(results.len(), 4);
7162
7163 let mut limited_state = Vf2SubState::new(&query, &target, true);
7164 let mut limited = Vec::new();
7165 let mut accept_all = |_: &[NodeId], _: &[NodeId]| true;
7166 assert!(limited_state.match_all(
7167 &|_, _| true,
7168 &|_, _| true,
7169 Some(&mut accept_all),
7170 &mut limited,
7171 2,
7172 ));
7173 assert_eq!(limited.len(), 2);
7174 }
7175
7176 #[test]
7177 fn smarts_vf2_free_match_one() {
7178 let query_molecule = make_mol_cc();
7179 let target_molecule = Molecule::from_smiles("CCC").expect("parse target");
7180 let query = build_vf2_graph(&query_molecule);
7181 let target = build_vf2_graph(&target_molecule);
7182 let mut state = Vf2SubState::new(&query, &target, false);
7183 let mut accept_all = |_: &[NodeId], _: &[NodeId]| true;
7184 let (c1, c2) = vf2_match(
7185 &mut state,
7186 &|_, _| true,
7187 &|_, _| true,
7188 Some(&mut accept_all),
7189 )
7190 .expect("free match finds mapping");
7191 assert_eq!(c1.len(), state.core_len());
7192 assert_eq!(c1, vec![0, 1]);
7193 assert_eq!(c2.len(), 2);
7194
7195 let mut dead = Vf2SubState::new(&target, &query, false);
7196 let mut accept_all = |_: &[NodeId], _: &[NodeId]| true;
7197 assert!(vf2_match(&mut dead, &|_, _| true, &|_, _| true, Some(&mut accept_all),).is_none());
7198 }
7199
7200 #[test]
7201 fn smarts_vf2_free_match_all() {
7202 let query_molecule = make_mol_cc();
7203 let target_molecule = Molecule::from_smiles("CCC").expect("parse target");
7204 let query = build_vf2_graph(&query_molecule);
7205 let target = build_vf2_graph(&target_molecule);
7206 let mut state = Vf2SubState::new(&query, &target, false);
7207 let mut results = Vec::new();
7208 let mut accept_all = |_: &[NodeId], _: &[NodeId]| true;
7209 assert!(vf2_match_all(
7210 &mut state,
7211 &|_, _| true,
7212 &|_, _| true,
7213 Some(&mut accept_all),
7214 &mut results,
7215 2,
7216 ));
7217 assert_eq!(results.len(), 2);
7218
7219 let mut dead = Vf2SubState::new(&target, &query, false);
7220 let mut no_results = Vec::new();
7221 let mut accept_all = |_: &[NodeId], _: &[NodeId]| true;
7222 assert!(!vf2_match_all(
7223 &mut dead,
7224 &|_, _| true,
7225 &|_, _| true,
7226 Some(&mut accept_all),
7227 &mut no_results,
7228 2,
7229 ));
7230 assert!(no_results.is_empty());
7231 }
7232
7233 #[test]
7234 fn smarts_vf2_entry_one() {
7235 let query_molecule = make_mol_cc();
7236 let target_molecule = Molecule::from_smiles("CCC").expect("parse target");
7237 let query = build_vf2_graph(&query_molecule);
7238 let target = build_vf2_graph(&target_molecule);
7239 let mut result = vec![(99, 99)];
7240 let mut accept_all = |_: &[NodeId], _: &[NodeId]| true;
7241 assert!(vf2_entry_one(
7242 &query,
7243 &target,
7244 &|_, _| true,
7245 &|_, _| true,
7246 Some(&mut accept_all),
7247 &mut result,
7248 ));
7249 assert_eq!(result.len(), 2);
7250 assert!(!result.contains(&(99, 99)));
7251
7252 let mut no_result = vec![(99, 99)];
7253 let mut accept_all = |_: &[NodeId], _: &[NodeId]| true;
7254 assert!(!vf2_entry_one(
7255 &target,
7256 &query,
7257 &|_, _| true,
7258 &|_, _| true,
7259 Some(&mut accept_all),
7260 &mut no_result,
7261 ));
7262 assert!(no_result.is_empty());
7263 }
7264
7265 #[test]
7266 fn smarts_vf2_entry_all() {
7267 let query_molecule = make_mol_cc();
7268 let target_molecule = Molecule::from_smiles("CCC").expect("parse target");
7269 let query = build_vf2_graph(&query_molecule);
7270 let target = build_vf2_graph(&target_molecule);
7271 let mut results = vec![(vec![99], vec![99])];
7272 let mut accept_all = |_: &[NodeId], _: &[NodeId]| true;
7273 assert!(vf2_entry_all(
7274 &query,
7275 &target,
7276 &|_, _| true,
7277 &|_, _| true,
7278 Some(&mut accept_all),
7279 &mut results,
7280 3,
7281 ));
7282 assert_eq!(results.len(), 3);
7283 assert!(!results.iter().any(|mapping| mapping.0 == [99]));
7284
7285 let mut no_results = vec![(vec![99], vec![99])];
7286 let mut accept_all = |_: &[NodeId], _: &[NodeId]| true;
7287 assert!(!vf2_entry_all(
7288 &target,
7289 &query,
7290 &|_, _| true,
7291 &|_, _| true,
7292 Some(&mut accept_all),
7293 &mut no_results,
7294 3,
7295 ));
7296 assert!(no_results.is_empty());
7297 }
7298
7299 #[test]
7300 fn smarts_match_chiral_label() {
7301 let atom = |tag| {
7302 let mut builder = MoleculeBuilder::new();
7303 builder.add_atom(crate::AtomSpec::new(crate::Element::C).with_chiral_tag(tag));
7304 let molecule = builder.build().expect("build one atom");
7305 molecule.atoms()[0].clone()
7306 };
7307 assert!(has_chiral_label(&atom(ChiralTag::TetrahedralCw)));
7308 assert!(has_chiral_label(&atom(ChiralTag::TetrahedralCcw)));
7309 for tag in [
7310 ChiralTag::Unspecified,
7311 ChiralTag::Other,
7312 ChiralTag::Tetrahedral,
7313 ChiralTag::Allene,
7314 ChiralTag::SquarePlanar,
7315 ChiralTag::TrigonalBipyramidal,
7316 ChiralTag::Octahedral,
7317 ] {
7318 assert!(
7319 !has_chiral_label(&atom(tag)),
7320 "unexpected label for {tag:?}"
7321 );
7322 }
7323 }
7324
7325 #[test]
7326 fn smarts_match_enhanced_stereo() {
7327 fn grouped(groups: &[(StereoGroupKind, &[usize])]) -> Molecule {
7328 let mut builder = MoleculeBuilder::new();
7329 let atoms: Vec<_> = (0..3)
7330 .map(|_| builder.add_atom(crate::AtomSpec::new(crate::Element::C)))
7331 .collect();
7332 for (kind, members) in groups {
7333 builder
7334 .add_stereo_group(crate::StereoGroup::new(
7335 *kind,
7336 members.iter().map(|&idx| atoms[idx]).collect(),
7337 Vec::new(),
7338 ))
7339 .expect("add stereo group");
7340 }
7341 builder.build().expect("build grouped molecule")
7342 }
7343
7344 let query_or = grouped(&[(StereoGroupKind::Or, &[0, 1])]);
7345 let mol_or = grouped(&[(StereoGroupKind::Or, &[0, 1])]);
7346 let mol_and = grouped(&[(StereoGroupKind::And, &[0, 1])]);
7347 let identity = [0, 1, 2];
7348 let or_membership = [Some(0), Some(0), None];
7349 let same = [Some(true), Some(true), None];
7350 assert!(enhanced_stereo_is_ok(
7351 &mol_or,
7352 &query_or,
7353 &identity,
7354 &or_membership,
7355 &same,
7356 ));
7357 assert!(enhanced_stereo_is_ok(
7358 &mol_and,
7359 &query_or,
7360 &identity,
7361 &or_membership,
7362 &same,
7363 ));
7364
7365 let query_and = grouped(&[(StereoGroupKind::And, &[0, 1])]);
7366 assert!(!enhanced_stereo_is_ok(
7367 &mol_or,
7368 &query_and,
7369 &identity,
7370 &or_membership,
7371 &same,
7372 ));
7373 assert!(enhanced_stereo_is_ok(
7374 &mol_and,
7375 &query_and,
7376 &identity,
7377 &or_membership,
7378 &same,
7379 ));
7380
7381 let absolute = grouped(&[]);
7382 assert!(!enhanced_stereo_is_ok(
7383 &absolute,
7384 &query_or,
7385 &identity,
7386 &[None, None, None],
7387 &[None, None, None],
7388 ));
7389 assert!(!enhanced_stereo_is_ok(
7390 &mol_or,
7391 &query_or,
7392 &identity,
7393 &or_membership,
7394 &[Some(true), Some(false), None],
7395 ));
7396
7397 let split_query = grouped(&[(StereoGroupKind::Or, &[0]), (StereoGroupKind::Or, &[1])]);
7398 assert!(!enhanced_stereo_is_ok(
7399 &mol_or,
7400 &split_query,
7401 &identity,
7402 &or_membership,
7403 &same,
7404 ));
7405 }
7406
7407 #[test]
7408 fn smarts_match_insert_unique() {
7409 let mut matches = BTreeSet::new();
7410 let first = vec![(0, 2), (1, 1)];
7411 assert!(insert_if_needed(&mut matches, first.clone()));
7412 assert!(!insert_if_needed(&mut matches, first.clone()));
7413
7414 let lexicographically_smaller = vec![(0, 1), (1, 2)];
7415 assert!(insert_if_needed(
7416 &mut matches,
7417 lexicographically_smaller.clone()
7418 ));
7419 assert_eq!(matches, BTreeSet::from([lexicographically_smaller]));
7420
7421 let distinct_atom_set = vec![(0, 3), (1, 2)];
7422 assert!(insert_if_needed(&mut matches, distinct_atom_set.clone()));
7423 assert!(matches.contains(&distinct_atom_set));
7424 assert_eq!(matches.len(), 2);
7425 }
7426
7427 #[test]
7428 fn smarts_match_try_insert() {
7429 let first = vec![(0, 2), (1, 1)];
7430 let duplicate_atom_set = vec![(0, 1), (1, 2)];
7431
7432 let mut params = SubstructMatchParams {
7433 max_matches: 1,
7434 uniquify: false,
7435 ..SubstructMatchParams::default()
7436 };
7437 let mut matches = BTreeSet::new();
7438 assert!(try_to_insert(&mut matches, first.clone(), ¶ms));
7439 assert!(!try_to_insert(
7440 &mut matches,
7441 duplicate_atom_set.clone(),
7442 ¶ms
7443 ));
7444 assert_eq!(matches, BTreeSet::from([first.clone()]));
7445
7446 params.max_matches = 10;
7447 params.uniquify = true;
7448 assert!(try_to_insert(
7449 &mut matches,
7450 duplicate_atom_set.clone(),
7451 ¶ms
7452 ));
7453 assert_eq!(matches, BTreeSet::from([duplicate_atom_set]));
7454
7455 params.uniquify = false;
7456 assert!(try_to_insert(&mut matches, first.clone(), ¶ms));
7457 assert!(try_to_insert(&mut matches, first, ¶ms));
7458 assert_eq!(matches.len(), 2);
7459 }
7460
7461 #[test]
7462 fn smarts_match_final_check_setup() {
7463 let mut builder = MoleculeBuilder::new();
7464 let atoms: Vec<_> = (0..4)
7465 .map(|_| builder.add_atom(crate::AtomSpec::new(crate::Element::C)))
7466 .collect();
7467 builder
7468 .add_stereo_group(crate::StereoGroup::new(
7469 StereoGroupKind::Absolute,
7470 vec![atoms[0]],
7471 Vec::new(),
7472 ))
7473 .expect("add absolute group");
7474 builder
7475 .add_stereo_group(crate::StereoGroup::new(
7476 StereoGroupKind::Or,
7477 vec![atoms[1], atoms[2]],
7478 Vec::new(),
7479 ))
7480 .expect("add OR group");
7481 builder
7482 .add_stereo_group(crate::StereoGroup::new(
7483 StereoGroupKind::And,
7484 vec![atoms[3]],
7485 Vec::new(),
7486 ))
7487 .expect("add AND group");
7488 let molecule = builder.build().expect("build grouped molecule");
7489
7490 let disabled =
7491 MolMatchFinalCheckSetup::new(&molecule, &molecule, &SubstructMatchParams::default());
7492 assert_eq!(disabled.mol_stereo_groups, vec![None; 4]);
7493
7494 let enabled = MolMatchFinalCheckSetup::new(
7495 &molecule,
7496 &molecule,
7497 &SubstructMatchParams {
7498 use_enhanced_stereo: true,
7499 ..SubstructMatchParams::default()
7500 },
7501 );
7502 assert_eq!(
7503 enabled.mol_stereo_groups,
7504 vec![None, Some(1), Some(1), Some(2)]
7505 );
7506 }
7507
7508 #[test]
7509 fn smarts_match_final_check() {
7510 let query = make_mol_cc();
7511 let molecule = Molecule::from_smiles("CCC").expect("parse target");
7512 let mapping = ([0, 1], [0, 1]);
7513
7514 let mut params = SubstructMatchParams::default();
7515 let setup = MolMatchFinalCheckSetup::new(&query, &molecule, ¶ms);
7516 let mut seen = Vec::new();
7517 assert!(
7518 rdkit_match_final_check(
7519 &molecule, &query, ¶ms, &mapping.0, &mapping.1, &setup, &mut seen,
7520 )
7521 .expect("final check")
7522 );
7523 assert!(
7524 !rdkit_match_final_check(
7525 &molecule, &query, ¶ms, &mapping.0, &mapping.1, &setup, &mut seen,
7526 )
7527 .expect("duplicate final check")
7528 );
7529
7530 params.uniquify = false;
7531 params.extra_final_check = Some(Arc::new(|_, atom_ids| atom_ids == [0, 1]));
7532 let setup = MolMatchFinalCheckSetup::new(&query, &molecule, ¶ms);
7533 assert!(
7534 rdkit_match_final_check(
7535 &molecule,
7536 &query,
7537 ¶ms,
7538 &mapping.0,
7539 &mapping.1,
7540 &setup,
7541 &mut Vec::new(),
7542 )
7543 .expect("accepted callback")
7544 );
7545 params.extra_final_check = Some(Arc::new(|_, _| false));
7546 assert!(
7547 !rdkit_match_final_check(
7548 &molecule,
7549 &query,
7550 ¶ms,
7551 &mapping.0,
7552 &mapping.1,
7553 &setup,
7554 &mut Vec::new(),
7555 )
7556 .expect("rejected callback")
7557 );
7558
7559 params.extra_final_check = None;
7560 params.use_generic_matchers = true;
7561 assert!(
7562 rdkit_match_final_check(
7563 &molecule,
7564 &query,
7565 ¶ms,
7566 &mapping.0,
7567 &mapping.1,
7568 &setup,
7569 &mut Vec::new(),
7570 )
7571 .expect("generic matcher without labels accepts the match")
7572 );
7573 }
7574
7575 #[test]
7576 fn smarts_match_atom_label() {
7577 fn one_atom(element: crate::Element, tag: ChiralTag) -> Molecule {
7578 let mut builder = MoleculeBuilder::new();
7579 builder.add_atom(crate::AtomSpec::new(element).with_chiral_tag(tag));
7580 builder.build().expect("build one atom")
7581 }
7582
7583 let query = one_atom(crate::Element::C, ChiralTag::TetrahedralCw);
7584 let unspecified = one_atom(crate::Element::C, ChiralTag::Unspecified);
7585 let specified = one_atom(crate::Element::C, ChiralTag::TetrahedralCcw);
7586 let oxygen = one_atom(crate::Element::O, ChiralTag::TetrahedralCcw);
7587 let context = build_query_match_context(&unspecified);
7588 let mut params = SubstructMatchParams {
7589 use_chirality: true,
7590 ..SubstructMatchParams::default()
7591 };
7592 assert!(!atom_label_matches(
7593 &query,
7594 &unspecified,
7595 0,
7596 0,
7597 ¶ms,
7598 None,
7599 &context,
7600 ));
7601 params.specified_stereo_query_matches_unspecified = true;
7602 assert!(atom_label_matches(
7603 &query,
7604 &unspecified,
7605 0,
7606 0,
7607 ¶ms,
7608 None,
7609 &context,
7610 ));
7611 let context = build_query_match_context(&specified);
7612 assert!(atom_label_matches(
7613 &query, &specified, 0, 0, ¶ms, None, &context,
7614 ));
7615 let context = build_query_match_context(&oxygen);
7616 assert!(!atom_label_matches(
7617 &query, &oxygen, 0, 0, ¶ms, None, &context,
7618 ));
7619 }
7620
7621 #[test]
7622 fn smarts_match_bond_label() {
7623 fn double_bond(stereo: BondStereo) -> Molecule {
7624 let mut builder = MoleculeBuilder::new();
7625 let begin = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
7626 let end = builder.add_atom(crate::AtomSpec::new(crate::Element::C));
7627 builder
7628 .add_bond(crate::BondSpec::new(begin, end, BondOrder::Double).with_stereo(stereo))
7629 .expect("add double bond");
7630 builder.build().expect("build double bond")
7631 }
7632
7633 let query = double_bond(BondStereo::E);
7634 let unspecified = double_bond(BondStereo::None);
7635 let specified = double_bond(BondStereo::Z);
7636 let mut params = SubstructMatchParams {
7637 use_chirality: true,
7638 ..SubstructMatchParams::default()
7639 };
7640 assert!(!bond_label_matches(
7641 &query,
7642 &unspecified,
7643 0,
7644 0,
7645 ¶ms,
7646 &build_query_match_context(&unspecified),
7647 ));
7648 params.specified_stereo_query_matches_unspecified = true;
7649 assert!(bond_label_matches(
7650 &query,
7651 &unspecified,
7652 0,
7653 0,
7654 ¶ms,
7655 &build_query_match_context(&unspecified),
7656 ));
7657 assert!(bond_label_matches(
7658 &query,
7659 &specified,
7660 0,
7661 0,
7662 ¶ms,
7663 &build_query_match_context(&specified),
7664 ));
7665
7666 let single = Molecule::from_smiles("CC").expect("parse single bond");
7667 assert!(!bond_label_matches(
7668 &query,
7669 &single,
7670 0,
7671 0,
7672 ¶ms,
7673 &build_query_match_context(&single),
7674 ));
7675 }
7676}