1use chematic_smarts::{QueryMolecule, find_matches, parse_smarts};
4use std::collections::HashSet;
5
6use crate::reaction::Reaction;
7
8#[derive(Clone, Debug)]
10pub struct ReactionQuery {
11 pub reactant_patterns: Vec<QueryMolecule>,
13 pub product_patterns: Vec<QueryMolecule>,
15}
16
17#[derive(Clone, Debug)]
19pub struct ReactionSmartsPattern {
20 pub reactant_patterns: Vec<QueryMolecule>,
22 pub agent_patterns: Vec<QueryMolecule>,
24 pub product_patterns: Vec<QueryMolecule>,
26 pub map_number_info: MapNumberInfo,
28}
29
30#[derive(Clone, Debug)]
32pub struct MapNumberInfo {
33 pub all_map_numbers: HashSet<u16>,
35 pub reactant_maps: HashSet<u16>,
37 pub agent_maps: HashSet<u16>,
39 pub product_maps: HashSet<u16>,
41}
42
43#[derive(Clone, Debug)]
45pub struct MoleculeMatch {
46 pub molecule_index: usize,
48 pub pattern_index: usize,
50 pub atom_indices: Vec<usize>,
52}
53
54#[derive(Clone, Debug)]
56pub struct ReactantMatches {
57 pub pattern_matches: Vec<Vec<MoleculeMatch>>,
59}
60
61impl ReactantMatches {
62 pub fn get_pattern_matches(&self, pattern_index: usize) -> Option<&[MoleculeMatch]> {
64 self.pattern_matches
65 .get(pattern_index)
66 .map(|v| v.as_slice())
67 }
68
69 pub fn pattern_matched(&self, pattern_index: usize) -> bool {
71 self.pattern_matches
72 .get(pattern_index)
73 .is_some_and(|matches| !matches.is_empty())
74 }
75}
76
77#[derive(Clone, Debug)]
79pub struct ProductMatches {
80 pub pattern_matches: Vec<Vec<MoleculeMatch>>,
82}
83
84impl ProductMatches {
85 pub fn get_pattern_matches(&self, pattern_index: usize) -> Option<&[MoleculeMatch]> {
87 self.pattern_matches
88 .get(pattern_index)
89 .map(|v| v.as_slice())
90 }
91
92 pub fn pattern_matched(&self, pattern_index: usize) -> bool {
94 self.pattern_matches
95 .get(pattern_index)
96 .is_some_and(|matches| !matches.is_empty())
97 }
98}
99
100#[derive(Clone, Debug)]
102pub struct ReactionSmartsMatch {
103 pub reactant_matches: ReactantMatches,
105 pub product_matches: ProductMatches,
107 pub is_complete_match: bool,
109}
110
111impl ReactionSmartsMatch {
112 pub fn all_reactants_matched(&self) -> bool {
114 self.reactant_matches
115 .pattern_matches
116 .iter()
117 .all(|m| !m.is_empty())
118 }
119
120 pub fn all_products_matched(&self) -> bool {
122 self.product_matches
123 .pattern_matches
124 .iter()
125 .all(|m| !m.is_empty())
126 }
127}
128
129impl MapNumberInfo {
130 pub fn validate(&self) -> Result<(), String> {
132 let missing_in_products: Vec<u16> = self
134 .reactant_maps
135 .iter()
136 .filter(|&m| !self.product_maps.contains(m))
137 .copied()
138 .collect();
139
140 if !missing_in_products.is_empty() {
141 return Err(format!(
142 "map numbers in reactants missing from products: {:?}",
143 missing_in_products
144 ));
145 }
146
147 let undefined_in_reactants: Vec<u16> = self
149 .product_maps
150 .iter()
151 .filter(|&m| !self.reactant_maps.contains(m))
152 .copied()
153 .collect();
154
155 if !undefined_in_reactants.is_empty() {
156 return Err(format!(
157 "map numbers in products not found in reactants: {:?}",
158 undefined_in_reactants
159 ));
160 }
161
162 Ok(())
163 }
164}
165
166#[derive(Debug)]
168pub enum ReactionQueryError {
169 SmartsParseError { smarts: String, source: String },
171 MissingArrowDelimiter,
173 InvalidAgentsSection,
175 MapNumberMismatch { map_num: u16, message: String },
177}
178
179impl core::fmt::Display for ReactionQueryError {
180 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
181 match self {
182 Self::SmartsParseError { smarts, source } => {
183 write!(f, "failed to parse SMARTS '{smarts}': {source}")
184 }
185 Self::MissingArrowDelimiter => {
186 write!(f, "reaction SMARTS must contain '>' or '>>' delimiter")
187 }
188 Self::InvalidAgentsSection => {
189 write!(f, "invalid agents section in reaction SMARTS")
190 }
191 Self::MapNumberMismatch { map_num, message } => {
192 write!(f, "map number :{} error: {}", map_num, message)
193 }
194 }
195 }
196}
197
198impl std::error::Error for ReactionQueryError {}
199
200pub mod rdkit_compat {
202 use super::*;
203
204 #[derive(Clone, Debug)]
206 pub struct RDKitCompatReport {
207 pub is_compatible: bool,
209 pub warnings: Vec<String>,
211 pub rdkit_format: RDKitFormat,
213 }
214
215 #[derive(Clone, Debug, PartialEq, Eq)]
217 pub enum RDKitFormat {
218 Legacy,
220 WithAgents,
222 }
223
224 pub fn check_rdkit_compatibility(
230 smarts: &str,
231 ) -> Result<RDKitCompatReport, ReactionQueryError> {
232 let mut warnings = Vec::new();
233
234 let rdkit_format = if smarts.contains(">>") {
236 RDKitFormat::Legacy
237 } else if smarts.contains('>') {
238 RDKitFormat::WithAgents
239 } else {
240 return Err(ReactionQueryError::MissingArrowDelimiter);
241 };
242
243 let _pattern = super::parse_reaction_smarts(smarts)?;
245
246 if smarts.contains("[#") {
248 }
250
251 if smarts.contains("[*") {
252 warnings.push("wildcard atoms [*] may match behavior differently".to_string());
254 }
255
256 if smarts.contains("$") {
257 warnings.push("recursive SMARTS ($(...)) have complex matching behavior".to_string());
259 }
260
261 let section_count = smarts.matches('>').count();
263 let has_or_patterns = smarts.contains('|');
264 if has_or_patterns && section_count == 0 {
265 warnings
266 .push("pipe-separated patterns (|) require at least one > delimiter".to_string());
267 }
268
269 Ok(RDKitCompatReport {
270 is_compatible: warnings.is_empty(),
271 warnings,
272 rdkit_format,
273 })
274 }
275
276 pub fn match_to_rdkit_format(smarts_match: &ReactionSmartsMatch) -> RDKitMatchSummary {
280 RDKitMatchSummary {
281 matched: smarts_match.is_complete_match,
282 reactant_count: smarts_match.reactant_matches.pattern_matches.len(),
283 product_count: smarts_match.product_matches.pattern_matches.len(),
284 reactant_molecules_matched: smarts_match
285 .reactant_matches
286 .pattern_matches
287 .iter()
288 .filter(|m| !m.is_empty())
289 .count(),
290 product_molecules_matched: smarts_match
291 .product_matches
292 .pattern_matches
293 .iter()
294 .filter(|m| !m.is_empty())
295 .count(),
296 }
297 }
298
299 #[derive(Clone, Debug)]
301 pub struct RDKitMatchSummary {
302 pub matched: bool,
304 pub reactant_count: usize,
306 pub product_count: usize,
308 pub reactant_molecules_matched: usize,
310 pub product_molecules_matched: usize,
312 }
313
314 pub fn validate_rdkit_conventions(smarts: &str) -> Result<(), String> {
316 let arrow_count = smarts.matches('>').count();
318 if arrow_count == 0 {
319 return Err("must contain at least one '>' or '>>' delimiter".to_string());
320 }
321
322 if smarts.contains(">>>") {
324 return Err("invalid delimiter sequence >>>".to_string());
325 }
326
327 let map_nums: Vec<u16> = smarts
330 .match_indices(':')
331 .filter_map(|(i, _)| {
332 let remainder = &smarts[i + 1..];
333 let digits: String = remainder
334 .chars()
335 .take_while(|c| c.is_ascii_digit())
336 .collect();
337 if !digits.is_empty() {
338 digits.parse().ok()
339 } else {
340 None
341 }
342 })
343 .collect();
344
345 for map_num in map_nums {
346 if map_num == 0 || map_num > 999 {
347 return Err(format!(
348 "map number :{} outside RDKit range [1-999]",
349 map_num
350 ));
351 }
352 }
353
354 Ok(())
355 }
356}
357
358pub fn parse_reaction_smarts(
374 smarts_str: &str,
375) -> Result<ReactionSmartsPattern, ReactionQueryError> {
376 let has_double_arrow = smarts_str.contains(">>");
378
379 let (reactant_strs, agent_strs, product_strs) = if has_double_arrow {
380 let parts: Vec<&str> = smarts_str.splitn(2, ">>").collect();
382 if parts.len() != 2 {
383 return Err(ReactionQueryError::MissingArrowDelimiter);
384 }
385 (parts[0], "", parts[1])
386 } else {
387 let parts: Vec<&str> = smarts_str.splitn(3, '>').collect();
389 if parts.len() < 2 {
390 return Err(ReactionQueryError::MissingArrowDelimiter);
391 }
392 if parts.len() == 2 {
393 (parts[0], "", parts[1])
395 } else {
396 (parts[0], parts[1], parts[2])
398 }
399 };
400
401 let reactant_patterns = parse_patterns(reactant_strs)?;
402 let product_patterns = parse_patterns(product_strs)?;
403 let agent_patterns = if agent_strs.is_empty() {
404 Vec::new()
405 } else {
406 parse_patterns(agent_strs)?
407 };
408
409 let map_number_info = extract_map_numbers(smarts_str)?;
410
411 Ok(ReactionSmartsPattern {
412 reactant_patterns,
413 agent_patterns,
414 product_patterns,
415 map_number_info,
416 })
417}
418
419fn extract_map_numbers(smarts_str: &str) -> Result<MapNumberInfo, ReactionQueryError> {
421 let mut all_map_numbers = HashSet::new();
422 let mut reactant_maps = HashSet::new();
423 let mut agent_maps = HashSet::new();
424 let mut product_maps = HashSet::new();
425
426 let has_double_arrow = smarts_str.contains(">>");
428
429 let (reactant_end, agent_start, agent_end, product_start) = if has_double_arrow {
430 let idx = smarts_str.find(">>").unwrap();
431 (idx, idx, idx, idx + 2)
432 } else {
433 let arrow_positions: Vec<_> = smarts_str.match_indices('>').collect();
434 match arrow_positions.len() {
435 0 => {
436 return Ok(MapNumberInfo {
437 all_map_numbers,
438 reactant_maps,
439 agent_maps,
440 product_maps,
441 });
442 }
443 1 => {
444 let idx = arrow_positions[0].0;
445 (idx, idx + 1, idx + 1, idx + 1)
446 }
447 _ => {
448 let first = arrow_positions[0].0;
449 let second = arrow_positions[1].0;
450 (first, first + 1, second, second + 1)
451 }
452 }
453 };
454
455 let reactant_section = &smarts_str[..reactant_end];
457 for map_num in extract_map_numbers_from_section(reactant_section) {
458 reactant_maps.insert(map_num);
459 all_map_numbers.insert(map_num);
460 }
461
462 if agent_start < agent_end && agent_end <= smarts_str.len() {
464 let agent_section = &smarts_str[agent_start..agent_end];
465 if !agent_section.is_empty() {
466 for map_num in extract_map_numbers_from_section(agent_section) {
467 agent_maps.insert(map_num);
468 all_map_numbers.insert(map_num);
469 }
470 }
471 }
472
473 if product_start < smarts_str.len() {
475 let product_section = &smarts_str[product_start..];
476 for map_num in extract_map_numbers_from_section(product_section) {
477 product_maps.insert(map_num);
478 all_map_numbers.insert(map_num);
479 }
480 }
481
482 let info = MapNumberInfo {
483 all_map_numbers,
484 reactant_maps,
485 agent_maps,
486 product_maps,
487 };
488
489 info.validate().map_err(|msg| {
491 let map_num = info.all_map_numbers.iter().next().copied().unwrap_or(0);
492 ReactionQueryError::MapNumberMismatch {
493 map_num,
494 message: msg,
495 }
496 })?;
497
498 Ok(info)
499}
500
501fn extract_map_numbers_from_section(smarts: &str) -> Vec<u16> {
503 let mut map_numbers = Vec::new();
504 let bytes = smarts.as_bytes();
505 let mut bracket_depth: u32 = 0;
506 let mut i = 0;
507
508 while i < bytes.len() {
509 match bytes[i] {
510 b'[' => {
511 bracket_depth += 1;
512 i += 1;
513 }
514 b']' => {
515 bracket_depth = bracket_depth.saturating_sub(1);
516 i += 1;
517 }
518 b':' if bracket_depth > 0 => {
519 let mut j = i + 1;
521 let mut num_str = String::new();
522 while j < bytes.len() && bytes[j].is_ascii_digit() {
523 num_str.push(bytes[j] as char);
524 j += 1;
525 }
526 if !num_str.is_empty()
527 && let Ok(num) = num_str.parse::<u16>()
528 {
529 map_numbers.push(num);
530 }
531 i = j;
532 }
533 _ => {
534 i += 1;
535 }
536 }
537 }
538
539 map_numbers
540}
541
542fn parse_patterns(side: &str) -> Result<Vec<QueryMolecule>, ReactionQueryError> {
544 if side.is_empty() {
545 return Ok(Vec::new());
546 }
547 side.split('|')
548 .filter(|p| !p.is_empty())
549 .map(|p| {
550 parse_smarts(p).map_err(|e| ReactionQueryError::SmartsParseError {
551 smarts: p.to_string(),
552 source: e.to_string(),
553 })
554 })
555 .collect()
556}
557
558pub fn parse_reaction_query(s: &str) -> Result<ReactionQuery, ReactionQueryError> {
565 let parts: Vec<&str> = s.splitn(2, ">>").collect();
566 if parts.len() != 2 {
567 return Err(ReactionQueryError::SmartsParseError {
568 smarts: s.to_string(),
569 source: "reaction query must contain '>>'".to_string(),
570 });
571 }
572
573 Ok(ReactionQuery {
574 reactant_patterns: parse_patterns(parts[0])?,
575 product_patterns: parse_patterns(parts[1])?,
576 })
577}
578
579pub fn has_reaction_substructure_match(rxn: &Reaction, query: &ReactionQuery) -> bool {
587 for pattern in &query.reactant_patterns {
589 let mut matched = false;
590 for mol in &rxn.reactants {
591 if !find_matches(pattern, mol).is_empty() {
592 matched = true;
593 break;
594 }
595 }
596 if !matched {
597 return false;
598 }
599 }
600
601 for pattern in &query.product_patterns {
603 let mut matched = false;
604 for mol in &rxn.products {
605 if !find_matches(pattern, mol).is_empty() {
606 matched = true;
607 break;
608 }
609 }
610 if !matched {
611 return false;
612 }
613 }
614
615 true
616}
617
618pub fn get_reaction_smarts_matches(rxn: &Reaction, query: &ReactionQuery) -> ReactionSmartsMatch {
627 let mut reactant_pattern_matches = Vec::new();
629 for (pattern_idx, pattern) in query.reactant_patterns.iter().enumerate() {
630 let mut matches_for_pattern = Vec::new();
631 for (mol_idx, mol) in rxn.reactants.iter().enumerate() {
632 let atom_matches = find_matches(pattern, mol);
633 if let Some(first_match) = atom_matches.first() {
635 let atom_indices: Vec<usize> = first_match
636 .values()
637 .map(|atom_idx| atom_idx.0 as usize)
638 .collect();
639 matches_for_pattern.push(MoleculeMatch {
640 molecule_index: mol_idx,
641 pattern_index: pattern_idx,
642 atom_indices,
643 });
644 }
645 }
646 reactant_pattern_matches.push(matches_for_pattern);
647 }
648
649 let mut product_pattern_matches = Vec::new();
651 for (pattern_idx, pattern) in query.product_patterns.iter().enumerate() {
652 let mut matches_for_pattern = Vec::new();
653 for (mol_idx, mol) in rxn.products.iter().enumerate() {
654 let atom_matches = find_matches(pattern, mol);
655 if let Some(first_match) = atom_matches.first() {
657 let atom_indices: Vec<usize> = first_match
658 .values()
659 .map(|atom_idx| atom_idx.0 as usize)
660 .collect();
661 matches_for_pattern.push(MoleculeMatch {
662 molecule_index: mol_idx,
663 pattern_index: pattern_idx,
664 atom_indices,
665 });
666 }
667 }
668 product_pattern_matches.push(matches_for_pattern);
669 }
670
671 let all_reactants_matched = reactant_pattern_matches.iter().all(|m| !m.is_empty());
673 let all_products_matched = product_pattern_matches.iter().all(|m| !m.is_empty());
674 let is_complete_match = all_reactants_matched && all_products_matched;
675
676 ReactionSmartsMatch {
677 reactant_matches: ReactantMatches {
678 pattern_matches: reactant_pattern_matches,
679 },
680 product_matches: ProductMatches {
681 pattern_matches: product_pattern_matches,
682 },
683 is_complete_match,
684 }
685}
686
687#[derive(Clone, Debug)]
689pub struct BatchQueryResults {
690 pub total_reactions: usize,
692 pub matching_reactions: usize,
694 pub match_percentage: f64,
696 pub matches: Vec<(usize, bool)>, }
699
700impl BatchQueryResults {
701 pub fn matching_indices(&self) -> Vec<usize> {
703 self.matches
704 .iter()
705 .filter_map(|(idx, matched)| if *matched { Some(*idx) } else { None })
706 .collect()
707 }
708
709 pub fn non_matching_indices(&self) -> Vec<usize> {
711 self.matches
712 .iter()
713 .filter_map(|(idx, matched)| if !*matched { Some(*idx) } else { None })
714 .collect()
715 }
716}
717
718#[derive(Clone, Debug)]
720pub struct ReactionPatternLibrary {
721 pub patterns: std::collections::HashMap<String, ReactionSmartsPattern>,
723}
724
725impl ReactionPatternLibrary {
726 pub fn new() -> Self {
728 ReactionPatternLibrary {
729 patterns: std::collections::HashMap::new(),
730 }
731 }
732
733 pub fn add_pattern(&mut self, name: String, pattern: ReactionSmartsPattern) {
735 self.patterns.insert(name, pattern);
736 }
737
738 pub fn add_pattern_from_smarts(
740 &mut self,
741 name: String,
742 smarts: &str,
743 ) -> Result<(), ReactionQueryError> {
744 let pattern = parse_reaction_smarts(smarts)?;
745 self.add_pattern(name, pattern);
746 Ok(())
747 }
748
749 pub fn len(&self) -> usize {
751 self.patterns.len()
752 }
753
754 pub fn is_empty(&self) -> bool {
756 self.patterns.is_empty()
757 }
758
759 pub fn get(&self, name: &str) -> Option<&ReactionSmartsPattern> {
761 self.patterns.get(name)
762 }
763
764 pub fn pattern_names(&self) -> Vec<String> {
766 self.patterns.keys().cloned().collect()
767 }
768}
769
770impl Default for ReactionPatternLibrary {
771 fn default() -> Self {
772 Self::new()
773 }
774}
775
776pub fn query_reaction(
778 rxn: &Reaction,
779 smarts: &str,
780) -> Result<ReactionSmartsMatch, ReactionQueryError> {
781 let pattern = parse_reaction_smarts(smarts)?;
782 let query = ReactionQuery {
784 reactant_patterns: pattern.reactant_patterns,
785 product_patterns: pattern.product_patterns,
786 };
787 Ok(get_reaction_smarts_matches(rxn, &query))
788}
789
790pub fn batch_query_reactions(
792 reactions: &[Reaction],
793 smarts: &str,
794) -> Result<BatchQueryResults, ReactionQueryError> {
795 let pattern = parse_reaction_smarts(smarts)?;
796 let query = ReactionQuery {
797 reactant_patterns: pattern.reactant_patterns,
798 product_patterns: pattern.product_patterns,
799 };
800
801 let mut matches = Vec::new();
802 let mut matching_count = 0;
803
804 for (idx, rxn) in reactions.iter().enumerate() {
805 let result = get_reaction_smarts_matches(rxn, &query);
806 let is_match = result.is_complete_match;
807 if is_match {
808 matching_count += 1;
809 }
810 matches.push((idx, is_match));
811 }
812
813 let match_percentage = if reactions.is_empty() {
814 0.0
815 } else {
816 (matching_count as f64 / reactions.len() as f64) * 100.0
817 };
818
819 Ok(BatchQueryResults {
820 total_reactions: reactions.len(),
821 matching_reactions: matching_count,
822 match_percentage,
823 matches,
824 })
825}
826
827pub fn batch_query_with_library(
829 reactions: &[Reaction],
830 library: &ReactionPatternLibrary,
831) -> std::collections::HashMap<String, BatchQueryResults> {
832 let mut results = std::collections::HashMap::new();
833
834 for (pattern_name, pattern) in &library.patterns {
835 let query = ReactionQuery {
836 reactant_patterns: pattern.reactant_patterns.clone(),
837 product_patterns: pattern.product_patterns.clone(),
838 };
839
840 let mut matches = Vec::new();
841 let mut matching_count = 0;
842
843 for (idx, rxn) in reactions.iter().enumerate() {
844 let result = get_reaction_smarts_matches(rxn, &query);
845 let is_match = result.is_complete_match;
846 if is_match {
847 matching_count += 1;
848 }
849 matches.push((idx, is_match));
850 }
851
852 let match_percentage = if reactions.is_empty() {
853 0.0
854 } else {
855 (matching_count as f64 / reactions.len() as f64) * 100.0
856 };
857
858 results.insert(
859 pattern_name.clone(),
860 BatchQueryResults {
861 total_reactions: reactions.len(),
862 matching_reactions: matching_count,
863 match_percentage,
864 matches,
865 },
866 );
867 }
868
869 results
870}
871
872#[cfg(test)]
873mod tests {
874 use super::*;
875 use crate::parse_reaction;
876
877 fn rxn(s: &str) -> Reaction {
878 crate::reaction::parse_reaction(s).unwrap()
879 }
880
881 #[test]
882 fn test_parse_reaction_query_basic() {
883 let query = parse_reaction_query("[#6]>>[#6]").unwrap();
884 assert_eq!(query.reactant_patterns.len(), 1);
885 assert_eq!(query.product_patterns.len(), 1);
886 }
887
888 #[test]
889 fn test_parse_reaction_query_multiple_patterns() {
890 let query = parse_reaction_query("[#6]|[#7]>>[#8]|[#9]").unwrap();
891 assert_eq!(query.reactant_patterns.len(), 2);
892 assert_eq!(query.product_patterns.len(), 2);
893 }
894
895 #[test]
896 fn test_has_reaction_substructure_match_simple() {
897 let rxn = rxn("CC>>CC");
899 let query = parse_reaction_query("[#6]>>[#6]").unwrap();
900 assert!(has_reaction_substructure_match(&rxn, &query));
901 }
902
903 #[test]
904 fn test_has_reaction_substructure_match_no_match() {
905 let rxn = rxn("CC>>CC");
907 let query = parse_reaction_query("[#7]>>[#7]").unwrap();
909 assert!(!has_reaction_substructure_match(&rxn, &query));
910 }
911
912 #[test]
913 fn test_has_reaction_substructure_match_product_mismatch() {
914 let rxn = rxn("CC>>C");
916 let query = parse_reaction_query("[#6]>>CC").unwrap();
918 assert!(!has_reaction_substructure_match(&rxn, &query));
919 }
920
921 #[test]
922 fn test_has_reaction_substructure_match_empty_query() {
923 let rxn = rxn("CC>>C");
925 let query = parse_reaction_query(">>").unwrap();
926 assert!(has_reaction_substructure_match(&rxn, &query));
927 }
928
929 #[test]
932 fn test_parse_reaction_smarts_legacy_format() {
933 let pattern = parse_reaction_smarts("[C:1][C:2]>>[C:2][C:1]").unwrap();
935 assert_eq!(pattern.reactant_patterns.len(), 1);
936 assert_eq!(pattern.agent_patterns.len(), 0);
937 assert_eq!(pattern.product_patterns.len(), 1);
938 }
939
940 #[test]
941 fn test_parse_reaction_smarts_with_agents() {
942 let pattern = parse_reaction_smarts("[C:1][C:2]>[Pd]>[C:2][C:1]").unwrap();
944 assert_eq!(pattern.reactant_patterns.len(), 1);
945 assert_eq!(pattern.agent_patterns.len(), 1);
946 assert_eq!(pattern.product_patterns.len(), 1);
947 }
948
949 #[test]
950 fn test_parse_reaction_smarts_empty_agents() {
951 let pattern = parse_reaction_smarts("[C:1][C:2]>>[C:2][C:1]").unwrap();
953 assert_eq!(pattern.agent_patterns.len(), 0);
954 }
955
956 #[test]
957 fn test_parse_reaction_smarts_multiple_agent_patterns() {
958 let pattern = parse_reaction_smarts("[C:1]>[Pd]|[Ni]>[C:1]").unwrap();
960 assert_eq!(pattern.agent_patterns.len(), 2);
961 }
962
963 #[test]
964 fn test_extract_map_numbers_basic() {
965 let pattern = parse_reaction_smarts("[C:1][C:2]>>[C:2][C:1]").unwrap();
966 assert!(pattern.map_number_info.reactant_maps.contains(&1));
967 assert!(pattern.map_number_info.reactant_maps.contains(&2));
968 assert!(pattern.map_number_info.product_maps.contains(&1));
969 assert!(pattern.map_number_info.product_maps.contains(&2));
970 }
971
972 #[test]
973 fn test_extract_map_numbers_with_agents() {
974 let pattern = parse_reaction_smarts("[C:1]>[Pd:3]>[C:1]").unwrap();
975 assert!(pattern.map_number_info.reactant_maps.contains(&1));
976 assert!(pattern.map_number_info.agent_maps.contains(&3));
977 assert!(pattern.map_number_info.product_maps.contains(&1));
978 }
979
980 #[test]
981 fn test_map_number_validation_missing_in_products() {
982 let result = parse_reaction_smarts("[C:1][C:2]>>[C:2]");
984 assert!(result.is_err());
985 if let Err(ReactionQueryError::MapNumberMismatch { message, .. }) = result {
986 assert!(message.contains("missing from products"));
987 } else {
988 panic!("expected MapNumberMismatch error");
989 }
990 }
991
992 #[test]
993 fn test_map_number_validation_undefined_in_reactants() {
994 let result = parse_reaction_smarts("[C:2]>>[C:1][C:2]");
996 assert!(result.is_err());
997 if let Err(ReactionQueryError::MapNumberMismatch { message, .. }) = result {
998 assert!(message.contains("not found in reactants"));
999 } else {
1000 panic!("expected MapNumberMismatch error");
1001 }
1002 }
1003
1004 #[test]
1005 fn test_map_number_validation_consistent() {
1006 let pattern = parse_reaction_smarts("[C:1][C:2][C:3]>>[C:3][C:1][C:2]").unwrap();
1008 assert_eq!(pattern.map_number_info.reactant_maps.len(), 3);
1009 assert_eq!(pattern.map_number_info.product_maps.len(), 3);
1010 }
1011
1012 #[test]
1013 fn test_map_numbers_no_maps() {
1014 let pattern = parse_reaction_smarts("[#6]>>[#6]").unwrap();
1016 assert_eq!(pattern.map_number_info.all_map_numbers.len(), 0);
1017 }
1018
1019 #[test]
1020 fn test_map_numbers_large_numbers() {
1021 let pattern = parse_reaction_smarts("[C:100][N:999]>>[N:999][C:100]").unwrap();
1023 assert!(pattern.map_number_info.reactant_maps.contains(&100));
1024 assert!(pattern.map_number_info.reactant_maps.contains(&999));
1025 }
1026
1027 #[test]
1028 fn test_parse_reaction_smarts_missing_delimiter() {
1029 let result = parse_reaction_smarts("[C:1][C:2][C:1][C:2]");
1031 assert!(result.is_err());
1032 }
1033
1034 #[test]
1035 fn test_agents_section_in_map_numbers() {
1036 let pattern = parse_reaction_smarts("[C:1]>[Pd:99]>[C:1]").unwrap();
1038 assert!(pattern.map_number_info.agent_maps.contains(&99));
1039 assert!(!pattern.map_number_info.reactant_maps.contains(&99));
1041 }
1042
1043 #[test]
1046 fn test_get_reaction_smarts_matches_basic() {
1047 let rxn = rxn("CC>>CC");
1049 let query = parse_reaction_query("[#6]>>[#6]").unwrap();
1050 let matches = get_reaction_smarts_matches(&rxn, &query);
1051
1052 assert_eq!(matches.reactant_matches.pattern_matches.len(), 1);
1054 assert_eq!(matches.product_matches.pattern_matches.len(), 1);
1055
1056 assert!(matches.all_reactants_matched());
1058 assert!(matches.all_products_matched());
1059 assert!(matches.is_complete_match);
1060 }
1061
1062 #[test]
1063 fn test_get_reaction_smarts_matches_multiple_reactants() {
1064 let rxn = rxn("C.C>>CC");
1066 let query = parse_reaction_query("[#6]>>[#6]").unwrap();
1067 let matches = get_reaction_smarts_matches(&rxn, &query);
1068
1069 assert_eq!(matches.reactant_matches.pattern_matches[0].len(), 2);
1071 assert_eq!(matches.product_matches.pattern_matches[0].len(), 1);
1073 }
1074
1075 #[test]
1076 fn test_get_reaction_smarts_matches_incomplete() {
1077 let rxn = rxn("CC>>CC");
1079 let query = parse_reaction_query("[#7]>>[#6]").unwrap();
1080 let matches = get_reaction_smarts_matches(&rxn, &query);
1081
1082 assert!(!matches.all_reactants_matched());
1084 assert!(!matches.is_complete_match);
1085 }
1086
1087 #[test]
1088 fn test_get_reaction_smarts_matches_empty_patterns() {
1089 let rxn = rxn("CC>>CC");
1091 let query = parse_reaction_query(">>").unwrap();
1092 let matches = get_reaction_smarts_matches(&rxn, &query);
1093
1094 assert!(matches.is_complete_match);
1096 }
1097
1098 #[test]
1099 fn test_molecule_match_has_correct_indices() {
1100 let rxn = rxn("CC>>CC");
1102 let query = parse_reaction_query("[#6]>>[#6]").unwrap();
1103 let matches = get_reaction_smarts_matches(&rxn, &query);
1104
1105 let reactant_matches = &matches.reactant_matches.pattern_matches[0];
1107 assert_eq!(reactant_matches.len(), 1);
1108 assert_eq!(reactant_matches[0].molecule_index, 0);
1109 assert!(!reactant_matches[0].atom_indices.is_empty()); let product_matches = &matches.product_matches.pattern_matches[0];
1113 assert_eq!(product_matches.len(), 1);
1114 assert_eq!(product_matches[0].molecule_index, 0);
1115 assert!(!product_matches[0].atom_indices.is_empty()); }
1117
1118 #[test]
1119 fn test_reactant_matches_get_pattern_matches() {
1120 let rxn = rxn("CC>>CC");
1122 let query = parse_reaction_query("[#6]>>[#6]").unwrap();
1123 let matches = get_reaction_smarts_matches(&rxn, &query);
1124
1125 let reactant_pattern_0 = matches.reactant_matches.get_pattern_matches(0);
1127 assert!(reactant_pattern_0.is_some());
1128 assert_eq!(reactant_pattern_0.unwrap().len(), 1);
1129
1130 let reactant_pattern_1 = matches.reactant_matches.get_pattern_matches(1);
1132 assert!(reactant_pattern_1.is_none());
1133 }
1134
1135 #[test]
1136 fn test_product_matches_get_pattern_matches() {
1137 let rxn = rxn("CC>>CC");
1139 let query = parse_reaction_query("[#6]>>[#6]").unwrap();
1140 let matches = get_reaction_smarts_matches(&rxn, &query);
1141
1142 let product_pattern_0 = matches.product_matches.get_pattern_matches(0);
1143 assert!(product_pattern_0.is_some());
1144 assert_eq!(product_pattern_0.unwrap().len(), 1);
1145 }
1146
1147 #[test]
1148 fn test_pattern_matched_helper() {
1149 let rxn = rxn("CC>>CC");
1151 let query = parse_reaction_query("[#6]>>[#7]").unwrap();
1152 let matches = get_reaction_smarts_matches(&rxn, &query);
1153
1154 assert!(matches.reactant_matches.pattern_matched(0));
1156
1157 assert!(!matches.product_matches.pattern_matched(0));
1159 }
1160
1161 #[test]
1162 fn test_multiple_patterns_or_logic() {
1163 let rxn = rxn("CC>>CC");
1165 let query = parse_reaction_query("[#6]|[#7]>>[#6]|[#8]").unwrap();
1166 let matches = get_reaction_smarts_matches(&rxn, &query);
1167
1168 assert_eq!(matches.reactant_matches.pattern_matches.len(), 2);
1170 assert_eq!(matches.product_matches.pattern_matches.len(), 2);
1171
1172 assert!(matches.reactant_matches.pattern_matched(0)); assert!(!matches.reactant_matches.pattern_matched(1)); assert!(matches.product_matches.pattern_matched(0)); assert!(!matches.product_matches.pattern_matched(1)); assert!(!matches.is_complete_match);
1182 }
1183
1184 #[test]
1185 fn test_complex_reaction_matches() {
1186 let rxn = rxn("CC(C)>>CC=C");
1188 let query = parse_reaction_query("[#6]>>[#6]").unwrap();
1189 let matches = get_reaction_smarts_matches(&rxn, &query);
1190
1191 assert!(matches.all_reactants_matched());
1193 assert!(matches.all_products_matched());
1194 assert!(matches.is_complete_match);
1195
1196 assert_eq!(matches.reactant_matches.pattern_matches[0].len(), 1);
1198 assert!(
1199 !matches.reactant_matches.pattern_matches[0][0]
1200 .atom_indices
1201 .is_empty()
1202 );
1203
1204 assert_eq!(matches.product_matches.pattern_matches[0].len(), 1);
1206 assert!(
1207 !matches.product_matches.pattern_matches[0][0]
1208 .atom_indices
1209 .is_empty()
1210 );
1211 }
1212
1213 #[test]
1216 fn test_rdkit_compat_check_legacy_format() {
1217 use crate::query::rdkit_compat::*;
1218
1219 let result = check_rdkit_compatibility("[C:1][C:2]>>[C:2][C:1]").unwrap();
1220 assert!(result.is_compatible);
1221 assert_eq!(result.rdkit_format, RDKitFormat::Legacy);
1222 assert!(result.warnings.is_empty());
1223 }
1224
1225 #[test]
1226 fn test_rdkit_compat_check_agents_format() {
1227 use crate::query::rdkit_compat::*;
1228
1229 let result = check_rdkit_compatibility("[C:1][C:2]>[Pd]>[C:2][C:1]").unwrap();
1230 assert!(result.is_compatible);
1231 assert_eq!(result.rdkit_format, RDKitFormat::WithAgents);
1232 assert!(result.warnings.is_empty());
1233 }
1234
1235 #[test]
1236 fn test_rdkit_compat_wildcard_warning() {
1237 use crate::query::rdkit_compat::*;
1238
1239 let result = check_rdkit_compatibility("[*:1]>>[*:1]").unwrap();
1240 assert!(!result.is_compatible);
1241 assert!(result.warnings.iter().any(|w| w.contains("wildcard")));
1242 }
1243
1244 #[test]
1245 fn test_rdkit_compat_recursive_smarts_warning() {
1246 use crate::query::rdkit_compat::*;
1247
1248 let result = check_rdkit_compatibility("[#6;$([#6]~[#8])]>>[#6]").unwrap();
1250 assert!(!result.is_compatible);
1251 assert!(result.warnings.iter().any(|w| w.contains("recursive")));
1252 }
1253
1254 #[test]
1255 fn test_rdkit_compat_missing_delimiter() {
1256 use crate::query::rdkit_compat::*;
1257
1258 let result = check_rdkit_compatibility("[C:1][C:2][C:1][C:2]");
1259 assert!(result.is_err());
1260 }
1261
1262 #[test]
1263 fn test_rdkit_format_detection_legacy() {
1264 use crate::query::rdkit_compat::*;
1265
1266 let result = check_rdkit_compatibility("C>>C").unwrap();
1267 assert_eq!(result.rdkit_format, RDKitFormat::Legacy);
1268 }
1269
1270 #[test]
1271 fn test_rdkit_format_detection_agents() {
1272 use crate::query::rdkit_compat::*;
1273
1274 let result = check_rdkit_compatibility("C>[Pd]>C").unwrap();
1275 assert_eq!(result.rdkit_format, RDKitFormat::WithAgents);
1276 }
1277
1278 #[test]
1279 fn test_rdkit_match_summary_complete() {
1280 use crate::query::rdkit_compat::*;
1281
1282 let rxn = rxn("CC>>CC");
1283 let query = parse_reaction_query("[#6]>>[#6]").unwrap();
1284 let matches = get_reaction_smarts_matches(&rxn, &query);
1285
1286 let summary = match_to_rdkit_format(&matches);
1287 assert!(summary.matched);
1288 assert_eq!(summary.reactant_count, 1);
1289 assert_eq!(summary.product_count, 1);
1290 assert_eq!(summary.reactant_molecules_matched, 1);
1291 assert_eq!(summary.product_molecules_matched, 1);
1292 }
1293
1294 #[test]
1295 fn test_rdkit_match_summary_incomplete() {
1296 use crate::query::rdkit_compat::*;
1297
1298 let rxn = rxn("CC>>CC");
1299 let query = parse_reaction_query("[#7]>>[#6]").unwrap();
1300 let matches = get_reaction_smarts_matches(&rxn, &query);
1301
1302 let summary = match_to_rdkit_format(&matches);
1303 assert!(!summary.matched);
1304 assert_eq!(summary.reactant_molecules_matched, 0);
1305 }
1306
1307 #[test]
1308 fn test_rdkit_validate_conventions_valid() {
1309 use crate::query::rdkit_compat::*;
1310
1311 let result = validate_rdkit_conventions("[C:1][C:2]>>[C:2][C:1]");
1312 assert!(result.is_ok());
1313 }
1314
1315 #[test]
1316 fn test_rdkit_validate_conventions_no_delimiter() {
1317 use crate::query::rdkit_compat::*;
1318
1319 let result = validate_rdkit_conventions("[C:1][C:2][C:1][C:2]");
1320 assert!(result.is_err());
1321 }
1322
1323 #[test]
1324 fn test_rdkit_validate_conventions_triple_arrow() {
1325 use crate::query::rdkit_compat::*;
1326
1327 let result = validate_rdkit_conventions("[C:1]>>>[C:1]");
1328 assert!(result.is_err());
1329 }
1330
1331 #[test]
1332 fn test_rdkit_validate_conventions_map_number_zero() {
1333 use crate::query::rdkit_compat::*;
1334
1335 let result = validate_rdkit_conventions("[C:0]>>[C:0]");
1336 assert!(result.is_err());
1337 if let Err(msg) = result {
1338 assert!(msg.contains("outside RDKit range"));
1339 }
1340 }
1341
1342 #[test]
1343 fn test_rdkit_validate_conventions_map_number_out_of_range() {
1344 use crate::query::rdkit_compat::*;
1345
1346 let result = validate_rdkit_conventions("[C:1000]>>[C:1000]");
1347 assert!(result.is_err());
1348 if let Err(msg) = result {
1349 assert!(msg.contains("outside RDKit range"));
1350 }
1351 }
1352
1353 #[test]
1354 fn test_rdkit_validate_conventions_map_number_valid() {
1355 use crate::query::rdkit_compat::*;
1356
1357 let result = validate_rdkit_conventions("[C:1][N:999]>>[N:999][C:1]");
1358 assert!(result.is_ok());
1359 }
1360
1361 #[test]
1362 fn test_rdkit_compat_agents_pipes() {
1363 use crate::query::rdkit_compat::*;
1364
1365 let result = check_rdkit_compatibility("[C:1]>[Pd]|[Ni]>[C:1]").unwrap();
1366 assert!(result.is_compatible);
1367 assert_eq!(result.rdkit_format, RDKitFormat::WithAgents);
1368 }
1369
1370 #[test]
1371 fn test_rdkit_compat_multiple_patterns() {
1372 use crate::query::rdkit_compat::*;
1373
1374 let result = check_rdkit_compatibility("[#6]|[#7]>>[#8]|[#9]").unwrap();
1375 assert!(result.is_compatible);
1376 }
1377
1378 #[test]
1381 fn test_batch_query_reactions_empty() {
1382 let reactions: Vec<Reaction> = vec![];
1383 let smarts = "[C:1]>>[C:1]";
1384
1385 let result = batch_query_reactions(&reactions, smarts);
1386 assert!(result.is_ok());
1387
1388 let batch = result.unwrap();
1389 assert_eq!(batch.total_reactions, 0);
1390 assert_eq!(batch.matching_reactions, 0);
1391 assert_eq!(batch.match_percentage, 0.0);
1392 }
1393
1394 #[test]
1395 fn test_batch_query_reactions_single_match() {
1396 let rxn = parse_reaction("C>>C").unwrap();
1397 let reactions = vec![rxn];
1398 let smarts = "[C:1]>>[C:1]";
1399
1400 let result = batch_query_reactions(&reactions, smarts);
1401 assert!(result.is_ok());
1402
1403 let batch = result.unwrap();
1404 assert_eq!(batch.total_reactions, 1);
1405 assert!(batch.matching_reactions > 0);
1406 }
1407
1408 #[test]
1409 fn test_batch_query_reactions_multiple() {
1410 let rxn1 = parse_reaction("C>>C").unwrap();
1411 let rxn2 = parse_reaction("CC>>C").unwrap();
1412 let rxn3 = parse_reaction("CCC>>CC").unwrap();
1413
1414 let reactions = vec![rxn1, rxn2, rxn3];
1415 let smarts = "[C:1]>>[C:1]";
1416
1417 let result = batch_query_reactions(&reactions, smarts);
1418 assert!(result.is_ok());
1419
1420 let batch = result.unwrap();
1421 assert_eq!(batch.total_reactions, 3);
1422 assert_eq!(batch.matches.len(), 3);
1423 }
1424
1425 #[test]
1426 fn test_batch_query_results_indices() {
1427 let rxn1 = parse_reaction("C>>C").unwrap();
1428 let rxn2 = parse_reaction("CC>>C").unwrap();
1429 let rxn3 = parse_reaction("CCC>>CC").unwrap();
1430
1431 let reactions = vec![rxn1, rxn2, rxn3];
1432 let smarts = "[C:1]>>[C:1]";
1433
1434 let batch = batch_query_reactions(&reactions, smarts).unwrap();
1435
1436 let matching = batch.matching_indices();
1438 let non_matching = batch.non_matching_indices();
1439
1440 assert_eq!(matching.len() + non_matching.len(), batch.total_reactions);
1441 }
1442
1443 #[test]
1444 fn test_reaction_pattern_library_new() {
1445 let library = ReactionPatternLibrary::new();
1446 assert!(library.is_empty());
1447 assert_eq!(library.len(), 0);
1448 }
1449
1450 #[test]
1451 fn test_reaction_pattern_library_add_pattern() {
1452 let mut library = ReactionPatternLibrary::new();
1453
1454 let pattern = parse_reaction_smarts("[C:1]>>[C:1]").unwrap();
1455 library.add_pattern("simple_carbon".to_string(), pattern);
1456
1457 assert_eq!(library.len(), 1);
1458 assert!(!library.is_empty());
1459 assert!(library.get("simple_carbon").is_some());
1460 }
1461
1462 #[test]
1463 fn test_reaction_pattern_library_add_from_smarts() {
1464 let mut library = ReactionPatternLibrary::new();
1465
1466 let result = library.add_pattern_from_smarts(
1467 "acylation".to_string(),
1468 "[C:1](=[O:2])[N:3]>>[C:1](=[O:2])[N:3]",
1469 );
1470 assert!(result.is_ok());
1471 assert_eq!(library.len(), 1);
1472 }
1473
1474 #[test]
1475 fn test_reaction_pattern_library_multiple_patterns() {
1476 let mut library = ReactionPatternLibrary::new();
1477
1478 let _ = library.add_pattern_from_smarts("reaction1".to_string(), "[C:1]>>[C:1]");
1479 let _ = library.add_pattern_from_smarts("reaction2".to_string(), "[N:1]>>[N:1]");
1480 let _ = library.add_pattern_from_smarts("reaction3".to_string(), "[O:1]>>[O:1]");
1481
1482 assert_eq!(library.len(), 3);
1483
1484 let names = library.pattern_names();
1485 assert_eq!(names.len(), 3);
1486 assert!(names.contains(&"reaction1".to_string()));
1487 }
1488
1489 #[test]
1490 fn test_batch_query_with_library() {
1491 let mut library = ReactionPatternLibrary::new();
1492 let _ = library.add_pattern_from_smarts("carbon_only".to_string(), "[C:1]>>[C:1]");
1493
1494 let rxn1 = parse_reaction("C>>C").unwrap();
1495 let rxn2 = parse_reaction("CC>>C").unwrap();
1496 let reactions = vec![rxn1, rxn2];
1497
1498 let results = batch_query_with_library(&reactions, &library);
1499
1500 assert!(!results.is_empty());
1501 assert!(results.contains_key("carbon_only"));
1502
1503 let batch = &results["carbon_only"];
1504 assert_eq!(batch.total_reactions, 2);
1505 }
1506
1507 #[test]
1508 fn test_query_reaction_simple() {
1509 let rxn = parse_reaction("C>>C").unwrap();
1510 let smarts = "[C:1]>>[C:1]";
1511
1512 let result = query_reaction(&rxn, smarts);
1513 assert!(result.is_ok());
1514
1515 let match_result = result.unwrap();
1516 assert!(!match_result.reactant_matches.pattern_matches.is_empty());
1518 }
1519
1520 #[test]
1521 fn test_batch_query_match_percentage() {
1522 let rxn1 = parse_reaction("C>>C").unwrap();
1523 let rxn2 = parse_reaction("CC>>C").unwrap();
1524 let rxn3 = parse_reaction("CCC>>CC").unwrap();
1525
1526 let reactions = vec![rxn1, rxn2, rxn3];
1527 let smarts = "[C:1]>>[C:1]";
1528
1529 let batch = batch_query_reactions(&reactions, smarts).unwrap();
1530
1531 assert!(batch.match_percentage >= 0.0);
1533 assert!(batch.match_percentage <= 100.0);
1534
1535 let expected_percentage =
1537 (batch.matching_reactions as f64 / batch.total_reactions as f64) * 100.0;
1538 assert!((batch.match_percentage - expected_percentage).abs() < 0.01);
1539 }
1540
1541 #[test]
1542 fn test_reaction_pattern_library_default() {
1543 let library = ReactionPatternLibrary::default();
1544 assert!(library.is_empty());
1545 }
1546}