use chematic_smarts::{find_matches, parse_smarts, QueryMolecule};
use std::collections::HashSet;
use crate::reaction::Reaction;
#[derive(Clone, Debug)]
pub struct ReactionQuery {
pub reactant_patterns: Vec<QueryMolecule>,
pub product_patterns: Vec<QueryMolecule>,
}
#[derive(Clone, Debug)]
pub struct ReactionSmartsPattern {
pub reactant_patterns: Vec<QueryMolecule>,
pub agent_patterns: Vec<QueryMolecule>,
pub product_patterns: Vec<QueryMolecule>,
pub map_number_info: MapNumberInfo,
}
#[derive(Clone, Debug)]
pub struct MapNumberInfo {
pub all_map_numbers: HashSet<u16>,
pub reactant_maps: HashSet<u16>,
pub agent_maps: HashSet<u16>,
pub product_maps: HashSet<u16>,
}
#[derive(Clone, Debug)]
pub struct MoleculeMatch {
pub molecule_index: usize,
pub pattern_index: usize,
pub atom_indices: Vec<usize>,
}
#[derive(Clone, Debug)]
pub struct ReactantMatches {
pub pattern_matches: Vec<Vec<MoleculeMatch>>,
}
impl ReactantMatches {
pub fn get_pattern_matches(&self, pattern_index: usize) -> Option<&[MoleculeMatch]> {
self.pattern_matches.get(pattern_index).map(|v| v.as_slice())
}
pub fn pattern_matched(&self, pattern_index: usize) -> bool {
self.pattern_matches
.get(pattern_index)
.is_some_and(|matches| !matches.is_empty())
}
}
#[derive(Clone, Debug)]
pub struct ProductMatches {
pub pattern_matches: Vec<Vec<MoleculeMatch>>,
}
impl ProductMatches {
pub fn get_pattern_matches(&self, pattern_index: usize) -> Option<&[MoleculeMatch]> {
self.pattern_matches.get(pattern_index).map(|v| v.as_slice())
}
pub fn pattern_matched(&self, pattern_index: usize) -> bool {
self.pattern_matches
.get(pattern_index)
.is_some_and(|matches| !matches.is_empty())
}
}
#[derive(Clone, Debug)]
pub struct ReactionSmartsMatch {
pub reactant_matches: ReactantMatches,
pub product_matches: ProductMatches,
pub is_complete_match: bool,
}
impl ReactionSmartsMatch {
pub fn all_reactants_matched(&self) -> bool {
self.reactant_matches.pattern_matches.iter().all(|m| !m.is_empty())
}
pub fn all_products_matched(&self) -> bool {
self.product_matches.pattern_matches.iter().all(|m| !m.is_empty())
}
}
impl MapNumberInfo {
pub fn validate(&self) -> Result<(), String> {
let missing_in_products: Vec<u16> = self.reactant_maps
.iter()
.filter(|&m| !self.product_maps.contains(m))
.copied()
.collect();
if !missing_in_products.is_empty() {
return Err(format!(
"map numbers in reactants missing from products: {:?}",
missing_in_products
));
}
let undefined_in_reactants: Vec<u16> = self.product_maps
.iter()
.filter(|&m| !self.reactant_maps.contains(m))
.copied()
.collect();
if !undefined_in_reactants.is_empty() {
return Err(format!(
"map numbers in products not found in reactants: {:?}",
undefined_in_reactants
));
}
Ok(())
}
}
#[derive(Debug)]
pub enum ReactionQueryError {
SmartsParseError { smarts: String, source: String },
MissingArrowDelimiter,
InvalidAgentsSection,
MapNumberMismatch { map_num: u16, message: String },
}
impl core::fmt::Display for ReactionQueryError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::SmartsParseError { smarts, source } => {
write!(f, "failed to parse SMARTS '{smarts}': {source}")
}
Self::MissingArrowDelimiter => {
write!(f, "reaction SMARTS must contain '>' or '>>' delimiter")
}
Self::InvalidAgentsSection => {
write!(f, "invalid agents section in reaction SMARTS")
}
Self::MapNumberMismatch { map_num, message } => {
write!(f, "map number :{} error: {}", map_num, message)
}
}
}
}
impl std::error::Error for ReactionQueryError {}
pub mod rdkit_compat {
use super::*;
#[derive(Clone, Debug)]
pub struct RDKitCompatReport {
pub is_compatible: bool,
pub warnings: Vec<String>,
pub rdkit_format: RDKitFormat,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RDKitFormat {
Legacy,
WithAgents,
}
pub fn check_rdkit_compatibility(smarts: &str) -> Result<RDKitCompatReport, ReactionQueryError> {
let mut warnings = Vec::new();
let rdkit_format = if smarts.contains(">>") {
RDKitFormat::Legacy
} else if smarts.contains('>') {
RDKitFormat::WithAgents
} else {
return Err(ReactionQueryError::MissingArrowDelimiter);
};
let _pattern = super::parse_reaction_smarts(smarts)?;
if smarts.contains("[#") {
}
if smarts.contains("[*") {
warnings.push("wildcard atoms [*] may match behavior differently".to_string());
}
if smarts.contains("$") {
warnings.push("recursive SMARTS ($(...)) have complex matching behavior".to_string());
}
let section_count = smarts.matches('>').count();
let has_or_patterns = smarts.contains('|');
if has_or_patterns && section_count == 0 {
warnings.push("pipe-separated patterns (|) require at least one > delimiter".to_string());
}
Ok(RDKitCompatReport {
is_compatible: warnings.is_empty(),
warnings,
rdkit_format,
})
}
pub fn match_to_rdkit_format(smarts_match: &ReactionSmartsMatch) -> RDKitMatchSummary {
RDKitMatchSummary {
matched: smarts_match.is_complete_match,
reactant_count: smarts_match.reactant_matches.pattern_matches.len(),
product_count: smarts_match.product_matches.pattern_matches.len(),
reactant_molecules_matched: smarts_match
.reactant_matches
.pattern_matches
.iter()
.filter(|m| !m.is_empty())
.count(),
product_molecules_matched: smarts_match
.product_matches
.pattern_matches
.iter()
.filter(|m| !m.is_empty())
.count(),
}
}
#[derive(Clone, Debug)]
pub struct RDKitMatchSummary {
pub matched: bool,
pub reactant_count: usize,
pub product_count: usize,
pub reactant_molecules_matched: usize,
pub product_molecules_matched: usize,
}
pub fn validate_rdkit_conventions(smarts: &str) -> Result<(), String> {
let arrow_count = smarts.matches('>').count();
if arrow_count == 0 {
return Err("must contain at least one '>' or '>>' delimiter".to_string());
}
if smarts.contains(">>>") {
return Err("invalid delimiter sequence >>>".to_string());
}
let map_nums: Vec<u16> = smarts
.match_indices(':')
.filter_map(|(i, _)| {
let remainder = &smarts[i + 1..];
let digits: String = remainder.chars().take_while(|c| c.is_ascii_digit()).collect();
if !digits.is_empty() {
digits.parse().ok()
} else {
None
}
})
.collect();
for map_num in map_nums {
if map_num == 0 || map_num > 999 {
return Err(format!("map number :{} outside RDKit range [1-999]", map_num));
}
}
Ok(())
}
}
pub fn parse_reaction_smarts(smarts_str: &str) -> Result<ReactionSmartsPattern, ReactionQueryError> {
let has_double_arrow = smarts_str.contains(">>");
let (reactant_strs, agent_strs, product_strs) = if has_double_arrow {
let parts: Vec<&str> = smarts_str.splitn(2, ">>").collect();
if parts.len() != 2 {
return Err(ReactionQueryError::MissingArrowDelimiter);
}
(parts[0], "", parts[1])
} else {
let parts: Vec<&str> = smarts_str.splitn(3, '>').collect();
if parts.len() < 2 {
return Err(ReactionQueryError::MissingArrowDelimiter);
}
if parts.len() == 2 {
(parts[0], "", parts[1])
} else {
(parts[0], parts[1], parts[2])
}
};
let reactant_patterns = parse_patterns(reactant_strs)?;
let product_patterns = parse_patterns(product_strs)?;
let agent_patterns = if agent_strs.is_empty() {
Vec::new()
} else {
parse_patterns(agent_strs)?
};
let map_number_info = extract_map_numbers(smarts_str)?;
Ok(ReactionSmartsPattern {
reactant_patterns,
agent_patterns,
product_patterns,
map_number_info,
})
}
fn extract_map_numbers(smarts_str: &str) -> Result<MapNumberInfo, ReactionQueryError> {
let mut all_map_numbers = HashSet::new();
let mut reactant_maps = HashSet::new();
let mut agent_maps = HashSet::new();
let mut product_maps = HashSet::new();
let has_double_arrow = smarts_str.contains(">>");
let (reactant_end, agent_start, agent_end, product_start) = if has_double_arrow {
let idx = smarts_str.find(">>").unwrap();
(idx, idx, idx, idx + 2)
} else {
let arrow_positions: Vec<_> = smarts_str.match_indices('>').collect();
match arrow_positions.len() {
0 => return Ok(MapNumberInfo {
all_map_numbers,
reactant_maps,
agent_maps,
product_maps,
}),
1 => {
let idx = arrow_positions[0].0;
(idx, idx + 1, idx + 1, idx + 1)
}
_ => {
let first = arrow_positions[0].0;
let second = arrow_positions[1].0;
(first, first + 1, second, second + 1)
}
}
};
let reactant_section = &smarts_str[..reactant_end];
for map_num in extract_map_numbers_from_section(reactant_section) {
reactant_maps.insert(map_num);
all_map_numbers.insert(map_num);
}
if agent_start < agent_end && agent_end <= smarts_str.len() {
let agent_section = &smarts_str[agent_start..agent_end];
if !agent_section.is_empty() {
for map_num in extract_map_numbers_from_section(agent_section) {
agent_maps.insert(map_num);
all_map_numbers.insert(map_num);
}
}
}
if product_start < smarts_str.len() {
let product_section = &smarts_str[product_start..];
for map_num in extract_map_numbers_from_section(product_section) {
product_maps.insert(map_num);
all_map_numbers.insert(map_num);
}
}
let info = MapNumberInfo {
all_map_numbers,
reactant_maps,
agent_maps,
product_maps,
};
info.validate().map_err(|msg| {
let map_num = info.all_map_numbers.iter().next().copied().unwrap_or(0);
ReactionQueryError::MapNumberMismatch {
map_num,
message: msg,
}
})?;
Ok(info)
}
fn extract_map_numbers_from_section(smarts: &str) -> Vec<u16> {
let mut map_numbers = Vec::new();
let bytes = smarts.as_bytes();
for i in 0..bytes.len() {
if bytes[i] == b':' && i + 1 < bytes.len() {
let mut j = i + 1;
let mut num_str = String::new();
while j < bytes.len() && bytes[j].is_ascii_digit() {
num_str.push(bytes[j] as char);
j += 1;
}
if !num_str.is_empty()
&& let Ok(num) = num_str.parse::<u16>() {
map_numbers.push(num);
}
}
}
map_numbers
}
fn parse_patterns(side: &str) -> Result<Vec<QueryMolecule>, ReactionQueryError> {
if side.is_empty() {
return Ok(Vec::new());
}
side.split('|')
.filter(|p| !p.is_empty())
.map(|p| {
let smarts_without_maps = strip_map_numbers(p);
parse_smarts(&smarts_without_maps).map_err(|e| ReactionQueryError::SmartsParseError {
smarts: p.to_string(),
source: e.to_string(),
})
})
.collect()
}
fn strip_map_numbers(smarts: &str) -> String {
let mut result = String::new();
let bytes = smarts.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b':' && i > 0 && i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() {
i += 1;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
} else {
result.push(bytes[i] as char);
i += 1;
}
}
result
}
pub fn parse_reaction_query(s: &str) -> Result<ReactionQuery, ReactionQueryError> {
let parts: Vec<&str> = s.splitn(2, ">>").collect();
if parts.len() != 2 {
return Err(ReactionQueryError::SmartsParseError {
smarts: s.to_string(),
source: "reaction query must contain '>>'".to_string(),
});
}
Ok(ReactionQuery {
reactant_patterns: parse_patterns(parts[0])?,
product_patterns: parse_patterns(parts[1])?,
})
}
pub fn has_reaction_substructure_match(rxn: &Reaction, query: &ReactionQuery) -> bool {
for pattern in &query.reactant_patterns {
let mut matched = false;
for mol in &rxn.reactants {
if !find_matches(pattern, mol).is_empty() {
matched = true;
break;
}
}
if !matched {
return false;
}
}
for pattern in &query.product_patterns {
let mut matched = false;
for mol in &rxn.products {
if !find_matches(pattern, mol).is_empty() {
matched = true;
break;
}
}
if !matched {
return false;
}
}
true
}
pub fn get_reaction_smarts_matches(
rxn: &Reaction,
query: &ReactionQuery,
) -> ReactionSmartsMatch {
let mut reactant_pattern_matches = Vec::new();
for (pattern_idx, pattern) in query.reactant_patterns.iter().enumerate() {
let mut matches_for_pattern = Vec::new();
for (mol_idx, mol) in rxn.reactants.iter().enumerate() {
let atom_matches = find_matches(pattern, mol);
if let Some(first_match) = atom_matches.first() {
let atom_indices: Vec<usize> = first_match
.values()
.map(|atom_idx| atom_idx.0 as usize)
.collect();
matches_for_pattern.push(MoleculeMatch {
molecule_index: mol_idx,
pattern_index: pattern_idx,
atom_indices,
});
}
}
reactant_pattern_matches.push(matches_for_pattern);
}
let mut product_pattern_matches = Vec::new();
for (pattern_idx, pattern) in query.product_patterns.iter().enumerate() {
let mut matches_for_pattern = Vec::new();
for (mol_idx, mol) in rxn.products.iter().enumerate() {
let atom_matches = find_matches(pattern, mol);
if let Some(first_match) = atom_matches.first() {
let atom_indices: Vec<usize> = first_match
.values()
.map(|atom_idx| atom_idx.0 as usize)
.collect();
matches_for_pattern.push(MoleculeMatch {
molecule_index: mol_idx,
pattern_index: pattern_idx,
atom_indices,
});
}
}
product_pattern_matches.push(matches_for_pattern);
}
let all_reactants_matched = reactant_pattern_matches.iter().all(|m| !m.is_empty());
let all_products_matched = product_pattern_matches.iter().all(|m| !m.is_empty());
let is_complete_match = all_reactants_matched && all_products_matched;
ReactionSmartsMatch {
reactant_matches: ReactantMatches {
pattern_matches: reactant_pattern_matches,
},
product_matches: ProductMatches {
pattern_matches: product_pattern_matches,
},
is_complete_match,
}
}
#[derive(Clone, Debug)]
pub struct BatchQueryResults {
pub total_reactions: usize,
pub matching_reactions: usize,
pub match_percentage: f64,
pub matches: Vec<(usize, bool)>, }
impl BatchQueryResults {
pub fn matching_indices(&self) -> Vec<usize> {
self.matches
.iter()
.filter_map(|(idx, matched)| if *matched { Some(*idx) } else { None })
.collect()
}
pub fn non_matching_indices(&self) -> Vec<usize> {
self.matches
.iter()
.filter_map(|(idx, matched)| if !*matched { Some(*idx) } else { None })
.collect()
}
}
#[derive(Clone, Debug)]
pub struct ReactionPatternLibrary {
pub patterns: std::collections::HashMap<String, ReactionSmartsPattern>,
}
impl ReactionPatternLibrary {
pub fn new() -> Self {
ReactionPatternLibrary {
patterns: std::collections::HashMap::new(),
}
}
pub fn add_pattern(&mut self, name: String, pattern: ReactionSmartsPattern) {
self.patterns.insert(name, pattern);
}
pub fn add_pattern_from_smarts(
&mut self,
name: String,
smarts: &str,
) -> Result<(), ReactionQueryError> {
let pattern = parse_reaction_smarts(smarts)?;
self.add_pattern(name, pattern);
Ok(())
}
pub fn len(&self) -> usize {
self.patterns.len()
}
pub fn is_empty(&self) -> bool {
self.patterns.is_empty()
}
pub fn get(&self, name: &str) -> Option<&ReactionSmartsPattern> {
self.patterns.get(name)
}
pub fn pattern_names(&self) -> Vec<String> {
self.patterns.keys().cloned().collect()
}
}
impl Default for ReactionPatternLibrary {
fn default() -> Self {
Self::new()
}
}
pub fn query_reaction(
rxn: &Reaction,
smarts: &str,
) -> Result<ReactionSmartsMatch, ReactionQueryError> {
let pattern = parse_reaction_smarts(smarts)?;
let query = ReactionQuery {
reactant_patterns: pattern.reactant_patterns,
product_patterns: pattern.product_patterns,
};
Ok(get_reaction_smarts_matches(rxn, &query))
}
pub fn batch_query_reactions(
reactions: &[Reaction],
smarts: &str,
) -> Result<BatchQueryResults, ReactionQueryError> {
let pattern = parse_reaction_smarts(smarts)?;
let query = ReactionQuery {
reactant_patterns: pattern.reactant_patterns,
product_patterns: pattern.product_patterns,
};
let mut matches = Vec::new();
let mut matching_count = 0;
for (idx, rxn) in reactions.iter().enumerate() {
let result = get_reaction_smarts_matches(rxn, &query);
let is_match = result.is_complete_match;
if is_match {
matching_count += 1;
}
matches.push((idx, is_match));
}
let match_percentage = if reactions.is_empty() {
0.0
} else {
(matching_count as f64 / reactions.len() as f64) * 100.0
};
Ok(BatchQueryResults {
total_reactions: reactions.len(),
matching_reactions: matching_count,
match_percentage,
matches,
})
}
pub fn batch_query_with_library(
reactions: &[Reaction],
library: &ReactionPatternLibrary,
) -> std::collections::HashMap<String, BatchQueryResults> {
let mut results = std::collections::HashMap::new();
for (pattern_name, pattern) in &library.patterns {
let query = ReactionQuery {
reactant_patterns: pattern.reactant_patterns.clone(),
product_patterns: pattern.product_patterns.clone(),
};
let mut matches = Vec::new();
let mut matching_count = 0;
for (idx, rxn) in reactions.iter().enumerate() {
let result = get_reaction_smarts_matches(rxn, &query);
let is_match = result.is_complete_match;
if is_match {
matching_count += 1;
}
matches.push((idx, is_match));
}
let match_percentage = if reactions.is_empty() {
0.0
} else {
(matching_count as f64 / reactions.len() as f64) * 100.0
};
results.insert(
pattern_name.clone(),
BatchQueryResults {
total_reactions: reactions.len(),
matching_reactions: matching_count,
match_percentage,
matches,
},
);
}
results
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse_reaction;
fn rxn(s: &str) -> Reaction {
crate::reaction::parse_reaction(s).unwrap()
}
#[test]
fn test_parse_reaction_query_basic() {
let query = parse_reaction_query("[#6]>>[#6]").unwrap();
assert_eq!(query.reactant_patterns.len(), 1);
assert_eq!(query.product_patterns.len(), 1);
}
#[test]
fn test_parse_reaction_query_multiple_patterns() {
let query = parse_reaction_query("[#6]|[#7]>>[#8]|[#9]").unwrap();
assert_eq!(query.reactant_patterns.len(), 2);
assert_eq!(query.product_patterns.len(), 2);
}
#[test]
fn test_has_reaction_substructure_match_simple() {
let rxn = rxn("CC>>CC");
let query = parse_reaction_query("[#6]>>[#6]").unwrap();
assert!(has_reaction_substructure_match(&rxn, &query));
}
#[test]
fn test_has_reaction_substructure_match_no_match() {
let rxn = rxn("CC>>CC");
let query = parse_reaction_query("[#7]>>[#7]").unwrap();
assert!(!has_reaction_substructure_match(&rxn, &query));
}
#[test]
fn test_has_reaction_substructure_match_product_mismatch() {
let rxn = rxn("CC>>C");
let query = parse_reaction_query("[#6]>>CC").unwrap();
assert!(!has_reaction_substructure_match(&rxn, &query));
}
#[test]
fn test_has_reaction_substructure_match_empty_query() {
let rxn = rxn("CC>>C");
let query = parse_reaction_query(">>").unwrap();
assert!(has_reaction_substructure_match(&rxn, &query));
}
#[test]
fn test_parse_reaction_smarts_legacy_format() {
let pattern = parse_reaction_smarts("[C:1][C:2]>>[C:2][C:1]").unwrap();
assert_eq!(pattern.reactant_patterns.len(), 1);
assert_eq!(pattern.agent_patterns.len(), 0);
assert_eq!(pattern.product_patterns.len(), 1);
}
#[test]
fn test_parse_reaction_smarts_with_agents() {
let pattern = parse_reaction_smarts("[C:1][C:2]>[Pd]>[C:2][C:1]").unwrap();
assert_eq!(pattern.reactant_patterns.len(), 1);
assert_eq!(pattern.agent_patterns.len(), 1);
assert_eq!(pattern.product_patterns.len(), 1);
}
#[test]
fn test_parse_reaction_smarts_empty_agents() {
let pattern = parse_reaction_smarts("[C:1][C:2]>>[C:2][C:1]").unwrap();
assert_eq!(pattern.agent_patterns.len(), 0);
}
#[test]
fn test_parse_reaction_smarts_multiple_agent_patterns() {
let pattern = parse_reaction_smarts("[C:1]>[Pd]|[Ni]>[C:1]").unwrap();
assert_eq!(pattern.agent_patterns.len(), 2);
}
#[test]
fn test_extract_map_numbers_basic() {
let pattern = parse_reaction_smarts("[C:1][C:2]>>[C:2][C:1]").unwrap();
assert!(pattern.map_number_info.reactant_maps.contains(&1));
assert!(pattern.map_number_info.reactant_maps.contains(&2));
assert!(pattern.map_number_info.product_maps.contains(&1));
assert!(pattern.map_number_info.product_maps.contains(&2));
}
#[test]
fn test_extract_map_numbers_with_agents() {
let pattern = parse_reaction_smarts("[C:1]>[Pd:3]>[C:1]").unwrap();
assert!(pattern.map_number_info.reactant_maps.contains(&1));
assert!(pattern.map_number_info.agent_maps.contains(&3));
assert!(pattern.map_number_info.product_maps.contains(&1));
}
#[test]
fn test_map_number_validation_missing_in_products() {
let result = parse_reaction_smarts("[C:1][C:2]>>[C:2]");
assert!(result.is_err());
if let Err(ReactionQueryError::MapNumberMismatch { message, .. }) = result {
assert!(message.contains("missing from products"));
} else {
panic!("expected MapNumberMismatch error");
}
}
#[test]
fn test_map_number_validation_undefined_in_reactants() {
let result = parse_reaction_smarts("[C:2]>>[C:1][C:2]");
assert!(result.is_err());
if let Err(ReactionQueryError::MapNumberMismatch { message, .. }) = result {
assert!(message.contains("not found in reactants"));
} else {
panic!("expected MapNumberMismatch error");
}
}
#[test]
fn test_map_number_validation_consistent() {
let pattern = parse_reaction_smarts("[C:1][C:2][C:3]>>[C:3][C:1][C:2]").unwrap();
assert_eq!(pattern.map_number_info.reactant_maps.len(), 3);
assert_eq!(pattern.map_number_info.product_maps.len(), 3);
}
#[test]
fn test_map_numbers_no_maps() {
let pattern = parse_reaction_smarts("[#6]>>[#6]").unwrap();
assert_eq!(pattern.map_number_info.all_map_numbers.len(), 0);
}
#[test]
fn test_map_numbers_large_numbers() {
let pattern = parse_reaction_smarts("[C:100][N:999]>>[N:999][C:100]").unwrap();
assert!(pattern.map_number_info.reactant_maps.contains(&100));
assert!(pattern.map_number_info.reactant_maps.contains(&999));
}
#[test]
fn test_parse_reaction_smarts_missing_delimiter() {
let result = parse_reaction_smarts("[C:1][C:2][C:1][C:2]");
assert!(result.is_err());
}
#[test]
fn test_agents_section_in_map_numbers() {
let pattern = parse_reaction_smarts("[C:1]>[Pd:99]>[C:1]").unwrap();
assert!(pattern.map_number_info.agent_maps.contains(&99));
assert!(!pattern.map_number_info.reactant_maps.contains(&99));
}
#[test]
fn test_get_reaction_smarts_matches_basic() {
let rxn = rxn("CC>>CC");
let query = parse_reaction_query("[#6]>>[#6]").unwrap();
let matches = get_reaction_smarts_matches(&rxn, &query);
assert_eq!(matches.reactant_matches.pattern_matches.len(), 1);
assert_eq!(matches.product_matches.pattern_matches.len(), 1);
assert!(matches.all_reactants_matched());
assert!(matches.all_products_matched());
assert!(matches.is_complete_match);
}
#[test]
fn test_get_reaction_smarts_matches_multiple_reactants() {
let rxn = rxn("C.C>>CC");
let query = parse_reaction_query("[#6]>>[#6]").unwrap();
let matches = get_reaction_smarts_matches(&rxn, &query);
assert_eq!(matches.reactant_matches.pattern_matches[0].len(), 2);
assert_eq!(matches.product_matches.pattern_matches[0].len(), 1);
}
#[test]
fn test_get_reaction_smarts_matches_incomplete() {
let rxn = rxn("CC>>CC");
let query = parse_reaction_query("[#7]>>[#6]").unwrap();
let matches = get_reaction_smarts_matches(&rxn, &query);
assert!(!matches.all_reactants_matched());
assert!(!matches.is_complete_match);
}
#[test]
fn test_get_reaction_smarts_matches_empty_patterns() {
let rxn = rxn("CC>>CC");
let query = parse_reaction_query(">>").unwrap();
let matches = get_reaction_smarts_matches(&rxn, &query);
assert!(matches.is_complete_match);
}
#[test]
fn test_molecule_match_has_correct_indices() {
let rxn = rxn("CC>>CC");
let query = parse_reaction_query("[#6]>>[#6]").unwrap();
let matches = get_reaction_smarts_matches(&rxn, &query);
let reactant_matches = &matches.reactant_matches.pattern_matches[0];
assert_eq!(reactant_matches.len(), 1);
assert_eq!(reactant_matches[0].molecule_index, 0);
assert!(!reactant_matches[0].atom_indices.is_empty());
let product_matches = &matches.product_matches.pattern_matches[0];
assert_eq!(product_matches.len(), 1);
assert_eq!(product_matches[0].molecule_index, 0);
assert!(!product_matches[0].atom_indices.is_empty()); }
#[test]
fn test_reactant_matches_get_pattern_matches() {
let rxn = rxn("CC>>CC");
let query = parse_reaction_query("[#6]>>[#6]").unwrap();
let matches = get_reaction_smarts_matches(&rxn, &query);
let reactant_pattern_0 = matches.reactant_matches.get_pattern_matches(0);
assert!(reactant_pattern_0.is_some());
assert_eq!(reactant_pattern_0.unwrap().len(), 1);
let reactant_pattern_1 = matches.reactant_matches.get_pattern_matches(1);
assert!(reactant_pattern_1.is_none());
}
#[test]
fn test_product_matches_get_pattern_matches() {
let rxn = rxn("CC>>CC");
let query = parse_reaction_query("[#6]>>[#6]").unwrap();
let matches = get_reaction_smarts_matches(&rxn, &query);
let product_pattern_0 = matches.product_matches.get_pattern_matches(0);
assert!(product_pattern_0.is_some());
assert_eq!(product_pattern_0.unwrap().len(), 1);
}
#[test]
fn test_pattern_matched_helper() {
let rxn = rxn("CC>>CC");
let query = parse_reaction_query("[#6]>>[#7]").unwrap();
let matches = get_reaction_smarts_matches(&rxn, &query);
assert!(matches.reactant_matches.pattern_matched(0));
assert!(!matches.product_matches.pattern_matched(0));
}
#[test]
fn test_multiple_patterns_or_logic() {
let rxn = rxn("CC>>CC");
let query = parse_reaction_query("[#6]|[#7]>>[#6]|[#8]").unwrap();
let matches = get_reaction_smarts_matches(&rxn, &query);
assert_eq!(matches.reactant_matches.pattern_matches.len(), 2);
assert_eq!(matches.product_matches.pattern_matches.len(), 2);
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);
}
#[test]
fn test_complex_reaction_matches() {
let rxn = rxn("CC(C)>>CC=C");
let query = parse_reaction_query("[#6]>>[#6]").unwrap();
let matches = get_reaction_smarts_matches(&rxn, &query);
assert!(matches.all_reactants_matched());
assert!(matches.all_products_matched());
assert!(matches.is_complete_match);
assert_eq!(matches.reactant_matches.pattern_matches[0].len(), 1);
assert!(!matches.reactant_matches.pattern_matches[0][0].atom_indices.is_empty());
assert_eq!(matches.product_matches.pattern_matches[0].len(), 1);
assert!(!matches.product_matches.pattern_matches[0][0].atom_indices.is_empty());
}
#[test]
fn test_rdkit_compat_check_legacy_format() {
use crate::query::rdkit_compat::*;
let result = check_rdkit_compatibility("[C:1][C:2]>>[C:2][C:1]").unwrap();
assert!(result.is_compatible);
assert_eq!(result.rdkit_format, RDKitFormat::Legacy);
assert!(result.warnings.is_empty());
}
#[test]
fn test_rdkit_compat_check_agents_format() {
use crate::query::rdkit_compat::*;
let result = check_rdkit_compatibility("[C:1][C:2]>[Pd]>[C:2][C:1]").unwrap();
assert!(result.is_compatible);
assert_eq!(result.rdkit_format, RDKitFormat::WithAgents);
assert!(result.warnings.is_empty());
}
#[test]
fn test_rdkit_compat_wildcard_warning() {
use crate::query::rdkit_compat::*;
let result = check_rdkit_compatibility("[*:1]>>[*:1]").unwrap();
assert!(!result.is_compatible);
assert!(result.warnings.iter().any(|w| w.contains("wildcard")));
}
#[test]
fn test_rdkit_compat_recursive_smarts_warning() {
use crate::query::rdkit_compat::*;
let result = check_rdkit_compatibility("[#6;$([#6]~[#8])]>>[#6]").unwrap();
assert!(!result.is_compatible);
assert!(result.warnings.iter().any(|w| w.contains("recursive")));
}
#[test]
fn test_rdkit_compat_missing_delimiter() {
use crate::query::rdkit_compat::*;
let result = check_rdkit_compatibility("[C:1][C:2][C:1][C:2]");
assert!(result.is_err());
}
#[test]
fn test_rdkit_format_detection_legacy() {
use crate::query::rdkit_compat::*;
let result = check_rdkit_compatibility("C>>C").unwrap();
assert_eq!(result.rdkit_format, RDKitFormat::Legacy);
}
#[test]
fn test_rdkit_format_detection_agents() {
use crate::query::rdkit_compat::*;
let result = check_rdkit_compatibility("C>[Pd]>C").unwrap();
assert_eq!(result.rdkit_format, RDKitFormat::WithAgents);
}
#[test]
fn test_rdkit_match_summary_complete() {
use crate::query::rdkit_compat::*;
let rxn = rxn("CC>>CC");
let query = parse_reaction_query("[#6]>>[#6]").unwrap();
let matches = get_reaction_smarts_matches(&rxn, &query);
let summary = match_to_rdkit_format(&matches);
assert!(summary.matched);
assert_eq!(summary.reactant_count, 1);
assert_eq!(summary.product_count, 1);
assert_eq!(summary.reactant_molecules_matched, 1);
assert_eq!(summary.product_molecules_matched, 1);
}
#[test]
fn test_rdkit_match_summary_incomplete() {
use crate::query::rdkit_compat::*;
let rxn = rxn("CC>>CC");
let query = parse_reaction_query("[#7]>>[#6]").unwrap();
let matches = get_reaction_smarts_matches(&rxn, &query);
let summary = match_to_rdkit_format(&matches);
assert!(!summary.matched);
assert_eq!(summary.reactant_molecules_matched, 0);
}
#[test]
fn test_rdkit_validate_conventions_valid() {
use crate::query::rdkit_compat::*;
let result = validate_rdkit_conventions("[C:1][C:2]>>[C:2][C:1]");
assert!(result.is_ok());
}
#[test]
fn test_rdkit_validate_conventions_no_delimiter() {
use crate::query::rdkit_compat::*;
let result = validate_rdkit_conventions("[C:1][C:2][C:1][C:2]");
assert!(result.is_err());
}
#[test]
fn test_rdkit_validate_conventions_triple_arrow() {
use crate::query::rdkit_compat::*;
let result = validate_rdkit_conventions("[C:1]>>>[C:1]");
assert!(result.is_err());
}
#[test]
fn test_rdkit_validate_conventions_map_number_zero() {
use crate::query::rdkit_compat::*;
let result = validate_rdkit_conventions("[C:0]>>[C:0]");
assert!(result.is_err());
if let Err(msg) = result {
assert!(msg.contains("outside RDKit range"));
}
}
#[test]
fn test_rdkit_validate_conventions_map_number_out_of_range() {
use crate::query::rdkit_compat::*;
let result = validate_rdkit_conventions("[C:1000]>>[C:1000]");
assert!(result.is_err());
if let Err(msg) = result {
assert!(msg.contains("outside RDKit range"));
}
}
#[test]
fn test_rdkit_validate_conventions_map_number_valid() {
use crate::query::rdkit_compat::*;
let result = validate_rdkit_conventions("[C:1][N:999]>>[N:999][C:1]");
assert!(result.is_ok());
}
#[test]
fn test_rdkit_compat_agents_pipes() {
use crate::query::rdkit_compat::*;
let result = check_rdkit_compatibility("[C:1]>[Pd]|[Ni]>[C:1]").unwrap();
assert!(result.is_compatible);
assert_eq!(result.rdkit_format, RDKitFormat::WithAgents);
}
#[test]
fn test_rdkit_compat_multiple_patterns() {
use crate::query::rdkit_compat::*;
let result = check_rdkit_compatibility("[#6]|[#7]>>[#8]|[#9]").unwrap();
assert!(result.is_compatible);
}
#[test]
fn test_batch_query_reactions_empty() {
let reactions: Vec<Reaction> = vec![];
let smarts = "[C:1]>>[C:1]";
let result = batch_query_reactions(&reactions, smarts);
assert!(result.is_ok());
let batch = result.unwrap();
assert_eq!(batch.total_reactions, 0);
assert_eq!(batch.matching_reactions, 0);
assert_eq!(batch.match_percentage, 0.0);
}
#[test]
fn test_batch_query_reactions_single_match() {
let rxn = parse_reaction("C>>C").unwrap();
let reactions = vec![rxn];
let smarts = "[C:1]>>[C:1]";
let result = batch_query_reactions(&reactions, smarts);
assert!(result.is_ok());
let batch = result.unwrap();
assert_eq!(batch.total_reactions, 1);
assert!(batch.matching_reactions > 0);
}
#[test]
fn test_batch_query_reactions_multiple() {
let rxn1 = parse_reaction("C>>C").unwrap();
let rxn2 = parse_reaction("CC>>C").unwrap();
let rxn3 = parse_reaction("CCC>>CC").unwrap();
let reactions = vec![rxn1, rxn2, rxn3];
let smarts = "[C:1]>>[C:1]";
let result = batch_query_reactions(&reactions, smarts);
assert!(result.is_ok());
let batch = result.unwrap();
assert_eq!(batch.total_reactions, 3);
assert_eq!(batch.matches.len(), 3);
}
#[test]
fn test_batch_query_results_indices() {
let rxn1 = parse_reaction("C>>C").unwrap();
let rxn2 = parse_reaction("CC>>C").unwrap();
let rxn3 = parse_reaction("CCC>>CC").unwrap();
let reactions = vec![rxn1, rxn2, rxn3];
let smarts = "[C:1]>>[C:1]";
let batch = batch_query_reactions(&reactions, smarts).unwrap();
let matching = batch.matching_indices();
let non_matching = batch.non_matching_indices();
assert_eq!(matching.len() + non_matching.len(), batch.total_reactions);
}
#[test]
fn test_reaction_pattern_library_new() {
let library = ReactionPatternLibrary::new();
assert!(library.is_empty());
assert_eq!(library.len(), 0);
}
#[test]
fn test_reaction_pattern_library_add_pattern() {
let mut library = ReactionPatternLibrary::new();
let pattern = parse_reaction_smarts("[C:1]>>[C:1]").unwrap();
library.add_pattern("simple_carbon".to_string(), pattern);
assert_eq!(library.len(), 1);
assert!(!library.is_empty());
assert!(library.get("simple_carbon").is_some());
}
#[test]
fn test_reaction_pattern_library_add_from_smarts() {
let mut library = ReactionPatternLibrary::new();
let result = library.add_pattern_from_smarts(
"acylation".to_string(),
"[C:1](=[O:2])[N:3]>>[C:1](=[O:2])[N:3]",
);
assert!(result.is_ok());
assert_eq!(library.len(), 1);
}
#[test]
fn test_reaction_pattern_library_multiple_patterns() {
let mut library = ReactionPatternLibrary::new();
let _ = library.add_pattern_from_smarts(
"reaction1".to_string(),
"[C:1]>>[C:1]",
);
let _ = library.add_pattern_from_smarts(
"reaction2".to_string(),
"[N:1]>>[N:1]",
);
let _ = library.add_pattern_from_smarts(
"reaction3".to_string(),
"[O:1]>>[O:1]",
);
assert_eq!(library.len(), 3);
let names = library.pattern_names();
assert_eq!(names.len(), 3);
assert!(names.contains(&"reaction1".to_string()));
}
#[test]
fn test_batch_query_with_library() {
let mut library = ReactionPatternLibrary::new();
let _ = library.add_pattern_from_smarts(
"carbon_only".to_string(),
"[C:1]>>[C:1]",
);
let rxn1 = parse_reaction("C>>C").unwrap();
let rxn2 = parse_reaction("CC>>C").unwrap();
let reactions = vec![rxn1, rxn2];
let results = batch_query_with_library(&reactions, &library);
assert!(!results.is_empty());
assert!(results.contains_key("carbon_only"));
let batch = &results["carbon_only"];
assert_eq!(batch.total_reactions, 2);
}
#[test]
fn test_query_reaction_simple() {
let rxn = parse_reaction("C>>C").unwrap();
let smarts = "[C:1]>>[C:1]";
let result = query_reaction(&rxn, smarts);
assert!(result.is_ok());
let match_result = result.unwrap();
assert!(!match_result.reactant_matches.pattern_matches.is_empty());
}
#[test]
fn test_batch_query_match_percentage() {
let rxn1 = parse_reaction("C>>C").unwrap();
let rxn2 = parse_reaction("CC>>C").unwrap();
let rxn3 = parse_reaction("CCC>>CC").unwrap();
let reactions = vec![rxn1, rxn2, rxn3];
let smarts = "[C:1]>>[C:1]";
let batch = batch_query_reactions(&reactions, smarts).unwrap();
assert!(batch.match_percentage >= 0.0);
assert!(batch.match_percentage <= 100.0);
let expected_percentage =
(batch.matching_reactions as f64 / batch.total_reactions as f64) * 100.0;
assert!((batch.match_percentage - expected_percentage).abs() < 0.01);
}
#[test]
fn test_reaction_pattern_library_default() {
let library = ReactionPatternLibrary::default();
assert!(library.is_empty());
}
}